模型层

orm

在我们常说的MVC模型或MTV模型中, ORM操作就占了很大的比重, 其中的模型(M)就代表的对数据库的操作. 没有ORM, 我们只能通过pymysql等模块操作数据库, 然后再传入原生的sql语句字符串来实现对数据库的增删改查, 而这就导致一个问题, 我们作为程序员, 不是专业的DBA, 写出来的sql语句可能效率很低, 并且不易维护, 并且还极易出错, 可能会把许多时间花在调试sql语句上, 这样开发效率就非常低下.

此外, 数据模型与数据库不能实现解耦, 一旦我们需要更换数据库, 就需要对代码进行大量的修改. 基于这种种原因, orm (对象关系映射)就被发明出来, 它能让程序员以熟悉的操作对象的方式来操作数据库, 能够轻松的实现创造表, 增删改查记录, 而无需编写一条原生sql语句, 并且它能让我们的数据模型不依赖与具体的数据库, 只需要在配置文件中简单的改动一些配置, 就能轻松的更换数据库.

总结上面的话, ORM就是能将程序员编写的代码翻译成sql语句, 再通过pymysql等模块来直接操作数据库,相当于在它的基础上又封装了一层.

创建模型表和数据库表

在Django利用orm操作可以帮我们自动在数据库中创建表.

  1. 首先在app目录下的models模块下创建一个User模型类
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=20)
    age = models.IntegerField()
  1. 配置数据库

这里我们选择mysql作为最后的存储引擎, 这时候就可以在settings文件做一个简单的配置了.

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'test',
        'USER': 'root',
        'PASSWORD': '123',
        'HOST': 'localhost',
        'PORT': 3306
    }
}

更改数据库之后还不要忘记在项目或app目录下的__init__文件中添加下面两句话

import pymysql
pymysql.install_as_MySQLdb()
  1. 迁移

现在只是简单的在逻辑层面上和数据库建立了关系, 还需要在数据库中真正的创建表.

这需要在shell环境下执行下面两句命令行:

python3 manage.py makemigrations
python3 manage.py migrate

创建上面的表相当于在数据库执行下面的sql语句

CREATE TABLE `app01_user` ( 
	`id` INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, 
	`name` VARCHAR ( 20 ) NOT NULL, 
	`age` INTEGER NOT NULL 
);
  1. 完成

到此模型表就与数据的库表建立一对一的关系了.

需要注意的是

  • 当我们在表中没有显示的声明哪个字段是主键的时候, ORM会自动为我们创建一个名为id的主键字段
  • 表名app01_user是由Django默认生成的, 规则是app名称_模型类名
  • 我们每一次更新了模型表中的字段的信息了, 需要重新执行上面两条迁移命令, 来与数据实现同步.

常用字段和参数

字段合集

AutoField(Field)
    - int自增列,必须填入参数 primary_key=True

BigAutoField(AutoField)
    - bigint自增列,必须填入参数 primary_key=True

    注:当model中如果没有自增列,则自动会创建一个列名为id的列
    from django.db import models

    class UserInfo(models.Model):
        # 自动创建一个列名为id的且为自增的整数列
        username = models.CharField(max_length=32)

    class Group(models.Model):
        # 自定义自增列
        nid = models.AutoField(primary_key=True)
        name = models.CharField(max_length=32)

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):
    - 可以为空的布尔值

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)
    - 字符串类型,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   上传图片的宽度保存的数据库字段名(字符串)

DateTimeField(DateField)
    - 日期+时间格式 YYYY-MM-DD HH:MM[:ss[.uuuuuu]][TZ]

DateField(DateTimeCheckMixin, Field)
    - 日期格式      YYYY-MM-DD

TimeField(DateTimeCheckMixin, Field)
    - 时间格式      HH:MM[:ss[.uuuuuu]]

DurationField(Field)
    - 长整数,时间间隔,数据库中按照bigint存储,ORM中获取的值为datetime.timedelta类型

FloatField(Field)
    - 浮点型

DecimalField(Field)
    - 10进制小数
    - 参数:
        max_digits,小数总长度
        decimal_places,小数位长度

BinaryField(Field)
    - 二进制类型

字段参数

通用字段
(1)null
如果为True,Django 将用NULL 来在数据库中存储空值。 默认值是 False.
 
(2)blank
 
如果为True,该字段允许不填。默认为False。
要注意,这与 null 不同。null纯粹是数据库范畴的,而 blank 是数据验证范畴的。
如果一个字段的blank=True,表单的验证将允许该字段是空值。如果字段的blank=False,该字段就是必填的。
 
(3)default
 
字段的默认值。可以是一个值或者可调用对象。如果可调用 ,每有新对象被创建它都会被调用。
 
(4)primary_key
 
如果为True,那么这个字段就是模型的主键。如果你没有指定任何一个字段的primary_key=True,
Django 就会自动添加一个IntegerField字段做为主键,所以除非你想覆盖默认的主键行为,
否则没必要设置任何一个字段的primary_key=True。
 
(5)unique
 
如果该值设置为 True, 这个数据字段的值在整张表中必须是唯一的
 
(6)choices
由二元组组成的一个可迭代对象(例如,列表或元组),用来给字段提供选择项。 如果设置了choices ,默认的表单将是一个选择框而不是标准的文本框,<br>而且这个选择框的选项就是choices 中的选项。

时间字段参数:
auto_now_add
配置auto_now_add=True,创建数据记录的时候会把当前时间添加到数据库。

auto_now
配置上auto_now=True,每次更新数据记录的时候会更新该字段。

上面两个参数互斥,不能共存
关系字段-ForeignKey
to
设置要关联的表

to_field
设置要关联的表的字段

on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。

models.CASCADE

删除关联数据,与之关联也删除

db_constraint
是否在数据库中创建外键约束,默认为True。

关系字段-OneToOne
to
设置要关联的表。

to_field
设置要关联的字段。

on_delete
当删除关联表中的数据时,当前表与其关联的行的行为。(参考上面的例子)

与外键的行为类似, 一对一字段一般应用在扩展字段上.

单表操作

添加表记录

添加单条数据

假设当前以User表为例

class User(models.Model):
    name = models.CharField(max_length=20)
    
    def __str__(self):
        return self.name

添加单条数据有两种方式:

方式一

# create方法添加数据会直接返回创建出来的对象
user = models.User.objects.create(name='user1')
print(user)
# out: user1

方式2

# save方法是先创建一个User对象, 再通过调用对象的save方法, 才能执行保存到数据库的操作.
user = models.User(name='user2')
user.save()

添加多条数据

添加多条数据, 我们第一反应就是通过循环调用create方法或save方法来执行插入操作, 但是这样的效率非常低. 需要通过频繁的请求数据库才能完成操作

为了避免这种效率低下的方式, 就需要用到bulk_create方式来进行批量操作

users = [models.User(name=f'user{i}') for i in range(10)]
models.User.objects.bulk_create(users)	
# (0.003) INSERT INTO `user` (`name`) VALUES ('user0'), ('user1') ...; args=('user0', 'user1', 'user2', ...) 
# 循环创建多个对象
for i in range(10):
    models.User.objects.create(name=f'user{i}')
    
# (0.003) INSERT INTO `app01_user` (`name`) VALUES ('user0'); args=['user0']
# (0.000) INSERT INTO `app01_user` (`name`) VALUES ('user1'); args=['user1']
# (0.001) INSERT INTO `app01_user` (`name`) VALUES ('user2'); args=['user2']
...

上面的方式可以通过在settings文件中配置打印sql语句就可以看出来, 批量操作只请求了数据库一次, 而create方法则每创建一个对象就需要请求一次数据库.

删除表记录

方式一: 直接多个删除, 在QuerySet对象直接调用delete方法. 会把符合条件的全部都删除.

models.User.objects.filter(pk__gt=6).delete()

方式二: 获取到单个对象, 直接调用delete方法来进行删除

user = models.User.objects.filter(pk=2).first()  # type: models.User
user.delete()

注意点:

  • 不能直接在User.objects对象上执行delete操作
  • 不能在valuesvalues_list上执行delete操作
  • Django删除对象默认的是级联删除, 删除带有外键 约束的对象的时候, 会连带被约束的对象一起删除.

修改表记录

修改表记录同样有两种方式.

方式一: 直接在QuerySet对象上执行修改操作

models.User.objects.filter(pk=4).update(name='user666')
# UPDATE `app01_user` SET `name` = 'user666' WHERE `app01_user`.`id` = 4;
user = models.User.objects.filter(pk=4).first()  # type: models.User
  

方式二: 在具体的对象上执行save方法, 来进行修改操作.

user.name = 'user777'
user.save()
# UPDATE `app01_user` SET `name` = 'user777', `age` = 18 WHERE `app01_user`.`id` = 4

区别:

  • 查看执行的sql语句, update只会对对应的字段进行修改, save方法会对所有字段进行修改, update效率更高
  • update是可以进行批量修改的.

小疑问: Django执行save方法如何正确的区分insert或update方法?

参考官方文档了解了原因.

内部是这么做逻辑判断的.

  1. 如果对象的主键存在的话(主键不为空或是空字符串), 就执行update操作
  2. 如果对象的主键存在的话或者当执行了update操作之后, 但是没有任何影响, 这时候会执行insert操作

针对上面第二条, 说明我们最好不要主动添加主键, 因为会多走一次数据库查询. 就像下面这样

# UPDATE `app01_user` SET `name` = 'user10', `age` = 18 WHERE `app01_user`.`id` = 10;
# INSERT INTO `app01_user` (`id`, `name`, `age`) VALUES (10, 'user10', 18);

实际多执行了一条sql语句.

查询表记录

查询是ORM的重中之重, 熟练掌握查询的操作是十分有必要的, 初始, 如果我们需要理解ORM和数据操作之间的关系, 可以通过两种方式查看.

  • 当执行查询操作获得了querySet对象的时候, 可以调用query属性查看当前操作执行sql语句
  • 直接在settings文件中配置logging参数, 直接打印出每条sqla语句的转换结果.

logging配置文件

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'handlers': {
        'console':{
            'level':'DEBUG',
            'class':'logging.StreamHandler',
        },
    },
    'loggers': {
        'django.db.backends': {
            'handlers': ['console'],
            'propagate': True,
            'level':'DEBUG',
        },
    }
}

查询API

假设当前的模型类的结构如下所示

class Book(models.Model):
    name = models.CharField(max_length=32)
    price = models.FloatField()
    pub_time = models.DateField()
    
    def __str__(self):
       return  self.name
    
表里数据:
1	三国演义    22.2	 2019-08-14
2	红楼梦		33.3	 2018-02-22
3	三国演义2   20		2019-10-25
4	水浒传		10		2017-01-02
5	射雕英雄传  24	   2018-02-12
6	西游记		54.9	2019-07-10

<1> all(): 查询所有结果

models.Book.objects.all()
<QuerySet [<Book: 三国演义>, <Book: 红楼梦>, <Book: 三国演义2>, <Book: 水浒传>, <Book: 射雕英雄传>, <Book: 西游记>]>


<2> filter(**kwargs): 它包含了与所给筛选条件相匹配的对象, 筛选条件之间是and的关系.

models.Book.objects.filter(price__gt=21.0, name__contains='三国演义')
<QuerySet [<Book: 三国演义>]>

<3> get(**kwargs): 返回与所给筛选条件相匹配的对象,返回结果有且只有一个,如果符合筛选条件的对象超过一个或者没有都会抛出错误, 这个方法不是很好用

print(models.Book.objects.get(name='三国演义'))
print(models.Book.objects.get(name__contains='三国演义'))

# 第二个报错 get() returned more than one Book -- it returned 2!

<4> exclude(**kwargs): 它包含了与所给筛选条件不匹配的对象

print(models.Book.objects.exclude(name__contains='三国演义'))
<QuerySet [<Book: 红楼梦>, <Book: 水浒传>, <Book: 射雕英雄传>, <Book: 西游记>]>

<5> order_by(*field): 对查询结果排序, 默认是升序, 在字段前面加-号表示降序

print(models.Book.objects.order_by('price'))
print(models.Book.objects.order_by('-price'))

<QuerySet [<Book: 水浒传>, <Book: 三国演义2>, <Book: 三国演义>, <Book: 射雕英雄传>, <Book: 红楼梦>, <Book: 西游记>]>
<QuerySet [<Book: 西游记>, <Book: 红楼梦>, <Book: 射雕英雄传>, <Book: 三国演义>, <Book: 三国演义2>, <Book: 水浒传>]>

<6> reverse(): 对查询结果反向排序

print(models.Book.objects.order_by('price').reverse())
<QuerySet [<Book: 西游记>, <Book: 红楼梦>, <Book: 射雕英雄传>, <Book: 三国演义>, <Book: 三国演义2>, <Book: 水浒传>]>
# 结果与上面的反序相同

<8> count(): 返回数据库中匹配查询(QuerySet)的对象数量

print(models.Book.objects.all().count())
# 6

<9> first(): 返回第一条记录, 这个经常使用

print(models.Book.objects.first())
# 三国演义

<10> last(): 返回最后一条记录

print(models.Book.objects.last())
西游记

<11> exists(): 如果QuerySet包含数据,就返回True,否则返回False

print(models.Book.objects.filter(name='1234').exists())
False

<12> values(*field): 返回一个ValueQuerySet——一个特殊的QuerySet,运行后得到的并不是一系列model的实例化对象,而是一个可迭代的字典序列.

print(models.Book.objects.filter(price__gt=24).values('name', 'pub_time'))
<QuerySet [{'name': '红楼梦', 'pub_time': datetime.date(2018, 2, 22)}, {'name': '西游记', 'pub_time': datetime.date(2019, 7, 10)}]>

<13> values_list(*field): 它与values()非常相似,它返回的是一个元组序列,values返回的是一个字典序列

print(models.Book.objects.filter(price__gt=24).values_list('name', 'pub_time'))
<QuerySet [('红楼梦', datetime.date(2018, 2, 22)), ('西游记', datetime.date(2019, 7, 10))]>

<14> distinct(): 从返回结果中剔除重复纪录, 对查询结果的每一个字段都做比较.

注意点

  • 返回结果是QuerySet对象的话, 支持链式操作, 可以一直做过滤操作
  • QuerySet对象实行的是惰性查询, 对QuerySet对象进行切片, 过滤, 创建, 传递值都不会真正查询数据库
  • 对于查询结果是否存在, 使用exists方法比bool值判断更高效

对于会真正执行数据操作的主要有以下操作

  • 迭代
  • 带有步长的切片
  • len()
  • list
  • bool

双下划线查询

models.Tb1.objects.filter(id__lt=10, id__gt=1)   # 获取id大于1 且 小于10的值
 
models.Tb1.objects.filter(id__in=[11, 22, 33])   # 获取id等于11、22、33的数据
models.Tb1.objects.exclude(id__in=[11, 22, 33])  # not in
 
models.Tb1.objects.filter(name__contains="ven")  # 获取name字段包含"ven"的
models.Tb1.objects.filter(name__icontains="ven") # icontains大小写不敏感
 
models.Tb1.objects.filter(id__range=[1, 3])      # id范围是1到3的,等价于SQL的bettwen and
 
类似的还有:startswith,istartswith, endswith, iendswith 

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周来表示,因为天数是不确定的,老外就是按周来计算薪资的哦~

posted @ 2019-09-28 08:16  yscl  阅读(297)  评论(0编辑  收藏  举报