数据模型定义

SQLAlchemy ORM中,数据被封装为了声明式的数据模型,框架会自动映射数据表内的字段到模型中,我们操作数据其实就是操作数据模型的实例。这篇笔记我们详细学习SQLAlchemy中数据模型的定义。

模型基础使用

下面是SQLAlchemy中数据模型定义的一个例子。

CREATE TABLE `t_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(20) NOT NULL,
  `email` varchar(50) NOT NULL,
  `password` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
)
from sqlalchemy import BigInteger, String
from sqlalchemy.orm import mapped_column, DeclarativeBase, Mapped


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = 't_user'

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
    username: Mapped[str] = mapped_column(String(20), nullable=False)
    email: Mapped[str] = mapped_column(String(50))
    password: Mapped[str] = mapped_column(String(255), nullable=False)

SQLAlchemy中,数据模型类都要继承DeclarativeBase类。代码中,我们首先基于DeclarativeBase创建了声明式基类Base,这样我们后续的其它数据模型都可以继承这个Base来定义。创建Base类的意义在于集中管理元数据,这样编写后所有继承Base的模型类都可以通过Base.metadata统一管理,此外它可以提供通用的元数据管理、公共配置和扩展能力。

User类是我们自定义的数据模型,__tablename__属性指定了表名,其它属性对应于具体的数据表字段,字段对象需要用mapped_column()函数生成,其第一个参数是数据表中的字段类型,BigInteger通常在MySQL中将被映射为bigint(20)String(20)将被映射为varchar(20),以此类推;此外函数还支持许多命名参数,name可以指定与Python类属性名不同的列名(常用于支持特殊的数据库字段命名规范场景),primary_key用于设置主键字段,nullable用于指定该字段是否可以为空(默认为True),此外还有index指定索引列,unique指定列是否使用唯一索引。

idusername等属性我们都使用了Mapped[]进行类型标注,它们主要用于Python的类型检查器和IDE的智能提示,不会影响ORM框架的具体行为,因此这些类型标注是可选的,即使不标注代码也可以正常运行,不过一般来说还是推荐添加标注。

关于Python类型、SQLAlchemy类型和MySQL类型之间的关系可以参考下表。

Python类型标注 SQLAlchemy类型 MySQL类型
Mapped[int] SmallInteger() SMALLINT
Mapped[int] Integer() INT
Mapped[int] BigInteger() BIGINT
Mapped[str] String(length) VARCHAR(length)
Mapped[str] Text() TEXT
Mapped[float] Float() FLOAT
Mapped[float] Double() DOUBLE
Mapped[bool] Boolean() BOOL或TINYINT(1)
Mapped[datetime] DateTime() DATETIME
Mapped[datetime] TIMESTAMP(timezone=True) TIMESTAMP
Mapped[date] Date() DATE
Mapped[time] Time() TIME
Mapped[bytes] LargeBinary() BLOB
Mapped[Decimal] Numeric(precision, scale) DECIMAL(precision, scale)
Mapped[dict] JSON() JSON

定义关联映射

SQLAlchemy中关联映射需要使用relationship()函数定义,不过SQLAlchemy默认并不会自动创建外键,我们需要显式的使用ForeignKey()定义外键字段。下面我们分别介绍如何在SQLAlchemy中创建一对多、一对一和多对多关系。

一对多关联

下面例子中,我们创建了User用户和Order订单模型,一个用户可以对应多个订单,它们具有一对多关系。

from typing import List

from sqlalchemy import BigInteger, String, ForeignKey
from sqlalchemy.orm import mapped_column, DeclarativeBase, Mapped, relationship


class Base(DeclarativeBase):
    pass


class User(Base):
    __tablename__ = 't_user'

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
    username: Mapped[str] = mapped_column(String(20), nullable=False)
    email: Mapped[str] = mapped_column(String(50))
    password: Mapped[str] = mapped_column(String(255), nullable=False)
    orders: Mapped[List['Order']] = relationship(back_populates="user", cascade="save-update, merge, delete")


class Order(Base):
    __tablename__ = 't_order'

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
    user_id: Mapped[int] = mapped_column(ForeignKey('t_user.id'))
    user: Mapped['User'] = relationship(back_populates="orders")

我们主要关注和关联映射相关的字段,User中的orders属性我们采用了Mapped[List['Order']]类型标注,它表示这个字段的类型是Order对象的列表,relationship()定义了模型之间的映射关联关系,back_populates参数是关联的属性名(即关联对方的属性名),cascade用于定义级联操作,save-update, merge, delete表示处理级联更新和级联删除,如果不设置cascade,默认的级联行为是save-update, merge,表示级联更新,但不级联删除。cascade的可选值如下:

  • save-update:当父对象被保存时,自动保存或更新关联的子对象
  • merge:当父对象被合并到当前会话时,自动合并关联的子对象
  • expunge:当父对象从会话中移除时,自动移除关联的子对象
  • delete:当父对象被删除时,自动删除关联的子对象
  • delete-orphan:当子对象不再与任何父对象关联时,自动删除该子对象
  • refresh-expire:当父对象被刷新或过期时,自动刷新或过期关联的子对象
  • all:包含以上除delete-orphan的所有级联操作

实际开发中,我们90%的情况都是使用默认的save-update, merge,如果需要级联删除则使用save-update, merge, delete,极特殊情况需要删除孤儿数据时使用save-update, merge, delete, delete-orphan。对于其它级联选项,我们需要充分了解其含义再使用,避免错误使用。

另外relationship()还有一个可配置的lazy属性,它用于设置关联对象的懒加载行为:

  • select:默认,访问关联属性时,执行一个单独的SELECT查询
  • immediate:访问父对象时,立即加载关联数据
  • joined:使用JOIN语句在查询父对象时一次性加载关联数据
  • subquery:使用子查询在查询父对象时一次性加载关联数据
  • dynamic:返回一个查询对象,可以进一步筛选数据
  • noload:永远不加载该属性,即使访问也不会触发查询
  • raise:访问该属性时,如果未加载,会抛出InvalidRequestError错误

实际开发中,我们主要使用默认是select和立即加载的immediate配置,其它选项较少使用。

Order中的user_id是一个外键字段,它被设置为了ForeignKey类型,参数t_user.id指定了关联的表和字段名(注意这里不是数据模型的属性名)。

一对一关联

一对一关联就是一种特殊的一对多关联,它的写法和一对多完全一致,我们使用relationship()定义关联关系,配合ForeignKey指定外键即可。

多对多关联

多对多关联相比一对多要复杂一些,多对多涉及中间表。下面例子代码中,角色Role和权限Permission具有多对多关系,一个角色可能包含多个权限,一个权限也可能被多个角色引用。

from typing import List

from sqlalchemy import BigInteger, String, ForeignKey, Table, Column
from sqlalchemy.orm import mapped_column, DeclarativeBase, Mapped, relationship


class Base(DeclarativeBase):
    pass


role_permission = Table(
    "t_role_permission",
    Base.metadata,
    Column("role_id", ForeignKey("t_role.id"), primary_key=True),
    Column("permission_id", ForeignKey("t_permission.id"), primary_key=True),
)


class Role(Base):
    __tablename__ = 't_role'

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
    code: Mapped[str] = mapped_column(String(20), nullable=False)
    name: Mapped[str] = mapped_column(String(20), nullable=False)
    permissions: Mapped[List['Permission']] = relationship(secondary=role_permission, back_populates="roles")


class Permission(Base):
    __tablename__ = 't_permission'

    id: Mapped[int] = mapped_column(BigInteger, primary_key=True)
    code: Mapped[str] = mapped_column(String(20), nullable=False)
    name: Mapped[str] = mapped_column(String(20), nullable=False)
    roles: Mapped[List['Role']] = relationship(secondary=role_permission, back_populates="permissions")

多对多中间表并不是一个“数据模型”,虽然我们确实可以很牵强的将中间表定义为数据模型类,但SQLAlchemy提供了更好的方式处理这个问题,SQLAlchemy Core中支持直接定义数据表的底层元数据信息,role_permission就是这样的一个对象,代码中我们指定了表名、元数据基类和两个外键列名。

RolePermission中,我们分别使用relationship()定义了关联关系,但这里我们使用的是secondary指定了中间关联元数据对象,back_populates则和之前一样,指定了关联对方的关联属性。

单向关联和双向关联

前面我们定义的全部都是双向关联,如果只需要单向关联,那么我们只在一方使用relationship()即可。

执行DDL操作

前面我们已经定义了数据模型,SQLAlchemy支持基于数据模型自动建表或删除表。

from sqlalchemy import create_engine
from models import Base

if __name__ == '__main__':
    DATABASE_URL = 'mysql+pymysql://root:root@localhost:3306/netstore'
    engine = create_engine(DATABASE_URL, echo=True, future=True)
    Base.metadata.create_all(engine)

代码中我们用到了之前定义的基类Base。由于我们所有的数据模型都继承自这个基类,因此框架运行后它会自动通过反射找到所有的模型类的元数据并注册到Base的metadata类属性,我们可以在其上调用create_all()方法自动建表。此外,也可以使用drop_all()方法删除所有表。

不过在实际开发中,我们通常不会直接这样调用create_all()建表。SQLAlchemy支持Alembic数据迁移工具,如果采用Code First开发模式,我们可以使用Alembic生成增量的数据迁移,这样操作表结构和数据更加可靠。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap