ORM模型

Python 小记 2019-02-07 10408 字 1031 浏览 点赞

前言

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()

感谢



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论