Django Model
目录
1. ORM 的基本概念
ORM 是 ‘对象关系映射’, 就是把我们定义的对象(类)映射到数据库的表上, 所以ORM就是代码(软件)层面对于数据库表和关系的一种抽象
Django 的Model就是ORM的一个具体实现
简单来说就是继承了Django的Model, 然后定义了对应的字段, Django就会帮我们吧Model对应到数据库的表上, Model中定义的属性(比如: name = model.CharField(max_length=50, verbose_name='名称'))就是对应表的一个字段, 所以一个Model也就对应关系数据库中的一张表, 而对于有关联的Model, 比如用到了ForeignKey的Model, 就是通过外键关联的表
举个例子:
class Foo(model.Model): name = model.CharField(max_length=50)
对应到数据库的表
+-----------+---------------+--------+-------+------------------+
| Field | Type | Null | Key | Extra | +-----------+---------------+--------+--------------------------+
| id | int(11) | NO | PRI | auto_increment |
| name | varchar(20) | NO | | |
+-----------+---------------+--------+-------+------------------+
表中的id是Django的Model内置字段, 可以被重写
类中的属性对应MySQL中的字段, 属性类型对应MySQL字段类型, 属性定义是传递的参数定义了字段的其他属性, 比如长度、是否允许为空等
在MySQL中, 一个表中的字段有多种类型, 比如int、varchar和datetime等. 因此我们定义Model中的字段时就需要使用不同的类型
在Model中字段类型跟MySQL中字段的类型对应是ORM中基本规则, 理解了字段类型跟数据库的映射规则, 在思考一下Model的定义跟表的对应, 你就能理解什么是ORM了. 其实就是把我们定义的数据模型对应到数据库表上, 或者反过来说也成立, 把数据库的表对应到我们定义的数据模型上. 理解了这些还不够, 数据库有数据操作语言(DML), 可以通过SQl语句对数据做CURD操作, 在Django中是使用QuerySet实现, 这里就先不介绍
简单的ORM实现
# orm 文件 from orm_pool.mysql_pool import Mysql class Field(object): def __init__(self, name, column_type, primary_key, default): self.name = name self.column_type = column_type self.primary_key = primary_key self.default = default class StringField(Field): def __init__(self, name, column_type='varchar(255)', primary_key=False, default=None): super().__init__(name, column_type, primary_key, default) class IntegerField(Field): def __init__(self, name, column_type='int', primary_key=False, default=None): super().__init__(name, column_type, primary_key, default) class MyMetaClass(type): def __new__(cls, class_name, class_bases, class_addrs): if class_name == 'Models': return type.__new__(cls, class_name, class_bases, class_addrs) primary_key = None mappings = {} table_name = class_addrs.get('table_name', class_name) for k, v in class_addrs.items(): if isinstance(v, Field): mappings[k] = v if v.primary_key: if primary_key: raise TypeError('只能拥有一个主键!!!') primary_key = k if not primary_key: return TypeError('必须拥有一个主键!!!') for k in mappings.keys(): class_addrs.pop(k) class_addrs['table_name'] = table_name class_addrs['mappings'] = mappings class_addrs['primary_key'] = primary_key return type.__new__(cls, class_name, class_bases, class_addrs) class Models(dict, metaclass=MyMetaClass): def __init__(self, **kwargs): super().__init__(**kwargs) def __getattr__(self, item): return self.get(item, '暂无此键!!!') def __setattr__(self, key, value): self[key] = value @classmethod def select(cls, **kwargs): conn = Mysql() if not kwargs: sql = 'select * from %s' % cls.table_name res = conn.select(sql) else: key = list(kwargs.keys())[0] value = kwargs[key] sql = 'select * from %s where %s = ?' % (cls.table_name, key) sql = sql.replace('?', '%s') res = conn.select(sql, value) if res: return [cls(**r) for r in res] def update(self): conn = Mysql() # sql = 'update user set name='dasd',age='da' where id = 'dasd''; primary_key = None keys = [] values = [] for k, v in self.mappings.items(): if v.primary_key: primary_key = getattr(self, v.name, v.default) else: keys.append(v.name + '=?') values.append(getattr(self, v.name, v.default)) sql = 'update %s set %s where %s=%s' % (self.table_name, ','.join(keys), self.primary_key, primary_key) sql = sql.replace('?', '%s') conn.execute(sql, values) def save(self): conn = Mysql() # sql = 'insert into user(name,age) value ('dasd','dasda')'; keys = [] values = [] args = [] for k,v in self.mappings.items(): if not v.primary_key: keys.append(v.name) values.append(getattr(self, v.name, v.default)) args.append('?') sql = 'insert into %s(%s) value (%s)'%(self.table_name, ','.join(keys), ','.join(args)) sql = sql.replace('?', '%s') print(sql) print(values) conn.execute(sql, values) # if __name__ == '__main__': # class User(Models): # table_name = 'user' # id = IntegerField(name='id', primary_key=True) # name = StringField(name='name') # password = StringField(name='password') # is_vip = IntegerField(name='is_vip') # is_locked = IntegerField(name='is_locked') # user_type = StringField(name='user_type') # register_time = StringField(name='register_time') # # obj = User( # name = 'dasd', # password = 'dada', # is_vip = 0, # is_locked = 0, # user_type = 'user', # register_time = '2019-05-26 20:41:15', # ) # obj.save()
# 数据库操作文件 import pymysql from orm_pool.db_pool import POOL class Mysql(object): def __init__(self): self.connect = POOL.connection() self.cursor = self.connect.cursor(pymysql.cursors.DictCursor) def mysql_close(self): self.cursor.close() self.connect.close() def select(self, sql, args=None): self.cursor.execute(sql, args) res = self.cursor.fetchall() return res def execute(self, sql, args): try: self.cursor.execute(sql, args) except BaseException as e: print(e)
这个orm是通过重写__new__方法实现, 对传过来的数据进行处理然后全部添加进class_addrs中, 添加, 查询, 修改也是对class_addre中的数据做操作
2. 字段类型
理解了ORM的基本概念和规则之后, 剩下需要了解的就是具体实现, 有了基础规则的理解之后, 下面这些工具的性质东西会变得很简单. 我们把Django中常用的字段类型以及参数配置进行说明, 这里可以根据类型来划分, 这其实就是数据库中字段类型的划分
2.1 数值类型
这些类型都是数值相关, 比如AutoField, 上面也看到了它在MySQL中的类型为int(11), 下面对每个字段做具体介绍
AutoField(Field) int(11) - int自增列,Django默认提供, 可以被重写完整的是 id = model.AutoField(primary_key=True) BigAutoField(AutoField) bigint() - bigint自增列,用法痛AutoField, 映射到数据库中成为20位bigint类型 SmallIntegerField(IntegerField): - 小整数 -32768 ~ 32767 PositiveSmallIntegerField(PositiveIntegerRelDbTypeMixin, IntegerField) - 正小整数 0 ~ 32767 IntegerField(Field) - 整数列(有符号的) -2147483648 ~ 2147483647 PositiveIntegerField(PositiveIntegerRelDbTypeMixin, IntegerField) - 正整数 0 ~ 2147483647 BigIntegerField(IntegerField): - 长整型(有符号的) -9223372036854775808 ~ 9223372036854775807 BooleanField(Field) - 布尔值类型 一般记录状态 NullBooleanField(Field): - 可以为空的布尔值 DecimalField(Field) - 10进制小数 开发对数据精度要求较高的业务时考虑使用, 比如做支付相关、金融相关. 定义时需要指定精确到多少位 - 参数: max_digits,小数总长度 decimal_places,小数位长度 DurationField(Field) - 长整数,时间间隔,数据库中按照bigint存储,ORM中获取的值为datetime.timedelta类型 FloatField(Field) - 浮点型
2.2 字符类型
下面这些字段都是用了存储字符数据的, 对应到MySQL中有两种类型: longtext 和 varchar. 除了TextField是longtext类型外, 其他均属于varchar类型.
CharField(Field) - 字符类型 - 必须提供max_length参数, max_length表示字符长度 TextField(Field) - 文本类型 EmailField(CharField): - 字符串类型,Django Admin以及ModelForm中提供验证机制 IPAddressField(Field) - 字符串类型,Django Admin以及ModelForm中提供验证 IPV4 机制 GenericIPAddressField(Field) - 字符串类型,Django Admin以及ModelForm中提供验证 Ipv4和Ipv6 - 参数: protocol,用于指定Ipv4或Ipv6, 'both',"ipv4","ipv6" unpack_ipv4, 如果指定为True,则输入::ffff:192.0.2.1时候,可解析为192.0.2.1,开启此功能,需要protocol="both" URLField(CharField) - 字符串类型,Django Admin以及ModelForm中提供验证 URL SlugField(CharField) - 字符串类型,Django Admin以及ModelForm中提供验证支持 字母、数字、下划线、连接符(减号) CommaSeparatedIntegerField(CharField) - 字符串类型,格式必须为逗号分割的数字 UUIDField(Field) char(32) - 字符串类型,Django Admin以及ModelForm中提供对UUID格式的验证 FilePathField(Field) - 字符串,Django Admin以及ModelForm中提供读取文件夹下文件的功能 - 参数: path, 文件夹路径 match=None, 正则匹配 recursive=False, 递归下面的文件夹 allow_files=True, 允许文件 allow_folders=False, 允许文件夹 FileField(Field) - 字符串,路径保存在数据库,文件上传到指定目录 - 参数: upload_to = "" 上传文件的保存路径 storage = None 存储组件,默认django.core.files.storage.FileSystemStorage ImageField(FileField) - 字符串,路径保存在数据库,文件上传到指定目录 - 参数: upload_to = "" 上传文件的保存路径 storage = None 存储组件,默认django.core.files.storage.FileSystemStorage width_field=None, 上传图片的高度保存的数据库字段名(字符串) height_field=None 上传图片的宽度保存的数据库字段名(字符串)
2.3 日期类型
下面3个都是日期类型, 分别对应MySQl的date、 datetime、 time
DateTimeField(DateField) - 日期+时间格式 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ] DateField(DateTimeCheckMixin, Field) - 日期格式 YYYY-MM-DD TimeField(DateTimeCheckMixin, Field) - 时间格式 HH:MM[:ss[.uuuuuu]]
2.4 关系类型
其中外键和一对一其实是一种, 只是一对一在外键的字段上加了unique, 而多对多会创建一个中间表, 来进行多对多关联
ForeignKey - 外键类型在ORM中用来表示外键关联关系,一般把ForeignKey字段设置在 '一对多'中'多'的一方。 - ForeignKey可以和其他表做关联关系同时也可以和自身做关联关系。 - 字段参数 to 设置要关联的表 to_field 设置要关联的表的字段 on_delete 当删除关联表中的数据时,当前表与其关联的行的行为。 models.CASCADE 删除关联数据,与之关联也删除 db_constraint 是否在数据库中创建外键约束,默认为True。
OneToOneField - 一对一字段。 - 通常一对一字段用来扩展已有字段。(通俗的说就是一个人的所有信息不是放在一张表里面的,简单的信息一张表,隐私的信息另一张表,之间通过一对一外键关联) - 字段参数 to 设置要关联的表。 to_field 设置要关联的字段。 on_delete 当删除关联表中的数据时,当前表与其关联的行的行为。(参考上面的例子)-
ManyToManyField "关联管理器"是在一对多或者多对多的关联上下文中使用的管理器。 它存在于下面两种情况: 外键关系的反向查询 多对多关联关系
2.5 参数
上面介绍了常用的类型以及不同字段类型的差异. 接着我们需要了解这些字段都提供了哪些参数供我们使用
null 可以同blank对比考虑, 其中null用于设定在数据库层面是否允许为空
blank
针对业务层面, 该值是否允许为空
choices
配置字段的choices之后, 在admin页面上就可以看到对应的可选项展示
db_cloumn
默认情况下, 我们定义的Field就是对应数据库中的字段名称, 通过这个参数可以指定Model中的某个字段对应数据库中的那个字段
db_index
索引配置, 对于业务上需要经常作为查询条件的字段吗应该配置此选项
default
默认值配置
editable 是否可编辑, 默认是true, 如果不想将这个字段展示到页面上吗可以配置费False
error_messages
用于自定义字段值校验失败时的异常提示, 它是字典格式. key的可选项为null、blank、invalid、invalid_choice、unique和unique_for_date
help_text
字段提示语, 配置这一选项后, 再也没对应字段的下方会展示此配置
primary_key
主键, 一个model只允许设置一个字段为primary_key
unique 唯一约束, 当需要配置值唯一字段时, 如果设置为unique=True,则该字段在此表中必须是唯一的 设置此选项之后, 不需要设置db_index
unique_for_month
针对月份的联合约束
unique_for_year
针对年份的联合约束
verbose_name
字段对应的展示文案
validators
自定义校验逻辑, 同form类似
DateField和DateTimeField auto_now_add 配置auto_now_add=True,创建数据记录的时候会把当前时间添加到数据库。 auto_now 配置上auto_now=True,每次更新数据记录的时候会更新该字段。
3. QuerySet的使用
3.1 QuerySet的概念
在Django的Model中, QuerySet是一个重要的概念, 必须了解! 因为我们同数据库的所有查询以及更新交互都是通过它完成的
在Model层中, Django通过Model增加一个objects属性来提供数据操作的接口. 比如, 想要查询所有文章的数据, 可以这么写: Post.objects.all(), 这样就可以拿到QuerySet对象. 这个对象中包含了我们需要的数据, 当我们用到它时, 他回去DB中获取数据
这样描述你可能有点奇怪, 为什么是用到数据是才回去DB中查询, 而不是执行Post.objects.all()时区执行数据库查询语句. 其原因是QuerySet 要支持链式操作. 如果没出执行都要查询数据库的话, 会存在性能问题, 因为你可能用不到你执行的代码, 举个例子,也顺便说下链式调用
比方说, 我们有下面的代码:
posts = Post.objects.all() available_posts = posts.filter(status=1)
如果这条语句要立即执行, 就会出现这种情况: 先执行 Post.objects.all(), 拿到所有的数据posts, 然后在执行过滤, 拿到所有上线状态的文章 available_posts, 这样就会产生两次数据库请求, 并且两次查询存在重复数据
当然, 平时可能不会出现这么低级的错误, 但是当代码比较复杂是, 谁也无法保证不会出现类似的问题
因此, Django中的QuerySet中的QuerySet本质上是一个懒加载的对象, 上面的两行代码执行后, 都不会产生数据库查询操作, 只是会返回一个QuerySet对象, 等你真正用到它时才会执行查询, 下面通过代码解释一下:
posts = Post.objects.all() # 返回一个QuerySet对象并赋值给posts available_posts = posts.filter(status=1) # 继续返回一个QuerySet对象并赋值给available_posts print(avaliable_posts) # 此时会根据上面的两个条件执行数据查询操作, 对应的SQL语句为:SELECT * FROM blog_post WHERE status = 1;
所以这部分的重点就是理解QuerySet的懒加载的, 在日常开发中我们遇到的一部分性能问题就是因为开发人员没有立即QuerySet特性
上面说到的链式调用, 就是执行一个对象的方法之后得到的结果还是这个对象, 这样可以接着执行对象上的其他方法, 比如下面这个代码
posts = Post.objects.filter(status=1).filter(catagory_id=2).filter(title__icontains='123')
每个函数(或者方法)的执行结果上可以继续调用同样的方法, 因为每个函数的返回值都是它自己, 也就是QuerySet
3.2 常用的QuerySet接口
这里根据是否支持链式调用分类进行介绍
3.2.1 支持链式调用的接口
支持链式调用的接口即返回QuerySet的接口, 具体如下:
all
相当于 SELECT * FROM table_name语句, 用于查询所有数据
filter
顾名思义, 根据条件过滤数据, 常用的条件基本上是字段等于、不等于、大于、小于. 当然还有其他的, 比如能改成产生LIKE查询: Model.objects.filter(content__contains='条件')
exclude
同filter, 只是相反的逻辑
reverse
把QuerySet中的结果倒序排列
distinct
用于去重查询, 产生SELECT DISTINCT这样的SQL查询
none
返回空的QuerySet
3.2.2 不支持链式调用的接口
不支持链式调用的接口即返回值不是QuerySet的接口, 具体如下:
get
比如 Post.objects.get(id=1)用于查询id为1的文章: 如果存在, 则直接返回对应的Post实例; 如果不存在, 则抛出DoesNotExist异常. 所以我们一般情况下都使用try: except Post.DoesNotExist
create
用来直接创建一个Model对象, 比如post = Post.objects.create(title='123')
get_or_create
根据条件查询, 如果没查到, 就调用create创建
update_or_create
同get_or_create, 只用用来做更新操作
count
用于返回QuerySet有多少条记录, 相当于SELECT COUNT(*) FROM table_name
latest
用于返回最新的一条记录, 但是需要在Model的Meta中定义: get_latest_by = <用来排序的字段>
earliest
同上, 返回最早一条记录
first
从当前QuerySet记录中获取第一条
last
同上, 获取最后一条
exists
返回True或False, 在数据库层面执行SELECT (1) AS 'a' FROM table_name LIMIT 1; 的查询, 如果只是需要判断QuerySet是否有数据, 用这个接口是最合适的方式.
不要用count或者len(queryset)这样的操作来判断是否存在. 相反, 如果可以预期接下来会用到QuerySet中的数据, 可以考虑使用len(queryset)方式来做判断, 这样可以减少一次DB请求
bulk_create
同create, 用来批量创建记录
in_bulk
批量查询, 接受两个参数 id_list 和 filed_name. 可以通过Post.objects.in_bulk([1, 2, 3])查询出id为1, 2, 3的数据, 返回结果是字典类型, 字典类型的key为查询条件
update
用来根据条件批量更新记录, 比如: Post.objects.filter(owner_name='123').update(title='456')
delete
同update, 这个接口是用来根据条件批量删除记录,需要注意的是, update和delete都会触发Django的signal
values
当我们明确知道只需要返回某个字段的值, 不需要Model实例时, 可以使用它, 如: title_list = Post.objects.filter(category=1).values('title')
# 返回结果类似: <QuerySet[{'title': xxx}]>
values_list
同values, 但是直接返回的是包含tuple的QuerySet, 如: title_list = Post.objects.filter(category=1).values_list('title')
# 返回结果类似: <QuerySet[('标题')]>
如果只是一个字段的话, 可以通过增加flat=True参数, 便于我们后续处理
title_list = Post.objects.filter(category=1).values_list('title', flat=True)
for title in title_list:
print(title)
3.3 接口进阶
在优化Django项目时, 有其要考虑这几种接口的用法
defer
把不需要展示的字段做延迟加载. 比如说, 需要获取到文章中除正文以外的其他字段, 就可以通过posts = Post.objects.all().defer('content'),这样拿到的记录就不会包含content部分, 但是当我们需要用到这个字段时, 在使用时会去加载, 下面通过代码演示:
posts = Post.objects.all().defer('content') for post in posts: # 此时会执行数据库查询 print(post.content) # 此时会执行数据查询, 获取到content
当不想加载某个过大的字段时(如: text 类型字段), 会使用defer, 但是上面的演示代码会产生N+1的查询问题, 在实际使用时千万要注意!
only
同defer接口刚好想法, 如果只想获取到所有的title记录, 就可以使用only, 只获取title的内容, 其他值在获取时会产生额外的查询
select_related
这就使用来解决一对一外键产生N+1问题的方案
posts = Post.objects.all() for post in posts: # 产生数据库查询 print(post.owner) # 产生额外的数据库查询
这里使用的是owner(关联表), 他的解决方法是用select_related接口
posts = Post.objects.all().select_related('category') for post in posts: # 产生数据库查询, category数据也会一次性查询出来 print(post.category)
prefetch_related
针对多对多关系的数据, 可以通过这个接口来避免N+1查询
posts = Post.objects.all().prefetch_related('tag') for post in posts: # 产生亮条查询语句, 分别查询post和tag print(post.tag.all())
3.4 常用的字段查询
如果你知道某个查询在SQL中如何实现, 可以对应来看Django提供的接口, 常用的查询关键字有:
contains 包含, 用来进行相似查询 Post.objects.filter(name__contains='yyy') # 获取name字段包含 yyy 的数据 icontains 同contains, 只是忽略大小写 Post.objects.filter(name__icontains='yyy') # 大小写不敏感 exact 精确匹配, 相当于 = 号 Post.objects.filter(name__'yyy') 同等于 Post.objects.filter(name='yyy') iexact 同exact, 只是忽略大小写 in 指定某个集合 Post.objects.filter(id__in=[1,2,3]) # 获取id等于 1,2,3的数据 gt 大于某值 Post.objects.filter(id__gt=1) # 获取id大于1的数据 gte 大于等于某值 Post.objects.filter(id__gte=1) # 获取id大于等于1的数据 lt 小于某值 Post.objects.filter(id__lt=10) # 获取id小于10的数据 lte 小于等于某值 Post.objects.filter(id__lte=10) # 获取id小于等于10的数据 startswitch 以某个字符串开头, 与contains类似, 只是会产生LIKE '关键字%' 这样的SQL istartswith 同startswith, 忽略大小写 endswith 以某个字符串结尾 iendswith 同iendswith, 忽略大小写 range 范围查询, 多用于时间范围 Post.objects.filter(id__range[1, 3]) # id范围是1到3的数据等价于bettwen and Post.objects.filter(created_time__range=('2018-05-01', '2018-06-01')) SELECT ... WHERE created_time BETTWEN '2018-05-01' AND '2018-06-01'
date字段还可以: models.Class.objects.filter(first_day__year=2017) date字段可以通过在其后加__year,__month,__day等来获取date的特点部分数据 # date # # Entry.objects.filter(pub_date__date=datetime.date(2005, 1, 1)) # Entry.objects.filter(pub_date__date__gt=datetime.date(2005, 1, 1)) # year # # Entry.objects.filter(pub_date__year=2005) # Entry.objects.filter(pub_date__year__gte=2005) # month # # Entry.objects.filter(pub_date__month=12) # Entry.objects.filter(pub_date__month__gte=6) # day # # Entry.objects.filter(pub_date__day=3) # Entry.objects.filter(pub_date__day__gte=3) # week_day # # Entry.objects.filter(pub_date__week_day=2) # Entry.objects.filter(pub_date__week_day__gte=2) 需要注意的是在表示一年的时间的时候,我们通常用52周来表示,因为天数是不确定的,老外就是按周来计算薪资的哦~
3.5 进阶查询
除了上面基础的查询表达式外, Django还提供了其他封装, 用来满足更复杂的查询, 比如: SELECT ... WHERE id = 1 OR id = 2 这样的查询, 用上面的基础查询就无法满足
F
F 表达式常用来执行数据库层面计算, 从而避免出现竞争状态. 比如需要处理每篇文章的访问量, 假设存在post.pv这样的字段, 当有用户访问时, 我们对其加1:
post = Post.objects.get(id=1) post.pv = post.pv + 1 post.save()

浙公网安备 33010602011771号