@@ -50,24 +50,42 @@ plugins:
5050- ` username ` : 数据库用户名 (仅在使用 MySQL/PostgreSQL 等远程数据库时需要)
5151- ` password ` : 数据库密码 (仅在使用 MySQL/PostgreSQL 等远程数据库时需要)
5252- ` query ` : 数据库连接参数 (仅在使用 MySQL/PostgreSQL 等远程数据库时需要)
53- - ` options ` : SQLAlchemy 的其他选项。参见 [ Engine Creation API] ( https://docs.sqlalchemy.org/en/21/core/engines.html#engine-creation-api )
54-
55- 若不传入配置项,则默认使用 SQLite 数据库,并将数据库文件存储在当前目录下。
53+ - ` options ` : 数据库连接其他选项。参见 [ Engine Creation API] ( https://docs.sqlalchemy.org/en/21/core/engines.html#engine-creation-api )
54+ - ` session_options ` : 会话选项。参见 [ Session] ( https://docs.sqlalchemy.org/en/21/orm/session_api.html#sqlalchemy.orm.Session.__init__ )
55+ - ` binds ` : 绑定多个数据库配置,用于为不同插件下的ORM模型指定不同的数据库连接。
56+ ``` yaml:no-line-numbers title=entari.yml
57+ plugins:
58+ database:
59+ type: sqlite
60+ name: main.db
61+ binds:
62+ entari_plugin_record: # 为 entari_plugin_record 插件下的模型指定单独的数据库连接
63+ type: postgresql
64+ host: localhost
65+ port: 5432
66+ username: user
67+ password: pass
68+ ```
69+ - `create_table_at`: 指定在数据库服务的哪个生命周期阶段创建表。可选值有 `preparing`, `prepared` 和 `blocking`.
70+
71+ - 若不传入配置项,则默认使用 SQLite 数据库,并将数据库文件存储在当前目录下。
5672
5773## 定义模型
5874
5975`database` 插件使用 SQLAlchemy 的 ORM 功能来定义模型。你可以通过继承 `database.Base` 类来定义你的模型类。
6076
77+ 假设我们要定义一个存储天气信息的模型:
78+
6179```python title=my_plugin.py
6280from entari_plugin_database import Base, Mapped, mapped_column
6381
6482class Weather(Base):
65- __tablename__ = " weather"
66-
6783 location: Mapped[str] = mapped_column(primary_key=True)
6884 weather: Mapped[str]
6985```
7086
87+ 其中,` primary_key=True ` 意味着此列 (location) 是主键,即内容是唯一的且非空的。 每一个模型必须有至少一个主键。
88+
7189我们可以用以下代码检查模型生成的数据库模式是否正确:
7290
7391``` python
@@ -77,51 +95,230 @@ print(CreateTable(Weather.__table__))
7795```
7896
7997``` sql
80- CREATE TABLE weather (
98+ CREATE TABLE my_plugin_weather (
8199 location VARCHAR NOT NULL ,
82100 weather VARCHAR NOT NULL ,
83- CONSTRAINT pk_weather PRIMARY KEY (location)
101+ CONSTRAINT pk_my_plugin_weather PRIMARY KEY (location)
84102)
85103```
86104
105+ 可以注意到表名是 ` my_plugin_weather ` 而不是 ` Weather ` 或者 ` weather ` 。 这是因为数据库插件会自动为模型生成一个表名,规则是:` <插件模块名>_<类名小写> ` 。
106+
107+ 你也可以通过指定 ` __tablename__ ` 属性,或传入关键字来自定义表名:
108+
109+ ::: code-group
110+
111+ ``` python title=my_plugin.py [指定 __tablename__]
112+ from entari_plugin_database import Base, Mapped, mapped_column
113+
114+ class Weather (Base ):
115+ __tablename__ = " custom_weather" # 自定义表名
116+ location: Mapped[str ] = mapped_column(primary_key = True )
117+ weather: Mapped[str ]
118+ ```
119+
120+ ``` python title=my_plugin.py [传入关键字]
121+ from entari_plugin_database import Base, Mapped, mapped_column
122+
123+ class Weather (Base , tablename = " custom_weather" ): # 自定义表名
124+ location: Mapped[str ] = mapped_column(primary_key = True )
125+ weather: Mapped[str ]
126+ ```
127+ :::
128+
87129
88130## 使用会话
89131
90- ` database ` 插件通过 ` SqlalchemyService ` 提供数据库会话服务 。
132+ 在 ` SQLAlchemy ` 中,操作数据库需要通过会话 (Session) 来进行。 关于如何通过会话使用 SQLAlchemy 的 ORM 功能,你可以参考 [ SQLAlchemy 官方文档 ] ( https://docs.sqlalchemy.org/en/21/orm/quickstart.html ) 。
91133
92- 你可以通过依赖注入的方式获取 ` SqlalchemyService ` 实例,并使用它来获取数据库会话。
134+ ` database ` 插件通过 ` SqlalchemyService ` 提供数据库会话服务。 你可以通过依赖注入的方式获取 ` SqlalchemyService ` 实例,并使用它来获取数据库会话。
93135
94136::: code-group
95137
96138``` python title=my_plugin.py [ORM]
97- from arclet.entari import Session, command
98- from entari_plugin_database import SqlalchemyService
99- from sqlalchemy import select
139+ from arclet.entari import command
140+ from entari_plugin_database import SqlalchemyService, select
100141
101142@command.on (" get_weather {location} " )
102- async def on_message (location : str , session : Session, db : SqlalchemyService):
143+ async def on_message (location : str , db : SqlalchemyService):
103144 async with db.get_session() as db_session:
104145 # 在这里使用 SQLAlchemy 的会话进行数据库操作
105146 result = await db_session.scalars(select(Weather).where(Weather.location == location))
106147 data = result.all()
107- await session.send( f " Data: { data} " )
148+ return f " Data: { data} "
108149```
109150
110-
111151``` python title=my_plugin.py [SQL语句]
112- from arclet.entari import Session, command
152+ from arclet.entari import command
113153from entari_plugin_database import SqlalchemyService
114154from sqlalchemy import text
115155
116156@command.on (" get_weather {location} " )
117- async def on_message (location : str , session : Session, db : SqlalchemyService):
157+ async def on_message (location : str , db : SqlalchemyService):
118158 async with db.get_session() as db_session:
119159 # 在这里使用 SQLAlchemy 的会话进行数据库操作
120160 result = await db_session.execute(text(" SELECT * FROM weather WHERE location=:location" ), {" location" : location})
121161 data = result.fetchall()
122- await session.send(f " Data: { data} " )
162+ return f " Data: { data} "
163+ ```
164+
165+ :::
166+
167+
168+ 又或者,你也可以通过直接依赖注入 ` AsyncSession ` 来获取数据库会话:
169+
170+ ``` python title=my_plugin.py
171+ from arclet.entari import command
172+ from entari_plugin_database import AsyncSession, select
173+
174+ @command.on (" get_weather {location} " )
175+ async def on_message (location : str , db_session : AsyncSession):
176+ # 在这里使用 SQLAlchemy 的会话进行数据库操作
177+ result = await db_session.scalars(select(Weather).where(Weather.location == location))
178+ data = result.all()
179+ return f " Data: { data} "
180+ ```
181+
182+ 直接依赖注入 ` AsyncSession ` 时,获取到的会话已经是一个上下文管理器,你不需要再使用 ` async with ` 来管理它。
183+
184+ ::: info
185+
186+ ` AsyncSession ` 的生命周期与单个订阅者同步,即每次命令或事件触发时,每个订阅者都会创建一个新的 ` AsyncSession ` 实例,并在处理完成后关闭它。
187+
188+ :::
189+
190+ ## 依赖注入
191+
192+ 在上面的示例中,我们都是通过会话获得数据的。 不过,我们也可以通过依赖注入获得数据:
193+
194+ ``` python title=my_plugin.py {6-8}
195+ from arclet.entari import Param, command
196+ from entari_plugin_database import SQLDepends, select
197+
198+ @command.command (" get_weather <location:str>" )
199+ async def on_message (
200+ weather : Weather = SQLDepends(
201+ select(Weather).where(Weather.location == Param(" location" ))
202+ ),
203+ ):
204+ return f " Data: { weather} "
205+ ```
206+
207+ 其中,SQLDepends 是一个特殊的依赖注入,它会根据类型标注和 SQL 语句提供数据,SQL 语句中也可以有子依赖。
208+ 但不建议使用 ` select ` 以外的语句,因为语句可能没有返回值(` returning ` 除外),而且代码不清晰。
209+
210+ 不同的类型标注也会获得不同形式的数据:
211+
212+ ``` python title=my_plugin.py {1,7-9}
213+ from collections.abc import Sequence
214+ from arclet.entari import Param, command
215+ from entari_plugin_database import SQLDepends, select
216+
217+ @command.command (" get_weather <location:str>" )
218+ async def on_message (
219+ weathers : Sequence[Weather] = SQLDepends(
220+ select(Weather).where(Weather.location == Param(" location" ))
221+ ),
222+ ):
223+ return " Data\n " + " \n " .join(f " - { weather} " for weather in weathers)
123224```
124225
226+ ::: tip
227+
228+ ` Param ` 也是一类 ` Depends ` , 等同于 ` Depends(lambda <name>: <name>) ` 或 ` Depends(lambda ctx: ctx[<name>]) ` 。
229+
125230:::
126231
127- 关于如何使用 SQLAlchemy 的 ORM 功能,你可以参考 [ SQLAlchemy 官方文档] ( https://docs.sqlalchemy.org/en/21/orm/quickstart.html ) 。
232+ 类型标注将决定依赖注入的实际的数据结构,主要影响以下几个层面:
233+ - 迭代器(` session.execute() ` )或异步迭代器(` session.stream() ` )
234+ - 标量(` session.execute().scalars() ` )或元组(` session.execute() ` )
235+ - 单个(` session.execute().one_or_none() ` )或全部(` session.execute() / session.execute().all() ` )
236+ - 连续(` session().execute() ` )或分块(` session.execute().partitions() ` )
237+
238+ 具体而言:
239+
240+ ::: code-group
241+
242+ ``` python:no-line-numbers [Iterator]
243+ async def _(rows_partitions: AsyncIterator[Sequence[tuple[Model, ...]]]):
244+ # 等价于 rows_partitions = await (await session.stream(sql).partitions())
245+
246+ async for partition in rows_partitions:
247+ for row in partition:
248+ print(row[0], row[1], ...)
249+
250+ async def _(row_partitions: Iterator[Sequence[tuple[Model, ...]]]):
251+ # 等价于 row_partitions = await session.execute(sql).partitions()
252+
253+ for partition in rows_partitions:
254+ for row in partition:
255+ print(row[0], row[1], ...)
256+
257+ async def _(model_partitions: AsyncIterator[Sequence[Model]]):
258+ # 等价于 model_partitions = await (await session.stream(sql).scalars().partitions())
259+
260+ async for partition in model_partitions:
261+ for model in partition:
262+ print(model)
263+
264+ async def _(model_partitions: Iterator[Sequence[Model]]):
265+ # 等价于 model_partitions = await (await session.execute(sql).scalars().partitions())
266+
267+ for partition in model_partitions:
268+ for model in partition:
269+ print(model)
270+ ```
271+
272+ ``` python:no-line-numbers [Result/ScalarResult]
273+ async def _(rows: sa_async.AsyncResult[tuple[Model, ...]]):
274+ # 等价于 rows = await session.stream(sql)
275+
276+ async for row in rows:
277+ print(row[0], row[1], ...)
278+
279+ async def _(rows: sa.Result[tuple[Model, ...]]):
280+ # 等价于 rows = await session.execute(sql)
281+
282+ for row in rows:
283+ print(row[0], row[1], ...)
284+
285+ async def _(models: sa_async.AsyncScalarResult[Model]):
286+ # 等价于 models = await session.stream(sql).scalars()
287+
288+ async for model in models:
289+ print(model)
290+
291+ async def _(models: sa.ScalarResult[Model]):
292+ # 等价于 models = await session.execute(sql).scalars()
293+
294+ for model in models:
295+ print(model)
296+ ```
297+
298+ ``` python:no-line-numbers [Sequence]
299+ async def _(rows: Sequence[tuple[Model, ...]]):
300+ # 等价于 rows = await (await session.stream(sql).all())
301+
302+ for row in rows:
303+ print(row[0], row[1], ...)
304+
305+ async def _(models: Sequence[Model]):
306+ # 等价于 models = await (await session.stream(sql).scalars().all())
307+
308+ for model in models:
309+ print(model)
310+ ```
311+
312+ ``` python:no-line-numbers [Model/tuple[Model, ...]]
313+ async def _(row: tuple[Model, ...]):
314+ # 等价于 row = await (await session.stream(sql).one_or_none())
315+
316+ if row:
317+ print(row[0], row[1], ...)
318+
319+ async def _(model: Model | None):
320+ # 等价于 model = await (await session.stream(sql).scalars().one_or_none())
321+ if model:
322+ print(model)
323+ ```
324+ :::
0 commit comments