ORM模型

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.dbTableself.fields是從哪兒來的?但清晰的是,我的模板類仿照了Django,模板基類提供了統一的save()接口,能夠有效避免代碼重複。在模板基類中重寫__init__方法,是爲了支持模板類的使用方式:

# 第一種
stu = Student(name="zty", age=18)

# 第二種
stu = Student()
stu.name = "zty"
stu.age = 18
stu.save()

最後,關於self.dbTableself.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()

感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章