Skip to content

Commit 1c6e5ea

Browse files
committed
📝 update database.md
1 parent da5fd12 commit 1c6e5ea

2 files changed

Lines changed: 230 additions & 25 deletions

File tree

tutorial/entari/database.md

Lines changed: 216 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -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
6280
from entari_plugin_database import Base, Mapped, mapped_column
6381
6482
class 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
113153
from entari_plugin_database import SqlalchemyService
114154
from 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+
:::

tutorial/entari/index.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,13 @@ pip install entari-cli
7777

7878
```shell:no-line-numbers
7979
$ entari config new --help
80-
用法: entari config new [-d] [-P NAMES]
80+
用法: entari config new [-d]
8181
8282
新建一个 Entari 配置文件
8383
8484
选项:
8585
-d, --dev 是否生成开发用配置文件
86+
-h, --help 显示帮助信息
8687
```
8788

8889
`config new` 指令会根据当前环境选择一个合适的文件格式。
@@ -272,23 +273,30 @@ app.run()
272273
如同配置文件一样,插件也是 Entari 的重要组成部分。它们可以扩展 Entari 的功能,提供更多的事件响应器、命令、定时任务等。
273274
Entari 内置了一些插件,例如 `echo`、`inspect`、`help` 等。你可以在配置文件中启用它们,也可以通过代码加载它们。
274275

275-
想要创建一个新的插件,你可以使用 `entari plugin new` 命令来生成一个插件模板:
276+
想要创建一个新的插件,你可以使用 `entari new` 命令来生成一个插件模板:
276277

277278
```shell:no-line-numbers
278-
$ entari plugin new --help
279-
用法: entari plugin new [-S] [-A] [-f]
279+
$ entari new --help
280+
用法: entari new [-S] [-A] [-f] [-D] [-O] [-p NUM] [-py PATH] [--pip-args PARAMS]
280281
281282
新建一个 Entari 插件
283+
基础指令: entari new [NAME]
282284
283285
选项:
284286
-S, --static 是否为静态插件
285287
-A, --application 是否为应用插件
286288
-f, --file 是否为单文件插件
289+
-D, --disabled 是否插件初始禁用
290+
-O, --optional 是否仅存储插件配置而不加载插件
291+
-p, --priority NUM 插件加载优先级
292+
-py, --python PATH 指定 Python 解释器路径
293+
--pip-args PARAMS 传递给 pip 的额外参数
294+
-h, --help 显示帮助信息
287295
```
288296

289297
其中对于 `--application` 选项,若你正在新建单个插件项目,则忽略这个选项;若你正在创建一个本地插件,则需要使用这个选项。
290298

291-
假设我们通过 `entari plugin new my_plugin --application --file` 创建了一个名为 `my_plugin` 的插件,那么它的目录结构大致如下:
299+
假设我们通过 `entari new my_plugin --application --file` 创建了一个名为 `my_plugin` 的插件,那么它的目录结构大致如下:
292300

293301
```text:no-line-numbers
294302
project/
@@ -939,7 +947,7 @@ async def on_message(session: Session):
939947
```
940948

941949
```shell:no-line-numbers
942-
2025-06-30 00:44:14 INFO | [core] Entari version 0.13.1
950+
2025-06-30 00:44:14 INFO | [core] Entari version 0.15.0
943951
2025-06-30 00:44:14 SUCCESS | [plugin] loaded plugin 'arclet.entari.builtins.auto_reload'
944952
2025-06-30 00:44:14 SUCCESS | [plugin] loaded plugin 'my_plugin'
945953
...

0 commit comments

Comments
 (0)