到目前为止,我们已经介绍了如何异步执行数据库操作。接下来,我们将在模型之间建立关系并发现相关的陷阱。
tl; dr
-
使用
sqlmodel.Field
在模型中创建外键。 -
使用
alembic revision --autogenerate -m "migration message"
和alembic upgrade head
迁移数据库。 -
使用
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_chonky
的id
的值。但是我们的用户还没有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_something
和user_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操作的关系。ð