ORM模型
前言
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併發編程