SQLMODEL关系的异步侧 - 第1部分
#教程 #python #database #async

到目前为止,我们已经介绍了如何异步执行数据库操作。接下来,我们将在模型之间建立关系并发现相关的陷阱。

tl; dr

  1. 使用sqlmodel.Field在模型中创建外键。

  2. 使用alembic revision --autogenerate -m "migration message"alembic upgrade head迁移数据库。

  3. 使用sqlmodel.Relationship(back_populates="column")添加关系属性3。

由于步骤3的陷阱需要仔细注意,因此您可能希望阅读文章。

目标

我们的目标保持不变。我们想创建一个宠物管理应用程序。

part 2 of this blog series中,我们讨论了如何异步执行数据库操作。

设置环境

我们使用Poetry进行依赖性管理。

我们的项目环境看起来像这样:

sqlmodel-alembic-async/
├── sqlmodel_alembic_async/
│   ├── __init__.py
│   ├── databases.py
│   └── models.py
├── poetry.lock
└── pyproject.toml

在写作时,依赖项为:

# pyproject.toml
[tool.poetry.dependencies]
python = "^3.8"
sqlmodel = "^0.0.8"
alembic = "^1.9.2"
aiosqlite = "^0.18.0" # you can use some other async database engine

确保使用 poetry shell

激活诗歌壳

与SQLModel建立关系

添加外键

因此,问题是:一个用户可以拥有多个宠物。或换句话说:一个宠物由一个用户拥有。

我们可以看到这是一对多的关系。用图表理解这一点:

              is owned by
1 Pet ────────────────────────────► 1 User


                     ┌───────► Pet
                     │
           owns      │
1 User ──────────────┼───────► Pet  (multiple Pets)
                     │
                     │
                     └───────► Pet

在数据库中,我们使用外键来表示我们的一对多关系。

# sqlmodel_alembic_async/models.py
from typing import Optional
from sqlmodel import Field, SQLModel

metadata = SQLModel.metadata


class User(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str


class Pet(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
+   user_id: Optional[int] = Field(default=None, foreign_key="user.id")

创建修订并迁移

创建修订

这个步骤非常简单。

alembic revision --autogenerate -m "add Pet.user_id foreign key"
# INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
# INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
# INFO  [alembic.autogenerate.compare] Detected added column 'pet.user_id'
# INFO  [alembic.autogenerate.compare] Detected added foreign key (user_id)(id) on table pet
#  Generating ./sqlmodel-alembic-async/migrations/versions/<somehash>_add_pet_user_id_foreign_key.py ...  done

检查修订

# migrations/versions/<somehash>_add_pet_user_id_foreign_key.py
"""add Pet.user_id foreign key

Revision ID: <some hash>
Revises: <some hash>
Create Date: <some date>
"""
from alembic import op
import sqlalchemy as sa
import sqlmodel.sql.sqltypes


# revision identifiers, used by Alembic.
revision = '<some hash>'
down_revision = '<some hash>'
branch_labels = None
depends_on = None


def upgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    with op.batch_alter_table('pet', schema=None) as batch_op:
        batch_op.add_column(sa.Column('user_id', sa.Integer(), nullable=True))
        batch_op.create_foreign_key(batch_op.f('fk_pet_user_id_user'), 'user', ['user_id'], ['id'])

    # ### end Alembic commands ###


def downgrade() -> None:
    # ### commands auto generated by Alembic - please adjust! ###
    with op.batch_alter_table('pet', schema=None) as batch_op:
        batch_op.drop_constraint(batch_op.f('fk_pet_user_id_user'), type_='foreignkey')
        batch_op.drop_column('user_id')

    # ### end Alembic commands ###

检查以下几行:

        # ...
        batch_op.create_foreign_key(batch_op.f('fk_pet_user_id_user'), 'user', ['user_id'], ['id'])
        # ...

此名称fk_pet_user_id_user是使用我们在part 1 of this blog series.

中创建的命名约定生成的

迁移数据库

alembic upgrade head
# INFO  [alembic.runtime.migration] Context impl SQLiteImpl.
# INFO  [alembic.runtime.migration] Will assume non-transactional DDL.
# INFO  [alembic.runtime.migration] Running upgrade <some hash> -> <some hash>, add Pet.user_id foreign key

玩数据库模型

创建会话

在开始之前,我们必须创建一个会话并将其与引擎绑定。在part 2 of this blog series,我们看到了如何创建异步会话。

>>> from sqlmodel.ext.asyncio.session import AsyncSession
>>> from sqlmodel_alembic_async.databases import engine
>>>
>>> session = AsyncSession(engine)

我们现在准备尝试使用数据库模型。

本实验

首先,我们导入数据库模型。

>>> # ...
>>> from sqlmodel_alembic_async.models import User, Pet

让我们创建一个称为“ Chonky”的用户。她碰巧拥有一只叫“ phroge”的宠物。

>>> # ...
>>> user_chonky = User(name="chonky")
>>> pet_frog = Pet(name="phroge")

要将用户链接到宠物,我们需要user_chonkyid的值。但是我们的用户还没有id。因此,我们需要将其提交到数据库中。

>>> session.add(user_chonky)
>>> await session.commit(user_chonky)
INFO sqlalchemy.engine.Engine BEGIN (implicit)
INFO sqlalchemy.engine.Engine INSERT INTO user (name) VALUES (?)
INFO sqlalchemy.engine.Engine [generated in 0.00016s] ('chonky',)
INFO sqlalchemy.engine.Engine COMMIT

然后我们刷新user_chonky对象以更新其属性。

>>> await session.refresh(user_chonky)
INFO sqlalchemy.engine.Engine BEGIN (implicit)
INFO sqlalchemy.engine.Engine SELECT user.name, user.id 
FROM user 
WHERE user.id = ?
INFO sqlalchemy.engine.Engine [generated in 0.00018s] (1,)
>>>
>>> print(user_chonky)
User(id=2, name='chonky')

和繁荣!我们现在有一个id

的值

INFO sqlalchemy... log来自?

Sqlalchemy正在记录其操作,因为我们将echo=True传递给create_async_engine。这已在part 2 of the blog series.中进行了描述

>>> pet_frog = user_chonky.id
>>> await session.commit()
>>> # ... log messages skipped ...
>>> await session.refresh(pet_frog)
>>> # ... log messages skipped ...
>>> print(pet_frog)
Pet(id=1, name='phroge', user_id=2)

并记住完成后关闭数据库会话。

>>> await session.close()

和tada!我们使用SQLModel成功地在两个对象之间建立了关系。

但是,使用 id 建立关系似乎很麻烦。如果我们能以某种方式互动

,那就太好了

关系属性

使用关系属性,我们可以使用熟悉的Python属性直接与用户及其宠物进行交互。

全部

# sqlmodel_alembic_async/models.py
# ...
+from typing import List, Optional

class User(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str

+   pets: List["Pet"] = Relationship(back_populates="user")


class Pet(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str

    user_id: int | None = Field(default=None, foreign_key="user.id")
+   user: Optional[User] = Relationship(back_populates="pets")

List["Pet"]Optional[User]是类型提示。假设您知道什么是类型的提示。 SQLModel's docs详细介绍了使用类型提示和关系属性。

使用具有关系属性的模型

首先导入所需的类和实例。

>>> from sqlmodel_alembic_async.models import User, Pet
>>> from sqlmodel_alembic_async.databases import engine
>>> from sqlmodel.ext.asyncio.session import AsyncSession
>>>
>>> session = AsyncSession(engine)

我们创建了一个用户及其宠物。

>>> # ...
>>> user_someone = User(name="someone")
>>> pet_something = Pet(name="something")

现在我们告诉pet_something它的“所有者”是user_someone

>>> # ...
>>> pet_something.user = user_someone

让我们看看pet_somethinguser_someone是什么样子。

>>> # ...
>>> pet_something
Pet(id=None, name='something', user_id=None, user=User(name='someone', id=None, pets=[Pet(id=None, name='something', user_id=None, user=User(name='someone', id=None, pets=[...]))]))
>>>
>>> user_someone
User(name='someone', id=None, pets=[Pet(id=None, name='something', user_id=None, user=User(name='someone', id=None, pets=[...]))])

发生了什么事?为什么 [...] ?这是递归对象吗?

是!它确实是一个递归对象。它表明SQLMODEL了解对象之间存在一种关系(确切地说是一对多)。

这样理解:

              is owned by
1 Pet ────────────────────────────► 1 User


                     ┌───────► Pet
                     │
           owns      │
1 User ──────────────┼───────► Pet  (multiple Pets)
                     │
                     │
                     └───────► Pet

现在,让我们将其添加到数据库中,提交它然后刷新对象。

>>> # ...
>>> session.add_all((user_someone, pet_something))
>>> await session.commit()
... # log message skipped
>>> await session.refresh(user_someone)
... # log message skipped
>>> await session.refresh(pet_something)
... # log message skipped

让我们尝试检查user_someone

>>> # ...
>>> user_someone.pets
INFO sqlalchemy.engine.Engine SELECT pet.id AS pet_id, pet.name AS pet_name, pet.user_id AS pet_user_id 
FROM pet 
WHERE ? = pet.user_id
INFO sqlalchemy.engine.Engine [generated in 0.00017s] (3,)

MissingGreenlet: greenlet_spawn has not been called; can't call await_only() here. Was IO attempted in an unexpected place? (Background on this error at: https://sqlalche.me/e/14/xd2s)

哦,不!它因某些模糊的MissingGreenlet错误而失败。发生了什么?

分析属性访问错误

让我们检查日志消息和错误消息。

>>> # ...
>>> user_someone.pets
INFO sqlalchemy.engine.Engine SELECT pet.id AS pet_id, pet.name AS pet_name, pet.user_id AS pet_user_id 
FROM pet 
WHERE ? = pet.user_id
INFO sqlalchemy.engine.Engine [generated in 0.00017s] (3,)

在日志消息中,很明显SQLalchemy对数据库进行查询。它试图获取具有外键user_id等于3的宠物。

MissingGreenlet: greenlet_spawn has not been called; can't call await_only() here. Was IO attempted in an unexpected place? (Background on this error at: https://sqlalche.me/e/14/xd2s)

但是,这个错误说“在没有预期的情况下进行了一些IO操作”。另外,它似乎使用 greenlet 。看来SQLalchemy试图执行查询时发生了错误。

The documentation for Greenlet将自己描述为并发编程的库。考虑到我们的错误,我们可以得出结论,Sqlalchemy试图尝试,但未能启动Greenlet Coroutine。 这表明当Sqlalchemy尝试执行隐式I/O操作时,当不在“异步上下文”中时发生错误。

下一步是什么?

现在,我们已经学会了如何在模型之间建立关系以及如何在“异步上下文”之外执行SQL查询会导致错误,我们将研究一些解决错误的方法。

或者您可以说,我们将改善与异步SQL操作的关系。ð

图像源

fabio上的Unsplash

摄影