注册 登录
  • 进入"运维那点事"后,希望您第一件事就是阅读“关于”栏目,仔细阅读“关于Ctrl+c问题”,不希望误会!

Django模型进阶

Python框架 彭东稳 11616次浏览 已收录 0个评论

一、模型关系

显然,关系数据库的威力体现在表之间的相互关联。 Django提供了三种最常见的数据库关系:多对一(many-to-one),多对多(many-to-many),一对一(one-to-one)。

1.1 多对一

Django使用django.db.models.ForeignKey定义多对一关系。和使用其它字段类型一样:在模型当中把它做为一个类属性包含进来。

ForeignKey需要一个位置参数:与该模型关联的类。

比如,一辆汽车(Car)有一个制造商(Manufacturer),但是一个制造商(Manufacturer)生产很多汽车(Car),每一辆汽车(Car)只能有一个制造商(Manufacturer)。使用下面的定义:

示例代码:Many-to-one relationship model example

1.2 多对多关系

ManyToManyField用来定义多对多关系,用法和其他Field字段类型一样,在模型中做为一个类属性包含进来。ManyToManyField在数据库中是通过中间表来实现的,Django会自动创建中间表来把另外两张表做个关联。

ManyToManyField需要一个位置参数:和该模型关联的类。

例如,一个披萨可以有多种馅料 ,一种馅料也可以位于多个披萨上。如下展示:

在哪个模型中设置ManyToManyField并不重要,在两个模型中任选一个即可,不要两个模型都设置。

通常,ManyToManyField实例应该位于可以编辑的表单中。在上面的例子中,toppings位于Pizza中(而不是在Topping里面设置pizzas的ManyToManyField字段),因为设想一个Pizza有多种Topping,比一个Topping位于多个Pizza上要更加自然。按照上面的方式,在Pizza的表单中将允许用户选择不同的Toppings。

示例代码:Many-to-many relationship model example

1.3 一对一关系

OneToOneField用来定义一对一关系。用法和其他字段类型一样:在模型里面做为类属性包含进来。

当某个对象想扩展自另一个对象时,最常用的方式就是在这个对象的主键上添加一对一关系。

OneToOneField要一个位置参数:与模型关联的类。

例如,你想建一个“places”数据库,里面有一些常用的字段,比如address、phone number等等。接下来,如果你想在Place数据库的基础上建立一个Restaurant 数据库,而不想将已有的字段复制到Restaurant模型,那你可以在Restaurant添加一个OneToOneField字段,这个字段指向Place(因为Restaurant本身就是一个Place;事实上,在处理这个问题的时候,你应该使用一个典型的 继承,它隐含一个一对一关系)。

示例代码:One-to-one relationship model example

二、元选项(meta)

对于Model的元选项,你可以在每个模型的Meta类中设置他们,这里给出一些可能常用。

  • abstract

如果abstract = True,就表示模型是抽象基类,也就是说在做迁移时并不会应用到数据库,做模型继承时使用。

  • db_table

用来设置该模型所用的数据表的名称,默认是App名称_Model名称。如:db_table = ‘music_album’。

  • default_related_name

这个名字会默认被用于一个关联对象到当前对象的关系,默认为<model_name>_set。

  • ordering

对象默认的顺序,获取一个对象的列表时使用:ordering = [‘-order_date’]。它是一个字符串的列表或元组,可有多个字段,每个字符串是一个字段名,前面带有可选的“-”前缀表示倒序。前面没有“-”的字段表示正序。使用”?”来表示随机排序。

  • proxy

如果proxy = True,它作为另一个模型的子类,将会作为一个代理模型。

  • unique_together

用来设置的不重复的字段组合,就是用来设置唯一索引约束。如:unique_together = ((“driver”, “restaurant”),)。

  • index_together

用来设置带有索引的字段组合,就是用来设置普通索引。如:index_together = [[“pub_date”, “deadline”],]。

比如,修改polls/models.py中Question模型,如下:

然后需要重新migrate,如下:

最后应用数据库,我们所做的更改就生效了,如下:

三、原始SQL查询

在模型查询API不够用的情况下,你可以使用原始的SQL语句。Django提供两种方法使用原始SQL进行查询:一种是使用Manager.raw()方法,进行原始查询并返回模型实例;另一种是完全避开模型层,直接执行自定义的SQL语句。

raw()管理器方法用于原始的SQL查询,并返回模型的实例:

这个方法执行原始的SQL查询,并返回一个django.db.models.query.RawQuerySet实例。这个RawQuerySet实例可以像一般的查询集那样,通过迭代来提供对象实例。

通过例子展示一下, 假设存在以下模型:

你可以像这样执行自定义的SQL语句:

当然,这个例子和直接使用Quesiton.objects.all()的结果一模一样。但是,raw()拥有其它更强大的使用方法。

另外,默认情况下表名是通过“应用名称_模型名称”组合而成的。我们这里是通过元选项db_table而改变的。

3.1 索引访问

raw()方法支持索引访问,所以如果只需要第一条记录,可以这样写:

然而,索引和切片并不在数据库层面上进行操作。如果数据库中有很多的Question对象,更加高效的方法是在SQL层面限制查询中结果的数量:

向raw()方法中传递参数

如果你需要参数化的查询,可以向raw()方法传递params参数。

但不要在原始查询中使用字符串格式化!类似于这种样子:

千万不要。使用params参数可以完全防止SQL注入攻击,它是一种普遍的漏洞,使攻击者可以向你的数据库中注入任何SQL语句。如果你使用字符串格式化,早晚会受到SQL注入攻击。 只要你记住默认使用 params 参数,就可以免于攻击。

3.2 直接执行自定义的SQL

有时Manager.raw()方法并不十分好用,你不需要将查询结果映射成模型,或者你需要执行UPDATE、 INSERT以及DELETE查询。

在这些情况下,你可以直接访问数据库,完全避开模型层。

django.db.connection对象提供了常规数据库连接的方式。为了使用数据库连接,先要调用connection.cursor()方法来获取一个游标对象之后,调用cursor.execute(sql, [params])来执行sql语句,调用cursor.fetchone()或者cursor.fetchall()来返回结果行。

注意如果你的查询中包含百分号字符,你需要写成两个百分号字符,以便能正确传递参数:

默认情况下,Python DB API会返回不带字段的结果,这意味着你得到的是一个列表,而不是一个字典。花费一点性能代价之后,你可以返回一个字典形式的结果,像这样:

下面是一个体现二者区别的例子:

四、模型继承

Django中的Model继承和Python中的类继承非常相似,只不过你要选择具体的实现方式:让父Model拥有独立的数据库;还是让父Model只包含基本的公共信息,而这些信息只能由子Model呈现。

Django中有三种继承关系:

  • 抽象基类

通常,你只是想用父Model来保存那些你不想在子Model中重复录入的信息。父类是不使用的也就是不生成单独的数据表,这种情况下使用抽象基类继承Abstract base classes。

如果你想把某些公共信息添加到很多Model中,抽象基类就显得非常有用。你编写完基类之后,在Meta内嵌类中设置abstract=True,该类就不能创建任何数据表。然而如果将它做为其他Model的基类,那么该类的字段就会被添加到子类中。抽象基类和子类如果含有同名字段,就会导致错误(Django将抛出异常)。

SQL结果:

按照我们指定的名称student_info生成了table。

继承时,Django会对基类的Meta内嵌类做一个调整:在安装Meta属性之前,Django会设置abstract=False。 这意味着抽象基类的子类不会自动变成抽象类。当然,你可以让一个抽象类继承另一个抽象基类,不过每次都要显式地设置abstract=True 。

对于抽象基类而言,有些属性放在Meta内嵌类里面是没有意义的。例如,包含db_table将意味着所有的子类(是指那些没有指定自己的Meta内嵌类的子类)都使用同一张数据表,一般来说,这并不是我们想要的。

小心使用related_name (Be careful with related_name)

如果你在ForeignKey或ManyToManyField字段上使用related_name属性,你必须总是为该字段指定一个唯一的反向名称。但在抽象基类上这样做就会引发一个很严重的问题。因为Django会将基类字段添加到每个子类当中,而每个子类的字段属性值都完全相同 (这里面就包括related_name)。注:这样使用ForeignKey或ManyToManyField反向指定时就无法确定是指向哪个子类了。当然如果必须想用,也是有方法可以解决的。

  • 多表继承

如果你想从现有的Model继承并让每个Model都有自己的数据表,那么使用多重表继承Multi-table inheritance。

使用这种继承方式时,同一层级下的每个子Model都是一个真正意义上完整的Model 。每个子Model都有专属的数据表,都可以查询和创建数据表。继承关系在子 Model和它的每个父类之间都添加一个链接 (通过一个自动创建的OneToOneField来实现)。 例如:

SQL结果:

父类和子类都生成了单独的数据表,Restaurant中存储了Place的id,也就是通过OneToOneField链接在一起。继承关系通过表的JOIN操作来表示。在JPA中称作JOINED。这种方式下,每个表只包含类中定义的字段,不存在字段冗余,但是要同时操作子类和所有父类所对应的表。

Place里面的所有字段在Restaurant中也是有效的,只不过数据保存在另外一张数据表当中。所以下面两个语句都是可以运行的:

如果你有一个Place,那么它同时也是一个Restaurant,意思就是说此条“name=”bishengke”,address=”shanghai xuhui”,serves_hot_dogs=True”记录必须在两个表中都唯一。那么你可以使用子Model的小写形式从Place对象中获得与其对应的Restaurant对象:

或者通过Restaurant对象获取Place对象:

在多表继承中,子类继承父类的Meta内嵌类是没什么意义的。所有的Meta选项已经对父类起了作用,再次使用只会起反作用。(这与使用抽象基类的情况正好相反,因为抽象基类并没有属于它自己的内容)

  • 代理继承

最后,如果你只想在Model中修改Python-level级的行为,而不涉及字段改变。代理Model (Proxy models) 适用于这种场合。

使用多表继承(multi-table inheritance) 时,Model的每个子类都会创建一张新数据表,通常情况下,这正是我们想要的操作。这是因为子类需要一个空间来存储不包含在基类中的字段数据。但有时,你可能只想更改Model在Python层的行为实现。比如:更改默认的manager,或是添加一个新方法。

而这,正是代理Model继承方式要做的:为原始Model创建一个代理(proxy)。你可以创建,删除,更新代理Model的实例,而且所有的数据都可以象使用原始Model一样被保存。不同之处在于:你可以在代理Model中改变默认的排序设置和默认的manager,更不会对原始Model产生影响。

声明代理Model和声明普通Model没有什么不同。设置Meta内置类中proxy的值为True,就完成了对代理Model的声明。

举个例子,假设你想给Django自带的标准User model (它被用在你的模板中)添加一个方法:

说明:MyPerson只是一个代理,数据库中不会建立表。MyPerson类和它的父类Person操作同一个数据表。特别的是,Person的任何实例也可以通过MyPerson访问,反之亦然:

另外我们在MyPerson模型中定义了一个方法do_something,那么就可以在MyPerson中使用了,但是Person是没有这个方法的,如下:

你也可以使用代理Model给Model定义不同的默认排序设置。Django自带的User model没有定义排序设置(这是故意为之,是因为排序开销极大,我们不想在获取用户时浪费额外资源)。你可以利用代理对username属性进行排序,这很简单:

普通的User查询,其结果是无序的;而OrderedUser查询的结果是按username排序。

多重继承(Multiple inheritance)

和Python一样,Django的Model也可以做多重继承。这里要记Python的名称解析规则。如果某个特定名称 (例如,Meta) 出现在第一个基类当中,那么子类就会使用第一个基类的该特定名称。例如,如果多重父类都包含Meta内嵌类,只有第一个基类的Meta才会被使用,其他的都被会忽略。

一般来说,没必要使用多重继承。

原文:Django中的Model继承 


如果您觉得本站对你有帮助,那么可以支付宝扫码捐助以帮助本站更好地发展,在此谢过。
喜欢 (0)or分享 (0)
关于作者:

您必须 登录 才能发表评论!