MiniDataAPI 规范

MiniDataAPI 是一个持久化 API 规范,其设计目标是小巧且易于在各种数据存储中实现。虽然早期的实现是基于 SQL 的,但该规范也可以在键/值存储、文档数据库等中快速实现。

开发中

MiniData API 规范仍在开发中,可能会发生变化。虽然大部分设计已经完成,但仍可能出现破坏性更改。

为何选择它?

MiniDataAPI 规范允许我们对许多不同的数据库引擎使用相同的 API。任何使用 MiniDataAPI 规范与其数据库交互的应用程序,在切换数据库引擎时,除了导入和配置的更改外,无需任何修改。例如,要将一个应用程序从运行 SQLite 的 Fastlite 转换为运行 PostgreSQL 的 FastSQL,只需更改以下两行代码:

FastLite 版本

from fastlite import *
db = database('test.db')

FastSQL 版本

from fastsql import *
db = Database('postgres:...')

由于这两个库都遵循 MiniDataAPI 规范,应用程序中的其余代码应保持不变。MiniDataAPI 规范的优势在于,它允许人们使用他们能够访问或偏好的任何数据存储。

注意

切换数据库不会迁移任何现有的数据。

易于学习,快速实现

MiniDataAPI 规范旨在易于学习和快速实现。它专注于简单的创建、读取、更新和删除 (CRUD) 操作。

MiniDataAPI 数据库不仅限于基于行的系统。事实上,该规范的设计更接近于键/值存储,而不是一组记录。令人兴奋的是,我们可以为像存储为 JSON 的 Python 字典、Redis,甚至 venerable ZODB 这样的工具编写实现。

MiniDataAPI 规范的局限性

“Mini 指的是规范的轻量性,而不是数据。”

– Jeremy Howard

MiniDataAPI 的优势是有代价的。与功能齐全的 ORM 和查询语言相比,MiniDataAPI 规范只关注一小部分功能。它有意避免了细微或复杂的功能。

这意味着该规范不包括连接 (joins) 或正式的外键 (foreign keys)。需要连接操作的、存储在多个表中的复杂数据处理得不好。对于这种情况,最好使用更复杂的 ORM 甚至直接的数据库查询。

MiniDataAPI 设计摘要

  • 易于学习
  • 为新的数据库引擎实现相对较快
  • 一个用于 CRUD 操作的 API
  • 适用于多种类型的数据库,包括基于行和键/值的设计
  • 功能上有意保持小巧:无连接、无外键、无特定于数据库的功能
  • 最适合简单的设计,复杂的架构需要更强大的工具。

连接/构建数据库

我们通过传递一个连接到数据库端点的字符串或代表数据库位置的文件路径来连接或构建数据库。虽然这个例子是针对在内存中运行的 SQLite,但其他数据库如 PostgreSQL、Redis、MongoDB 可能会使用指向数据库文件路径或端点的 URI。连接到数据库的方法*不*是此 API 的一部分,而是底层库的一部分。例如,对于 fastlite:

db = database(':memory:')

以下是 API 中可用方法的完整列表,所有方法都在下面有文档说明(假设 db 是一个数据库,t 是一个表):

  • db.create
  • t.insert
  • t.delete
  • t.update
  • t[key]
  • t(...)
  • t.xtra

为方便起见,本文档使用一个 SQL 示例。然而,表可以代表任何东西,不仅仅是 SQL 数据库的基本结构。它们可以代表键/值结构中的键,或硬盘上的文件。

创建表

我们使用附加到 Database 对象(在我们的示例中是 db)上的 create() 方法来创建表。

class User: name:str; email: str; year_started:int
users = db.create(User, pk='name')
users
<Table user (name, email, year_started)>
class User: name:str; email: str; year_started:int
users = db.create(User, pk='name')
users
<Table user (name, email, year_started)>

如果没有提供 pk,则假定 id 为主键。无论您是否将一个类标记为数据类 (dataclass),它都将被转换成一个——具体来说是一个 flexiclass

@dataclass
class Todo: id: int; title: str; detail: str; status: str; name: str
todos = db.create(Todo) 
todos
<Table todo (id, title, detail, status, name)>

复合主键

MiniData API 规范支持复合主键,即使用多个列来识别记录。我们还将使用这个例子来演示如何使用一个关键字参数字典来创建表。

class Publication: authors: str; year: int; title: str
publications = db.create(Publication, pk=('authors', 'year'))

转换表

根据数据库类型的不同,此方法可以包含转换——即修改表的能力。让我们继续,为我们的表添加一个名为 pwd 的密码字段。

class User: name:str; email: str; year_started:int; pwd:str
users = db.create(User, pk='name', transform=True)
users
<Table user (name, email, year_started, pwd)>

操作数据

该规范旨在提供尽可能直接的 CRUD API(创建、读取、更新和删除)。像连接 (joins) 这样的额外功能超出了范围。

.insert()

向数据库添加一条新记录。我们希望支持尽可能多的类型,目前我们对 Python 类、数据类和字典进行了测试。返回新记录的一个实例。

以下是如何使用 Python 类添加记录:

users.insert(User(name='Braden', email='[email protected]', year_started=2018))
User(name='Braden', email='[email protected]', year_started=2018, pwd=None)

我们也可以直接使用关键字参数:

users.insert(name='Alma', email='[email protected]', year_started=2019)
User(name='Alma', email='[email protected]', year_started=2019, pwd=None)

现在通过一个 Python 字典添加 Charlie。

users.insert({'name': 'Charlie', 'email': '[email protected]', 'year_started': 2018})
User(name='Charlie', email='[email protected]', year_started=2018, pwd=None)

现在添加待办事项 (TODOs)。请注意,插入的行会被返回。

todos.insert(Todo(title='Write MiniDataAPI spec', status='open', name='Braden'))
todos.insert(title='Implement SSE in FastHTML', status='open', name='Alma')
todo = todos.insert(dict(title='Finish development of FastHTML', status='closed', name='Charlie'))
todo
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')

让我们对 Publications 表做同样的操作。

publications.insert(Publication(authors='Alma', year=2019, title='FastHTML'))
publications.insert(authors='Alma', year=2030, title='FastHTML and beyond')
publication= publications.insert((dict(authors='Alma', year=2035, title='FastHTML, the early years')))
publication
Publication(authors='Alma', year=2035, title='FastHTML, the early years')

.update()

更新数据库中的现有记录。必须接受 Python 字典、数据类和标准类。使用主键来识别要更改的记录。返回更新后记录的一个实例。

以下是使用普通 Python 类的示例:

user
User(name='Alma', email='[email protected]', year_started=2019, pwd=None)
user.year_started = 2099
users.update(user)
User(name='Alma', email='[email protected]', year_started=2099, pwd=None)

或者使用字典:

users.update(dict(name='Alma', year_started=2199, email='[email protected]'))
User(name='Alma', email='[email protected]', year_started=2199, pwd=None)

或者使用关键字参数:

users.update(name='Alma', year_started=2149)
User(name='Alma', email='[email protected]', year_started=2149, pwd=None)

如果主键与任何记录都不匹配,则引发 NotFoundError

John 还没有加入我们,所以他还没有机会进行时间旅行。

try: users.update(User(name='John', year_started=2024, email='[email protected]'))
except NotFoundError: print('User not found')
User not found

.delete()

删除数据库中的一条记录。使用主键来识别要移除的记录。返回一个表对象。

Charlie 决定不进行时间旅行了。他退出了我们的小组。

users.delete('Charlie')
<Table user (name, email, year_started, pwd)>

如果找不到主键值,则引发 NotFoundError

try: users.delete('Charlies')
except NotFoundError: print('User not found')
User not found

在 John 的情况下,他还没有和我们一起进行时间旅行,所以不能被移除。

try: users.delete('John')
except NotFoundError: print('User not found')
User not found

删除具有复合主键的记录需要提供完整的键。

publications.delete(['Alma' , 2035])
<Table publication (authors, year, title)>

in 关键字

AlmaJohn 是否包含 in Users 表中?或者,从技术上讲,具有指定主键值的项是否 in 此表中?

'Alma' in users, 'John' in users
(True, False)

也适用于复合主键,如下所示。您会注意到该操作可以使用 listtuple 来完成。

['Alma', 2019] in  publications
True

现在是一个返回 False 的结果,其中 John 没有任何出版物。

('John', 1967) in publications
False

.xtra()

如果我们在 .xtra 函数中将字段设置为特定值,那么索引也将按这些值进行过滤。这适用于除记录创建之外的所有数据库方法。这使得限制用户(或其他对象)只能访问他们有权限的内容变得更容易。这是一个单向操作,一旦为特定的表对象设置,就无法撤销。

例如,如果我们在下面查询所有记录而不通过 .xtra 函数设置值,我们可以看到每个人的待办事项。请特别注意所有三条记录的 id 值,因为我们即将过滤掉其中的大部分。

todos()
[Todo(id=1, title='Write MiniDataAPI spec', detail=None, status='open', name='Braden'),
 Todo(id=2, title='Implement SSE in FastHTML', detail=None, status='open', name='Alma'),
 Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]

让我们使用 .xtra 将结果限制为仅限 Charlie。我们在 Todos 中设置了 name 字段,但它可以是为此表定义的任何字段。

todos.xtra(name='Charlie')

我们现在已经用 .xtra 将一个字段设置为一个值,如果我们再次遍历所有记录,只有那些分配给 nameCharlie 的记录才会被显示。

todos()
[Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')]

in 关键字也受到影响。只有 name 为 Charlie 的记录才会评估为 True。让我们用一条 Charlie 的记录来测试一下。

ct = todos[3]
ct
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')

Charlie 的记录 ID 为 3。这里我们演示了可以在待办事项列表中找到 Charlie 的待办事项。

ct.id in todos
True

如果我们对其他 ID 使用 in,查询会失败,因为过滤现在已设置为仅限 name 为 Charlie 的记录。

1 in todos, 2 in todos
(False, False)
try: todos[2]
except NotFoundError: print('Record not found')
Record not found

我们可以更新的记录也受到了限制。在下面的例子中,我们尝试更新一个不叫 'Charlie' 的待办事项。因为名字不对,.update 函数将引发 NotFoundError

try: todos.update(Todo(id=1, title='Finish MiniDataAPI Spec', status='closed', name='Braden'))
except NotFoundError as e: print('Record not updated')
Record not updated

与可怜的 Braden 不同,Charlie 没有被过滤掉。让我们更新他的待办事项。

todos.update(Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie'))
Todo(id=3, title='Finish development of FastHTML', detail=None, status='closed', name='Charlie')

最后,一旦被 .xtra 约束,只有 name 为 Charlie 的记录可以被删除。

try: todos.delete(1)
except NotFoundError as e: print('Record not updated')
Record not updated

Charlie 的待办事项是完成 FastHTML 的开发。虽然该框架会趋于稳定,但像任何好项目一样,它将在未来许多年里不断添加新功能并修正偶尔的错误。因此,Charlie 的待办事项是无意义的。让我们删除它。

todos.delete(ct.id)
<Table todo (id, title, detail, status, name)>

当插入一个待办事项时,xtra 字段会自动设置。这确保了我们不会意外地,例如,为其他用户插入项目。请注意,这里我们没有设置 name 字段,但它仍然包含在结果行中。

ct = todos.insert(Todo(title='Rewrite personal site in FastHTML', status='open'))
ct
Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')

如果我们试图将用户名更改为其他人,由于 xtra 的存在,该更改将被忽略。

ct.name = 'Braden'
todos.update(ct)
Todo(id=3, title='Rewrite personal site in FastHTML', detail=None, status='open', name='Charlie')

SQL 优先的设计

users = None
User = None
users = db.t.user
users
<Table user (name, email, year_started, pwd)>

(本节需要适当的文档说明。)

我们可以从表对象中提取我们表的 Dataclass 版本。通常这会以我们表名的单数大写形式命名,在本例中是 User

User = users.dataclass()
User(name='Braden', email='[email protected]', year_started=2018)
User(name='Braden', email='[email protected]', year_started=2018, pwd=UNSET)

实现

为新的数据存储实现 MiniDataAPI

对于创建新的实现,本规范中的代码示例是 API 的测试用例。新的实现应通过这些测试,以符合该规范。

实现

  • fastlite - 原始实现,仅适用于 Sqlite
  • fastsql - 一个基于优秀的 SQLAlchemy 库的、与 SQL 数据库无关的实现。