跳转至

关系模型定义

定义模型间关系

Cherry 支持关系型数据库的一对一、一对多以及多对多关系,只需简单的配置即可。

一对一

要声明一对一关系,只需使用 cherry.ForeignKey 注解包裹对应的模型即可,在对应的模型上则使用 cherry.ReverseRelation 来声明反向关系。

import cherry

db = cherry.Database("sqlite+aiosqlite:///:memory:")


class UserDetail(cherry.Model):
    id: cherry.AutoIntPK = None
    age: int
    address: str
    email: str
    user: cherry.ForeignKey["User"]

    cherry_config = cherry.CherryConfig(tablename="user_detail", database=db)


class User(cherry.Model):
    id: cherry.AutoIntPK = None
    name: str
    detail: cherry.ReverseRelation[UserDetail]

    cherry_config = cherry.CherryConfig(tablename="user", database=db)

一对多

一对多和一对一关系类似,只需要把 cherry.ReverseRelation[Model] 改成 cherry.ReverseRelation[List[Model]],注解为模型列表即可。

from typing import Optional

import cherry

db = cherry.Database("sqlite+aiosqlite:///:memory:")


class Student(cherry.Model):
    id: cherry.AutoIntPK = None
    name: str
    school: cherry.ForeignKey[Optional["School"]] = None

    cherry_config = cherry.CherryConfig(tablename="student", database=db)


class School(cherry.Model):
    id: cherry.AutoIntPK = None
    name: str
    students: cherry.ReverseRelation[list[Student]] = []

    cherry_config = cherry.CherryConfig(tablename="school", database=db)


async def main():
    await db.init()

    school = School(name="school 1")
    await school.insert()

    await Student(name="student 1", school=school).insert()

    school2 = School(
        name="school 2",
        students=[
            Student(name="student 2"),
            Student(name="student 3"),
        ],
    )
    await school2.insert_with_related()

    student4 = Student(name="student 4", school=School(name="school 3"))
    await student4.insert_with_related()

    # Pythonic Style
    student: list[Student] = await Student.filter(School.name == "school 2").all()
    # Django Style
    student: list[Student] = await Student.filter(school_name="school 2").all()

    student_with_school: Student = (
        await Student.filter(Student.name == "student 1")
        .prefetch_related(Student.school)
        .get()
    )

    school_with_students: School = (
        await School.filter(School.name == "school 2")
        .prefetch_related(School.students)
        .get()
    )

    schools_with_students: list[School] = (
        await School.select_related().prefetch_related(School.students).all()
    )

    school = await School.get(name="school 3")
    await school.fetch_related(School.students)
    assert len(school.students) == 1


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())

多对多

多对多关系关系则使用 cherry.ManyToMany 来注解。

import cherry

db = cherry.Database("sqlite+aiosqlite:///:memory:")


class Tag(cherry.Model):
    id: cherry.AutoIntPK = None
    content: str
    posts: cherry.ManyToMany[list["Post"]] = []

    cherry_config = cherry.CherryConfig(tablename="tag", database=db)


class Post(cherry.Model):
    id: cherry.AutoIntPK = None
    title: str
    tags: cherry.ManyToMany[list[Tag]] = []

    cherry_config = cherry.CherryConfig(tablename="post", database=db)


async def main():
    await db.init()

    tag1 = await Tag(content="tag 1").insert()
    tag2 = await Tag(content="tag 2").insert()

    post1 = await Post(title="post 1").insert()
    post2 = await Post(title="post 2").insert()

    await post1.add(tag1)
    await post1.add(tag2)

    await post2.add(tag1)

    assert post1.tags == [tag1, tag2]
    assert post2.tags == [tag1]

    await tag1.fetch_related(Tag.posts)
    assert len(tag1.posts) == 2

    await post1.remove(tag1)
    assert post1.tags == [tag2]

    await tag1.fetch_related(Tag.posts)
    assert len(tag1.posts) == 1


if __name__ == "__main__":
    import asyncio

    asyncio.run(main())

关系字段配置

ForeignKey, ReverseRelation, ManyToMany 等注解只能在模型主键只有一个时使用,如果是复合主键或者想要使用其他字段做外键,则需要使用 cherry.Relationship 来声明。

另外,关于表关系的级联操作等,也需要在 cherry.Relationship 中定义。

cherry.Relationship 具有以下参数:

  • foreign_key - 外键目标字段。在一对一或一对多关系的外键侧表的使用,指定使用目标表的哪个字段作为外键。
  • foreign_key_extra - 一些传给 sqlalchemy.ForeignKey 的额外配置。
  • reverse_related - 反向关系字段。在一对一或一对多关系中的反向关系中使用,设为 True 即可。
  • many_to_many - 多对多外键字段。在多对多关系中使用,指定自身模型的哪个字段为多对多关系中的外键值。
  • on_update - 相关模型更新时采取的措施,来自 sqlalchemy.ForeignKey
  • on_delete - 相关模型删除时采取的措施,来自 sqlalchemy.ForeignKey
  • related_field - 关联的字段。通常无需你自己配置,模型会自动查找。

on_updateon_delete 允许的值有:

  • RESTRICT - 限制更新/删除。
  • CASCADE - 级联更新/删除。
  • SET NULL - 设为 NULL(None),如果字段不允许为 None,则会抛出异常。
  • SET DEFAULT - 设为默认值,如果字段没有默认值,则会抛出异常。
  • NO ACTION - 不做任何行动,该报错就报错。

以下是两个例子。

一对一、一对多

class Student(cherry.Model):
    id: cherry.AutoIntPK = None
    name: str
    school: Optional["School"] = cherry.Relationship(
        default=None,
        foreign_key="school_id",
        on_update="CASCADE",
        on_delete="CASCADE",
    )

    cherry_config = cherry.CherryConfig(tablename="student", database=db)


class School(cherry.Model):
    school_id: cherry.PrimaryKey[int]
    school_name: cherry.PrimaryKey[str]
    students: list[Student] = cherry.Relationship(default=[], reverse_related=True)

    cherry_config = cherry.CherryConfig(tablename="school", database=db)

多对多

class Tag(cherry.Model):
    tag_id1: cherry.PrimaryKey[int]
    tag_id2: cherry.PrimaryKey[int]
    content: str
    posts: list["Post"] = cherry.Relationship(
        default=[],
        many_to_many="post_id2",
        on_delete="RESTRICT",
        on_update="RESTRICT",
    )

    cherry_config = cherry.CherryConfig(tablename="tag", database=db)


class Post(cherry.Model):
    post_id1: cherry.PrimaryKey[int]
    post_id2: cherry.PrimaryKey[int]
    title: str
    tags: list[Tag] = cherry.Relationship(default=[], many_to_many="tag_id1")

    cherry_config = cherry.CherryConfig(tablename="post", database=db)