前言
ORM是三个单词首字母组合而成,包含了Object(对象-类),Relations(关系),Mapping(映射)。解释过字面意思,但ORM的概念仍然模糊。私以为要理解一个事物,最好的法子是搞明白它出现是为了解决什么问题。
然而,ORM是否应该存在仍被许多程序员争论着。我所实习的公司,项目组负责封装数据层抽象出接口的程序员也为此跟leader激烈讨论过。我同他持相同观点,即:过于复杂的sql操作不应该让ORM代为实现,原生sql会让代码简单,目的明确。离开复杂的“层次”,代码也易维护。
显然,上面一段话是在说ORM不适合的场景。那它的对立面,也就是ORM大展拳脚的领域了。当项目中的sql语句比较简单,复用率大,映射关系清晰,通过ORM避开sql语句的直接使用,其实是不错选择。
Django中的ORM
在Django中,有现成的ORM模型,利用它,往数据库写东西变得简洁。我在models.py文件做了如下定义:
from django.db import models
class Student(models.Model):
# db_column 指定列名
name = models.CharField(db_column="NAME", max_length=20)
age = models.IntegerField(db_column="AGE")
class Meta:
db_table = "student" # 指定表名
然后执行两个命令。第一个是生成迁移文件,第二个是执行迁移文件:
python manage.py makemigrations
python manage.py migrate
此时查看数据库,发现student表已经创建好了(Django会默认创建id列)。
mysql> show tables;
+----------------------------+
| Tables_in_model |
+----------------------------+
| ...... |
| student |
+----------------------------+
11 rows in set (0.00 sec)
mysql> desc student;
+-------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| NAME | varchar(20) | NO | | NULL | |
| AGE | int(11) | NO | | NULL | |
+-------+-------------+------+-----+---------+----------------+
3 rows in set (0.00 sec)
现在利用Django提供的接口,向数据库写入数据。
>>> from studymodel.models import Student
>>> stu = Student()
>>> stu.name = "zty"
>>> stu.age = 19
>>> stu.save()
一个简单的ORM模型
体验过Django中的ORM后,发现模板类操作数据库在一定环境下确实带来了方便。事实上,我们也可以通过元类来实现自己的ORM。下面将涉及两个知识点:元类,描述符。因为已经在《元类》和《属性描述符》中总结过了,后面不再细述。
首先,完成属性描述符的设计:
class Field(object):
pass
class IntegerField(Field):
def __init__(self, col=None, minvalue=None, maxvalue=None):
self._value = None
self.col = col
if not isinstance(maxvalue, numbers.Integral):
raise ValueError("'maxvalue'需要一个整数")
self.maxvalue = maxvalue or 100
if not isinstance(minvalue, numbers.Integral):
raise ValueError("'minvalue'需要一个整数")
self.minvalue = minvalue or 0
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if not isinstance(value, numbers.Integral):
raise ValueError("'age'需要一个整数")
if not self.minvalue < value <= self.maxvalue:
raise ValueError("'age'的取值范围在[%s, %s]" % (self.minvalue, self.maxvalue))
self._value = value
class CharField(Field):
def __init__(self, col=None, maxlen=None):
self._value = None
self.col = col
if not (isinstance(maxlen, numbers.Integral) and maxlen > 0):
raise ValueError
self.maxlen = maxlen or 10
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if not (isinstance(value, str) and len(value) < self.maxlen):
raise ValueError
self._value = value
为了方便管理描述符们,让他们继承自一个父类是行之有效的办法。
然后,实现模板类:
class ModelBase(metaclass=ModelMeta):
def __init__(self, *args, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def save(self):
table = self.dbTable
fields = []
for k, v in self.fields.items():
# 如果col为有效值,则列名为col对应的键值,否则用属性名作为列名
fields.append(getattr(v, "col") or k)
# 构建sql语句
values = [getattr(self, field) for field in fields]
valuesStrList = ["'"+str(item)+"'" for item in values]
sql = "INSERT INTO {table}({fields}) VALUES({values})".format(
table=table,
fields=",".join(fields),
values=",".join(valuesStrList)
)
create_table(table)
try:
if cursor.execute(sql):
conn.commit()
print("保存成功")
except Exception:
conn.rollback()
raise
class Student(ModelBase):
name = CharField(col="", maxlen=10)
age = IntegerField(col="", minvalue=12, maxvalue=19)
class Meta: # 表名
dbTable = "SCHOOL"
单看这里的代码,细节上会有些难以明白,比如self.dbTable和self.fields是从哪儿来的?但清晰的是,我的模板类仿照了Django,模板基类提供了统一的save()接口,能够有效避免代码重复。在模板基类中重写__init__
方法,是为了支持模板类的使用方式:
# 第一种
stu = Student(name="zty", age=18)
# 第二种
stu = Student()
stu.name = "zty"
stu.age = 18
stu.save()
最后,关于self.dbTable和self.fields两个属性,其实是通过元类动态注入的:
class ModelMeta(type):
def __new__(cls, name, bases, attrs):
# 如果是模板基类,不做处理
if name == "ModelBase":
return super().__new__(cls, name, bases, attrs)
# 用文件名伪造模板名
attrs.update(__module__=os.path.basename(__file__)[:-3]) # 构建模板名
# 如果存在Meta属性,且设置了属性dbTable的值,则dbTable的值为数据库中表的名字
meta = attrs.pop("Meta", {})
dbTable = getattr(meta, "dbTable", None)
# 否则,自动设置名字。格式:"模板名_类名"。
if dbTable is None:
dbTable = attrs["__module__"] + "_" + name
# 将与数据库相关内容存放进fields字段中
fields = {}
for k, v in attrs.items():
# 利用父类Field对其子类统一管理
if isinstance(v, Field):
fields[k] = v
attrs["dbTable"] = dbTable
attrs["fields"] = fields
return super().__new__(cls, name, bases, attrs)
为方便测试,我还写了create_table()
和drop_table()
方法。
完整代码
说明:以下代码不能直接投入生产使用,功能既不全,也存在明显BUG。这里只是为了理解Django中ORM模型实现(尽管源码更为复杂),同时加深对元类的印象。
import os
import numbers
import pymysql
# 连接数据库
conn = pymysql.connect(host="127.0.0.1", port=3306, user="root", password="******", db="model")
# 创建一个游标
cursor = conn.cursor()
class Field(object):
pass
class IntegerField(Field):
def __init__(self, col=None, minvalue=None, maxvalue=None):
self._value = None
self.col = col
if not isinstance(maxvalue, numbers.Integral):
raise ValueError("'maxvalue'需要一个整数")
self.maxvalue = maxvalue or 100
if not isinstance(minvalue, numbers.Integral):
raise ValueError("'minvalue'需要一个整数")
self.minvalue = minvalue or 0
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if not isinstance(value, numbers.Integral):
raise ValueError("'age'需要一个整数")
if not self.minvalue < value <= self.maxvalue:
raise ValueError("'age'的取值范围在[%s, %s]" % (self.minvalue, self.maxvalue))
self._value = value
class CharField(Field):
def __init__(self, col=None, maxlen=None):
self._value = None
self.col = col
if not (isinstance(maxlen, numbers.Integral) and maxlen > 0):
raise ValueError
self.maxlen = maxlen or 10
def __get__(self, instance, owner):
return self._value
def __set__(self, instance, value):
if not (isinstance(value, str) and len(value) < self.maxlen):
raise ValueError
self._value = value
class ModelMeta(type):
def __new__(cls, name, bases, attrs):
# 如果是模板基类,不做处理
if name == "ModelBase":
return super().__new__(cls, name, bases, attrs)
# 用文件名伪造模板名
attrs.update(__module__=os.path.basename(__file__)[:-3]) # 构建模板名
# 如果存在Meta属性,且设置了属性dbTable的值,则dbTable的值为数据库中表的名字
meta = attrs.pop("Meta", {})
dbTable = getattr(meta, "dbTable", None)
# 否则,自动设置名字。格式:"模板名_类名"。
if dbTable is None:
dbTable = attrs["__module__"] + "_" + name
# 将与数据库相关内容存放进fields字段中
fields = {}
for k, v in attrs.items():
# 利用父类Field对其子类统一管理
if isinstance(v, Field):
fields[k] = v
attrs["dbTable"] = dbTable
attrs["fields"] = fields
return super().__new__(cls, name, bases, attrs)
class ModelBase(metaclass=ModelMeta):
def __init__(self, *args, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def save(self):
table = self.dbTable
fields = []
for k, v in self.fields.items():
# 如果col为有效值,则列明为col对的键值,否则用属性名作为列名
fields.append(getattr(v, "col") or k)
# 构建sql语句
values = [getattr(self, field) for field in fields]
valuesStrList = ["'"+str(item)+"'" for item in values]
sql = "INSERT INTO {table}({fields}) VALUES({values})".format(
table=table,
fields=",".join(fields),
values=", ".join(valuesStrList)
)
create_table(table)
try:
if cursor.execute(sql):
conn.commit()
print("保存成功")
except Exception:
conn.rollback()
raise
class Student(ModelBase):
name = CharField(col="", maxlen=10)
age = IntegerField(col="", minvalue=12, maxvalue=19)
class Meta: # 表名
dbTable = "school"
def create_table(name):
"""创建表"""
try:
cursor.execute("CREATE TABLE %s(name VARCHAR (10) NOT NULL , age INT DEFAULT 0)" % name)
conn.commit()
print("创建【%s】成功" % name)
except pymysql.err.InternalError as e:
if "exists" in e.args[1]:
pass
def drop_table(name):
"""删除表"""
try:
cursor.execute("DROP TABLE %s" % name)
conn.commit()
print("删除【%s】成功" % name)
except pymysql.err.InternalError as e:
if "Unknown table" in e.args[1]:
pass
if __name__ == "__main__":
"""测试代码"""
s = Student(name="zty", age=18)
s.save()
f = Student()
f.name = "guanf"
f.age = 18
f.save()
# drop_table("school")
conn.close()
感谢
- 参考慕课Bobby老师课程Python高级编程和异步IO并发编程
还不快抢沙发