再识ORM
Peewee是一个轻量级Python``ORM库。对于我,对ORM的经历了反反复复的爱恨交织过程。最早对其神奇之处的十分感叹;而后又鄙视其生成sql方式;如今,适时适当的在项目中使用ORM。
归根结底,对ORM态度的转变,源于对其认识的不足。最早接触ORM源自Django自带的ORM,用起来很爽。当时对sql并不熟悉,ORM了却了心头一大痛点。后来使用flask,没有自带的ORM,接触了sqlalchemy,也是一个很优秀的ORM。当时,我对于ORM的认识还是很浅薄,大概就认为ORM一个是数据库对象的map,其次就是能够生成sql查询。至于如何map,如何生成,如何执行等,基本上处于认知上的蛮荒时代。
直到为了学习Python的元类,并尝试写自己的ORM,才算是对ORM有一个完整的认识。以前的认识可以称之为对其原理和使用方式上的纠结。不明其根本原理,使用也一知半解。现在的认识更多源于工程哲学上方面。使用程序人生的一篇文章软件随想录:代码与数据对ORM工程上的总结如下:
我们用 ORM,或者 LINQ,而尽量减少 SQL 的使用,是因为我们通过把SQL这种嵌在代码里的字符串数据,转化成了代码,也就意味着我们拥有了编译时或者运行时的检查,乃至编辑时的检查 —— 各种 linter 很难检测出字符串中的SQL语法或者语义错误,但可以在你撰写 ORM代码时便提示你其中蕴含的语法/语义错误。这是非常伟大的一个进步 —— 对于软件工程来说,越早发现错误,消弭错误所花费的时间就越短。
诚然,ORM是数据层中的更高的抽象,在工程上,抽象程度越高,对于扩展和维护将会更有利。当然,ORM常被人诟病的一大理由就是程式生成的SQL没有原生的高效。的确,对于复杂的SQL语句,使用ORM的组合可能比原生的sql更不可读或者不可维护。此时,我们就需要在项目中继续使用raw sql的形式。
因此,在项目中使用数据层的抽象,ORM最好具备以下几个方面特性:
ORM和数据库可以相互独立存在,ORM可以选择是否引入关系约束。即数据库的字段可以大于ORM中定义的map,有的orm数据表字段和orm中的class必须一直,这就在已经在的表中引入orm会有点棘手。
ORM具备提供raw sql的功能。这样在orm无法方便提供查询的时候可以使用原生的sql解决问题。
ORM提供生成数据表和根据数据表生成model的特性。这样就在数据的merge时候方便的操作。
基于上述几点,发现Python的peewee基本满足我的要求。下面就peewee的简单使用方式做一个说明。例子主要取自官网的Quickstart。
分为两个部分,第一部分为基本使用,第二部分为整合到项目(以tornado)为例。
Peewee 快速开始
快速开始部分根据官网的文档为例子。
定义数据模型
顾名思义,ORM为数据对象关系的映射。首先我们需要定义数据模型(model)。新建一个文件learn_peewee.py。
import logging
from peewee import MySQLDatabase, Model, CharField, DateField, BooleanField, IntegerField
logger = logging.getLogger('peewee')
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler())
db = MySQLDatabase('test', host='127.0.0.1', user='root', passwd='', charset='utf8', port=3306)
class BaseModel(Model):
class Meta:
database = db
class Person(BaseModel):
name = CharField(verbose_name='姓名', max_length=10, null=False, index=True)
passwd = CharField(verbose_name='密码', max_length=20, null=False, default='123456')
email = CharField(verbose_name='邮件', max_length=50, null=True, unique=True)
gender = IntegerField(verbose_name='姓别', null=False, default=1)
birthday = DateField(verbose_name='生日', null=True, default=None)
is_admin = BooleanField(verbose_name='是否是管理员', default=True)
为了更好的测试学习,把log打开。首先,使用MySQLDatabase指定了所连接的数据库test。
然后定义了Person这个表的数据字段,如果不指定主键,peewee会自动帮我们创建一个id的字段作为主键。每一个Field都有几个参数可以配置,大概就是长度的大小,是否为空(null)和默认值(default),索引(index)和唯一索引(unique)几个常见的数据库选项。
创建数据表
定义好数据模型之后,下一步就是根据模型创建数据表了。
In [1]: from learn_peewee import *
In [2]: db
Out[2]:
In [3]: db.is_closed()
Out[3]: True
In [4]: db.connect()
In [6]: db.is_closed()
Out[6]: False
导入定义的数据库配置和数据模型。然后通过db.is_closed函数查看数据库的连接状态,使用db.connect函数创建连接。
☁ peewee-orm netstat -ant | grep -i 3306
tcp4 0 0 127.0.0.1.3306 127.0.0.1.61925 ESTABLISHED
tcp4 0 0 127.0.0.1.61925 127.0.0.1.3306 ESTABLISHED
tcp46 0 0 *.3306 *.* LISTEN
此时确实也能看见mysql的连接创建了。接下来就能创建数据表啦。使用模型的create_table方法。
In [5]: Person.sqlall()
Out[5]:
('CREATE TABLE `person` (`id` INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` VARCHAR(10) NOT NULL, `passwd` VARCHAR(20) NOT NULL, `email` VARCHAR(50), `gender` INTEGER NOT NULL, `birthday` DATE, `is_admin` BOOL NOT NULL)', [])
('CREATE INDEX `person_name` ON `person` (`name`)', [])
('CREATE UNIQUE INDEX `person_email` ON `person` (`email`)', [])
In [6]: Person.create_table()
('CREATE TABLE `person` (`id` INTEGER AUTO_INCREMENT NOT NULL PRIMARY KEY, `name` VARCHAR(10) NOT NULL, `passwd` VARCHAR(20) NOT NULL, `email` VARCHAR(50), `gender` INTEGER NOT NULL, `birthday` DATE, `is_admin` BOOL NOT NULL)', [])
('CREATE INDEX `person_name` ON `person` (`name`)', [])
('CREATE UNIQUE INDEX `person_email` ON `person` (`email`)', [])
通过sqlall方法可以看见peewee为我们生成的创建表的sql语句。执行的时候,log也显示了将要执行的sql。
如果数据表已经存在,执行create_table的时候,将会抛出异常。
default的含义
通过查看上面创建数据表的sql可以发现,default的行为很古怪。查看数据库的schema,所谓的数据表的defalut并不是我们在class中指定的。对于可以为null的字段。生成的schema中的字段的默认值为null,对于非null的字段,生成的默认值为其初始值(零值),即CharField(varchar)为空字串。InterFiled(int)则为0。
那么指定default的含义有什么用呢?default用于使用orm插入数据的时候,如果没有显示的给字段赋值,那么就会采用default指定的值。使用 create方法插入记录,使用save方法更新记录。
In [2]: p = Person.create(name='master')
('INSERT INTO `person` (`name`, `passwd`, `gender`, `is_admin`) VALUES (%s, %s, %s, %s)', [u'master', u'123456', 1, True])
In [3]: p.save()
('UPDATE `person` SET `name` = %s, `passwd` = %s, `gender` = %s, `is_admin` = %s WHERE (`person`.`id` = %s)', [u'master', u'123456', 1, True, 3])
Out[3]: 0L
可以看到,我们只指定了name字段的值,因为passwd和gender还有is_admin字段都指定default,此时插入的时候,就使用了默认的值。调用save方法的时候,执行是update语句,因此也同样执行了默认值。而没有指定默认值的字段,peewee则直接忽略。
In [4]: p.email = 'master@g.com'
In [5]: p.save()
('UPDATE `person` SET `name` = %s, `passwd` = %s, `email` = %s, `gender` = %s, `is_admin` = %s WHERE (`person`.`id` = %s)', [u'master', u'123456', u'master@g.com', 1, True, 3])
Out[5]: 1L
In [8]: p.passwd = '11'
In [9]: p.save()
('UPDATE `person` SET `name` = %s, `passwd` = %s, `email` = %s, `gender` = %s, `is_admin` = %s WHERE (`person`.`id` = %s)', [None, u'11', u'master@g.com', 1, True, 3])
Out[9]: 1L
In [8]: p = Person.get(id=1)
('SELECT `t1`.`id`, `t1`.`name`, `t1`.`passwd`, `t1`.`email`, `t1`.`gender`, `t1`.`birthday`, `t1`.`is_admin` FROM `person` AS t1 WHERE (`t1`.`id` = %s) LIMIT 1 OFFSET 0', [1])
In [9]: p.save()
('UPDATE `person` SET `name` = %s, `passwd` = %s, `email` = %s, `gender` = %s, `birthday` = %s, `is_admin` = %s WHERE (`person`.`id` = %s)', [u'master', u'11', u'master@g.com', 1, None, True, 1])
Out[9]: 0L
有默认值的字段,如果指定了值,就使用所赋的值。新读的数据,就不用考虑默认值了,如果读取的值没有变,更新的时候就依然采用这个值,与default值没有关系。
总结
default的设置与创建数据表的schema没有关系。仅与插入的时候有关系。
插入的时候,如果字段设置了default值,则会按照default指定的值插入,如果没有指定,同时字段可以为null,则数据库自动初始化值为null,如果字段不能为null,则数据库自动初始化为其零值。
最佳实践,如果字段为非Null,最好设置default值,同时数据库schema也设置其default值,如果字段为可以为null,那么初始值就设置为null即可。
快速认识了Peewee,接下来讨论peewee更强大的功能,例如定义关系和增删改查。