美文网首页程序员
SQLAlchemy告别手动建模,实现动态写库

SQLAlchemy告别手动建模,实现动态写库

作者: 玩物励志老乐 | 来源:发表于2020-11-25 19:13 被阅读0次

问题

无论哪种编程语言,哪个实现了ORM的框架,对于数据库的增删改查操作,基本都是先根据表结构,手动定义一个数据模型。如果表结构发生了变化,则这个数据模型类也必须进行修改,否则就会报错
那么有没有这样一种办法,就是开发者无需知道数据库表结构,也不用担心库表结构有变化,就能完成增删改查,且不用定义模型类呢?
在python里,答案是肯定的,用SQLAlchemy的另一种写法,就能做到。

思路

SQLAlchemy有一种用法,是先通过连接字符串创建引擎,然后通过反射机制,拿到数据库表的结构。再使用核心方法Insert,将反射出来的类和你要新增的字典数据进行组合之后得以实现。下面我来详细说明。

举例说明

假设有一张表,student,表结构如下(简书的markdown竟然不支持表格):

| 字段名 | 数据类型 | 不为空 | 备注 |
| id | int | not null | primary_key |
| nickname | varchar(20) | not null | 学生姓名 |
| gender | tinyint | not null | 性别 |

常规建模:
# 手动ORM
class Student(db.Models)
    id = db.Column(Integer, primary_key=True, nullable=False)
    nickname = db.Column(String(20), nullable=False)
    gender = db.Column(Integer, nullable=False)
# 插入数据
stu = Student(nickname="阿花", gender=2)
db.session.add(stu)
db.session.commit()

看起来还不错。但是,如果我增加了一个age字段,那上面就必须要修改了。必须增加age字段,参考gender的写法。

动态模式:
conn_str = "mssql+pyodbc://{1}:{2}@{0}:1433/{3}?driver=ODBC+Driver+17+for+SQL+Server".format(db_host, db_user, db_password, db_name)
# 连接数据库
engine = create_engine(conn_str)
meta = MetaData()
conn = engine.connect()

# 反射数据库
meta.reflect(bind=engine, only=[])
# 建立模型
Base = automap_base(metadata=self.meta)
Base.prepare()
# 获取表对象,进行插入操作
Student = Base.classes["student"]
data_dict = {
    "nickname": "小明",
    "gender": 1,
    "age": 18
}
ins = Insert(Student, values=data_dict)
conn.execute(ins)

讲解

连接数据库的部分和手动ORM是一样的,只是我们使用了meta对象来做反射和自动映射。插入数据时不能再和以前一样使用先实例化对象模型,在用session.add()来插入了,而是用核心方法Insert,参数是表模型和字典对象

使用自动映射,其实也是客户的强烈要求,他希望所有的数据处理逻辑都是数据库配置的。所以在操作数据时,是不能参考表结构,完全读取配置的。用这种方式实现了之后,确实我们不用关心表结构了,给我啥数据,我都能正常操作且不用修改业务代码,也适合一些架构设计场景或数据接口场景。

还有一事

后来我发现,如果要插入的数据不合法,就会报错,稳健性不够好。比如数据包含了不存在的字段,或者缺失了非空字段等等。

于是我增加了一个检查字段的方法,插数据前,先调用这个方法,可以过滤掉不存在的字段,且为缺失了的字段构建默认值。代码如下:

# 检查数据是否存在不合法的字段名
# @param data_dict dict 要插入的数据
# @param table_name str 表名
# @return dict
    def checkFields(self, data_dict, table_name):
        insp = reflection.Inspector.from_engine(self.engine)   # 通过反射拿到映射对象
        columns = insp.get_columns(table_name)   # 获得某个表的所有字段集合
        checked_dict = {}
        # 去掉表结构不存在的字段
        for dk in data_dict.keys():
             # 备注1
            if dk in map(lambda x: x["name"], columns):
                checked_dict[dk] = data_dict[dk]
        # 为表结构存在但数据里没有的字段设置默认值(timestamp除外)
        for field in columns:
            # 备注2
            if field["name"] not in checked_dict and isinstance(field["type"], Numeric):
                checked_dict[field["name"]] = 0
            elif field["name"] not in checked_dict and isinstance(field["type"], DateTime):
                checked_dict[field["name"]] = "1753-01-01 00:00:00.000"
            elif field["name"] not in checked_dict and field["name"] != "timestamp":
                checked_dict[field["name"]] = ""
        return checked_dict
  • 备注1:data_dict是要插入的数据。先取出所有的key,和表结构的所有的列columns进行对比,去掉不存在的key。为了防止循环套循环不好看,所以这里就用了map+lambda的方式进行列表对列表的检查。
  • 备注2:遍历库表的所有列,读取类型,赋予不同类型相应的默认值。比如,列是Numeric的子类,说明是数字类型,自动补上默认值0。DateTime的子类,则给了默认时间“1753-01-01”(这是SQLServer的默认时间,MySQL的话就1980-01-01了)。

总结

动态写库的核心,就是利用SQLAlchemy的反射机制拿到表对象,调用核心Insert即可。可以通过一些代码对数据做一些预处理。其他3个操作碍于时间和篇幅不再展开,大家可以自行去实现,思路都一样。

相关文章

网友评论

    本文标题:SQLAlchemy告别手动建模,实现动态写库

    本文链接:https://www.haomeiwen.com/subject/olkniktx.html