Tortoise-ORM 深入浅出第一篇
Tortoise-ORM 学习笔记
关于tortoise-orm,他是一个支持异步的数据库ORM,使用比较简单,在功能上面和Django的ORM也比较类似,所以如果你对Django的ORM本身就比较熟悉,同时也有以后想迁移比如FastAPI或者要使用纯异步编程的需求,那Tortoise-orm都是你一定要学习的一个框架。
官方的定位
Tortoise ORM 是一款易于使用的 asyncio ORM(对象关系映射器),其灵感来自 Django。 Tortoise ORM 在设计时考虑到了关系,并对优秀且流行的 Django ORM 表示钦佩。它的设计理念是,你不仅要处理表格,还要处理关系数据。
环境和版本
Python版本是 3.9.10,理论只要你是3.8以后的版本都是可以支持的。主要官方支持的数据库类型是(SQLite、MySQL、PostgreSQL)
tortoise-orm 0.20.0
aiosqlite 0.17.0
aiomysql 0.2.0
我准备要把这个框架彻底吃透,所以会将官方文档也全部都过一遍,同时也会将例子都逐个尝试,在结合自己的经验来一起来逐个过一遍。因为实际项目中使用sqlite的场景也并不多,所以我的测试都会基于mysql:5.7版本来进行。
基本的使用
先通过简单的引用和使用,来看看最基本的使用,整体还是非常的简单和简洁。如果你对ORM不熟悉,我建议你先熟悉了在来看
下面的代码我加入了一下注释,你可以简单看一下。
from tortoise import Tortoise, fields, run_async
from tortoise.models import Model
# 定义个数据模型
class Event(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
detetime = fields.DatetimeField(null=True)
class Meta:
# 数据表的名称
table = "event"
def __str__(self):
return self.name
async def run():
await Tortoise.init(
db_url="mysql://root:123456@127.0.0.1:3306/test",
modules={"models": ["__main__"]},
)
await Tortoise.generate_schemas()
event = await Event.create(name="Test")
await Event.filter(id=event.id).update(name="Updated name")
print(await Event.filter(name="Updated name").first())
# # Updated name
await Event(name="Test 2").save()
# 将所有的结果返回,并直接返回一个列表
# flat为True,则直接返回列表可以遍历,而False也为元组包裹
print(await Event.all().values_list("id", flat=False))
# [1, 2, 3, 4, 5, 6, 7, 8, 9]
print(await Event.all().values_list("id", flat=False))
# [(1,), (2,), (3,), (4,), (5,), (6,), (7,), (8,), (9,), (10,)]
# 将所有的结果返回,并以列表包含字典的方式,字典的key则是字段名称
print(await Event.all().values("id", "name"))
# [{'id': 1, 'name': 'Updated name'}, {'id': 2, 'name': 'Updated name'}, {'id': 3, 'name': 'Updated name'}, {'id': 4, 'name': 'Test 2'}, {'id': 5, 'name': 'Updated name'}, {'id': 6, 'name': 'Test 2'}, {'id': 7, 'name': 'Updated name'}, {'id': 8, 'name': 'Test 2'}, {'id': 9, 'name': 'Test 2'}]
if __name__ == "__main__":
run_async(run())
第二个小例子
这里我们主要是在创建数据结构的时候,可以给数据字段通过description添加描述,并在Meta的table_description可以给数据表增加描述。
from tortoise import Tortoise, fields, run_async
from tortoise.models import Model
class Event(Model):
id = fields.IntField(pk=True)
name = fields.TextField(description="事件名称")
detetime = fields.DatetimeField(null=True, description="事件时间")
class Meta:
table = "event"
table_description = "数据表的一些描述说明"
def __str__(self):
return self.name
async def run():
await Tortoise.init(
db_url="mysql://root:123456@127.0.0.1:3306/test",
modules={"models": ["__main__"]},
)
await Tortoise.generate_schemas()
# 在数据表里面创建一个名字叫Test的事件名称,并将对象返回到 event上
event = await Event.create(name="Test")
# 通过filter查询id=上面创建的对象id,并通过 update 把事件名称更改为Updated name
await Event.filter(id=event.id).update(name="Updated name")
# 打印查询名字为 Updated name 的第一条数据
print(await Event.filter(name="Updated name").first())
# Updated name
# 再次创建一个名叫 Test2的事件对象,并通过.save()存储
await Event(name="Test2").save()
# 查询所有的事件数据,并将id输出列表
print(await Event.all().values_list("id", flat=True))
# [1, 2, 3]
# 查询所有的事件数据,并将id、name以字典的模式输出
print(await Event.all().values("id", "name"))
# [{'id': 1, 'name': 'Updated name'}, {'id': 2, 'name': 'Updated name'}, {'id': 3, 'name': 'Test2'}]
if __name__ == "__main__":
run_async(run())
预加载prefetch_related(关联查询)
根据官网的解释是预加载,实际也就是正向查询和反向查询,同时在查询的时候把关联表的数据查询出来。 实际说实话这个官方例子并不好,甚至某种程度来说还有点复杂和难以理解。 先用官方的例子,大致有概念以后在用我们便于理解的例子来重新过一遍。
官方例子
from tortoise import Tortoise, fields, run_async
from tortoise.models import Model
from tortoise.query_utils import Prefetch
class Tournament(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
events: fields.ReverseRelation["Event"]
def __str__(self):
return self.name
class Team(Model):
id = fields.IntField(pk=True)
name = fields.TextField()
events: fields.ManyToManyRelation["Event"]
def __str__(self):
return self.name
class Event(Model):
id = fields.IntField(pk=True)
name = fields.TextField(description="事件名称")
tournament: fields.ForeignKeyRelation[Tournament] = fields.ForeignKeyField(
"models.Tournament", related_name="events"
)
participants: fields.ManyToManyRelation["Team"] = fields.ManyToManyField(
"models.Team", related_name="events", through="event_team"
)
def __str__(self):
return self.name
async def run():
await Tortoise.init(
db_url="mysql://root:123456@127.0.0.1:3306/test",
modules={"models": ["__main__"]},
)
await Tortoise.generate_schemas()
# 先在 Tournament 创建 名字是 tournament 的数据,并返回在 tournament 中
tournament = await Tournament.create(name="tournament")
# 在创建事件表的数据,名称分别是 First、Second,他们关联的比赛分别是 刚创建的 tournament 对象
await Event.create(name="First", tournament=tournament)
await Event.create(name="Second", tournament=tournament)
# 先查询 Tournament 的所有比赛,然后通过 prefetch_related 预加载Event中的数据,在通过queryset,进行filter过滤取出名字是First的数据,并只取第一条
tournament_with_filtered = (
await Tournament.all()
.prefetch_related(Prefetch("events", queryset=Event.filter(name="First")))
.first()
)
# 这样就可以关联查询,类似赛事下的所有事件
event = await tournament_with_filtered.events
print(event)
# [<Event: 1>, <Event: 2>]
# 同时也支持对事件在进行过滤查询
event = await tournament_with_filtered.events.filter(name="First").first()
print(event)
# First
print(tournament_with_filtered, type(tournament_with_filtered))
# tournament <class '__main__.Tournament'>
tournament_with_filtered = await Tournament.first().prefetch_related("events")
print(tournament_with_filtered, type(tournament_with_filtered))
# tournament <class '__main__.Tournament'>
# 这个理解比较难一点了,通过查询 Tournament的所有数据,在预加载Tournament对象的events字段
# Prefetch 预加载配置,它包含了两个查询,每个查询都有一个特定的 queryset 参数,用于过滤 Event 对象。这里有两个 Prefetch 配置,每个都指定了一个不同的 queryset:
# 第一个 Prefetch 使用 Event.filter(name="First") 来过滤出所有名称为 "First" 的 Event 对象,并将这些对象预加载到 Tournament 的 events 字段中,并且将这些对象的集合命名为 to_attr_events_first。
# 第二个 Prefetch 使用 Event.filter(name="Second") 来过滤出所有名称为 "Second" 的 Event 对象,并将这些对象预加载到 Tournament 的 events 字段中,并且将这些对象的集合命名为 to_attr_events_second。
# first():这个方法用于获取查询结果的第一个元素。在 Tortoise-ORM 中,.first() 通常用于获取单个对象,而不是列表。
tournament_with_filtered_to_attr = (
await Tournament.all()
.prefetch_related(
Prefetch(
"events",
queryset=Event.filter(name="First"),
to_attr="to_attr_events_first",
),
Prefetch(
"events",
queryset=Event.filter(name="Second"),
to_attr="to_attr_events_second",
),
)
.first()
)
print(tournament_with_filtered_to_attr, type(tournament_with_filtered_to_attr))
# tournament <class '__main__.Tournament'>
print(tournament_with_filtered_to_attr.to_attr_events_first)
# [<Event: 1>]
print(tournament_with_filtered_to_attr.to_attr_events_second)
# [<Event: 2>]
if __name__ == "__main__":
run_async(run())
可能你看完上面的例子,和我一样懵逼,学习的复杂度感觉突然一下上来了。 接下来先用一个简单的例子,更好的理解
gushi_dict = {
"李白": ["将进酒", "古朗月行", "行路难", "蜀道难"],
"白居易": ["长恨歌", "卖炭翁", "琵琶行"],
"欧阳修": ["醉翁亭记", "卖油翁", "秋声赋", "画眉鸟"],
"李商隐": ["锦瑟", "夜雨寄北", "嫦娥", "贾生"],
}
我们想通过一个作者表和一个 诗词名称表,来进行一对多关系的设定,也就是一个作者可以有多个诗词,每个诗词只有一个作者。 同时要能通过 作者查询所有的古诗,也可以通过其中任意一篇古诗的名称来查询到作者。
from tortoise import Tortoise, fields, run_async
from tortoise.models import Model
from tortoise.query_utils import Prefetch
class Author(Model):
name = fields.CharField(max_length=60, description="作者")
class Meta:
table = "author"
table_description = "作者表"
class Book(Model):
book_name = fields.CharField(max_length=60, description="书名")
author = fields.ForeignKeyField(
"models.Author", description="作者", related_name="books_list"
)
class Meta:
table = "book"
table_description = "书籍表"
async def run():
await Tortoise.init(
db_url="mysql://root:123456@127.0.0.1:3306/test",
modules={"models": ["__main__"]},
)
await Tortoise.generate_schemas()
# 我们先创建一些作者进到表里
author_list = ["李白", "白居易", "欧阳修", "李商隐"]
for author in author_list:
await Author.create(name=author)
# 作者创建完毕
# 接下来在逐个填写对应的作者和对应的诗词(不推荐)
gushi_dict = {
"李白": ["将进酒", "古朗月行", "行路难", "蜀道难"],
"白居易": ["长恨歌", "卖炭翁", "琵琶行"],
"欧阳修": ["醉翁亭记", "卖油翁", "秋声赋", "画眉鸟"],
"李商隐": ["锦瑟", "夜雨寄北", "嫦娥", "贾生"],
}
for author_name in gushi_dict:
author_obj, _ = await Author.get_or_create(name=author_name)
for gushi_name in gushi_dict.get(author_name):
await Book.create(book_name=gushi_name, author=author_obj)
# 上面是第一种创建方式,但是这种方式对于存储来说,实际次数较多,使用Tortoise-orm 可以使用官方推荐的模式来操作。(推荐)
gushi_dict = {
"李白": ["将进酒", "古朗月行", "行路难", "蜀道难"],
"白居易": ["长恨歌", "卖炭翁", "琵琶行"],
"欧阳修": ["醉翁亭记", "卖油翁", "秋声赋", "画眉鸟"],
"李商隐": ["锦瑟", "夜雨寄北", "嫦娥", "贾生"],
}
# 用下面这个方法,可以更为快速的创建,因为次数少了一次查询,效率也会更高
for author in gushi_dict:
author_obj = await Author.create(name=author)
for gushi_name in gushi_dict.get(author):
await Book.create(book_name=gushi_name, author=author_obj)
# 接下来就是如何来进行查询了,比如我们想查询李白都写了那些古诗词(不推荐)
author_obj = await Author.get(name="李白")
books_obj = await Book.filter(author=author_obj)
print(books_obj)
# 在换成官方推荐的方式来试着改写一下(推荐)
author_obj = (
await Author.get(name="李白")
.prefetch_related(Prefetch("books_list", queryset=Book.all()))
.first()
)
print(await author_obj.books_list)
# 接下来我们在实现一个 通过书名来查询作者的模型(不推荐)
book_obj = await Book.get(book_name="卖炭翁")
author_obj = await book_obj.author
print(author_obj, author_obj.name)
# <Author> 白居易
# 而使用Tortoise-orm则可以这样改写(推荐)
book_obj = await Book.get(book_name="卖炭翁").prefetch_related("author")
print(book_obj.author, book_obj.author.name)
# <Author> 白居易
if __name__ == "__main__":
run_async(run())