虽然我在日常工作中使用了很多SQLAlchemy,但我对它的 api 并不满意,与我开始 python 之旅的Django ORM相比,我发现它有点困难! Django ORM 可能是 python 生态系统中最好的ORM恕我直言。不幸的是,它对 Django 项目很紧,不能在其他地方使用。所以我决定寻找 SQLAlchemy 的替代品,从这篇文章开始,我将向你展示我最喜欢的那些。

对于数据工程师/数据科学家来说,这个库对你特别有用,因为它有一个更干净的 api 来从数据库中获取数据。

简介

Prisma是最近的 TypeScript ORM,它采用了不同于其前身的方法并由于TypeScript而专注于类型安全。

不过不用担心,在本教程中我们不会讨论 TypeScript,我将向您展示为这个项目创建的_unofficial_python 客户端。为了简要介绍它,我将借用他的话:

Prisma Client Python 是构建在 Prisma 之上的下一代 ORM,它从头开始设计,易于使用和正确性。

安装

要安装它,您可以使用 pip 或诗歌和以下命令:

$ pip install prisma
# or
$ poetry add prisma

进入全屏模式 退出全屏模式

prisma python 客户端带有一个 CLI,它实际上嵌入了官方的prisma CLI和一些额外的命令。要确保它已安装,请在 shell 中键入以下内容:

$ prisma -h

◭  Prisma is a modern DB toolkit to query, migrate and model your database (https://prisma.io)

Usage

  $ prisma [command]

Commands

            init   Setup Prisma for your app
        generate   Generate artifacts (e.g. Prisma Client)
              db   Manage your database schema and lifecycle
         migrate   Migrate your database
          studio   Browse your data with Prisma Studio
          format   Format your schema
...

进入全屏模式 退出全屏模式

注意:当您第一次调用此命令时,您可能会注意到它会下载一些二进制文件。这是正常的,它会获取 prisma 使用的节点 prisma cli 和引擎。 😁

Prisma 架构文件

在 Prima 中,这一切都始于一个schema.prisma文件,您可以在其中定义您的业务模型和它们之间的关系。之后,您将调用 prisma CLI 命令来生成客户端。这与使用Active Record的 Django ORM 和 SQLAlchemy 等 ORM 不同

我们用数据和方法定义模型类以与数据库交互的模式。这里 Prisma(至少是 python 库)负责定义操作数据库的 api 以及操作中涉及的不同数据模型。

所以让我们从这个模式开始(保存在一个名为schema.prisma的文件中):

datasource db {
  provider = "sqlite"
  url      = "file:db.sqlite"
}

generator client {
  provider  = "prisma-client-py"
}

model User {
  id         String   @id @default(uuid())
  firstname  String
  lastname   String
  is_admin   Boolean  @default(false)
  email      String   @unique
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
  posts      Post[] // not represented in the database

  @@map("user")
}

model Post {
  id         String   @id @default(uuid())
  title      String
  content    String
  published  Boolean  @default(false)
  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
  // user field will not be represented in the database
  // To link a post to its author we precise which field of this table
  // is a foreign key (user_id) and on which field of the other table
  // it points (id)
  user       User     @relation(fields: [user_id], references: [id])
  user_id    String

  @@map("post")
}

进入全屏模式 退出全屏模式

好的,让我们分解一下我们所拥有的。

数据源

在本节中,我们定义了如何连接到数据库。我们提供:

  • aprovider:在我们的例子中是sqlite,但 prisma 支持MySQLMariaDBPostgreSQLSQL Server、CockroachDB和MongoDB。

  • url:连接字符串,包含连接所需的所有信息。对于sqlite这很简单,我们传递一个以file:为前缀的文件名路径。对于其他数据库,请参阅prisma 文档。

注意:在写这篇文章的时候CockroachDB不被python客户端支持,但是应该不会花很长时间。

此外,您可以使用环境变量来不泄露 prisma 模式文件中的秘密。例如,您可以将前面示例中的url值替换为env("DATABASE_URL")。prisma 客户端将知道它需要从名为DATABASE_URL的环境变量中获取数据库连接字符串。

发电机

在本节中,我们指定将生成可用于查询的 python 客户端的脚本。

对于我们的 python 库,该值为prisma-client-py,但您可以根据需要创建一个自定义生成器。

警告!这是一个高级主题。 🙂

型号

我们终于有了_model_定义部分。在我们的示例中,我们定义了两个模型UserPost。这两个模型将代表数据库db.sqlite中的两个,每个模型中定义的属性将

代表表

要了解 prisma 中可用的所有类型,请参阅此

的官方文档。他们并不多。

一些注意事项:

  • 要定义一个主字段,我们使用属性@id并确保使用属性@default结合函数uuid()使用 uuid 填充该值

  • User模型中,为is_admin布尔字段分配了默认值false。这意味着创建的每个用户都不是管理员。这个布尔字段的另一个可能值当然是true

  • User模型中,为email字段分配了@unique属性,这意味着我们不能在用户表中拥有两次相同的电子邮件。

  • UserPost模型中,created_at日期时间字段被分配一个default属性,函数now()这意味着每次我们创建记录时,该字段将自动填充当前时间戳。

  • UserPost模型中,为updated_at日期时间字段分配了一个@updatedAt属性,这意味着每次我们更新记录时,该字段将自动填充当前时间戳。

  • 我们定义了PostUser模型之间的 1-n 关系,由userposts字段表示。这些字段不在数据库中表示,而是在 prisma 级别以轻松管理关系。

  • @@map模型属性用于定义数据库中表的名称。如果我们不定义它,默认情况下 prisma 将使用模型名称

基本用法

现在我们有了一个模式文件,我们可以使用以下命令创建数据库并生成客户端:

$ prisma db push

进入全屏模式 退出全屏模式

通常,您应该有类似于以下的输出:

Prisma schema loaded from schema.prisma
Datasource "db": SQLite database "db.sqlite" at "file:db.sqlite"

SQLite database db.sqlite created at file:db.sqlite
...
✔ Generated Prisma Client Python (v0.6.6) to ...

进入全屏模式 退出全屏模式

创建一个包含以下内容的 python 文件。这是使用 python 客户端的最低要求。

import asyncio
from prisma import Prisma


async def main() -> None:
    db = Prisma()
    await db.connect()
    await db.user.find_first()

    await db.disconnect()


if __name__ == '__main__':
    asyncio.run(main())

进入全屏模式 退出全屏模式

如果你运行它,它应该什么都不返回,因为数据库中没有记录,但至少一切都很好:)

好吧,如果你不想要async接口,例如你想在一个同步的 web 框架中采用它,或者你是一个不习惯在这个范式中工作的数据工程师/数据科学家,你可以生成客户端一个同步接口。首先,您必须修改架构文件的 generator 部分以插入interface信息。它将如下所示:

// I just put the interesting part, not the whole file
generator client {
  provider  = "prisma-client-py"
  interface = "sync"
}

进入全屏模式 退出全屏模式

现在重新生成客户端:

$ prisma db push

进入全屏模式 退出全屏模式

您应该能够运行以下脚本:

from prisma import Prisma


def main() -> None:
    db = Prisma()
    db.connect()
    db.user.find_first()

    db.disconnect()


if __name__ == '__main__':
    main()

进入全屏模式 退出全屏模式

我们现在很好!在下面的示例中,我将使用异步示例,但您需要做的就是使其工作

与您的同步客户端是删除async/await关键字。 😉

好的,让我们创建一些用户和帖子。

import asyncio

from prisma import Prisma


async def main() -> None:
    db = Prisma()
    await db.connect()
    user_1 = await db.user.create(
        data={
            'firstname': 'Kevin',
            'lastname': 'Bogard',
            'email': 'kevin@bogard.com',
            'posts': {
                'create': [
                    {
                        'title': 'Prisma is awesome',
                        'content': 'This is the truth!'
                    },
                    {
                        'title': 'Do you know the Bogard family?',
                        'content': "If not, you probably don't know King of Fighters!"
                    }
                ]
            }
        }
    )
    user_2 = await db.user.create(
        data={
            'firstname': 'Rolland',
            'lastname': 'Beaugosse',
            'email': 'rolland@beaugosse.fr',
            'posts': {
                'create': [
                    {
                        'title': 'Prisma is awesome',
                        'content': 'you should use it!'
                    },
                    {
                        'title': 'What other ORM do you use?',
                        'content': 'Tell me in the comments'
                    }
                ]
            }
        }
    )
    print(user_1)
    print(user_2.json(indent=2))

    await db.disconnect()


if __name__ == '__main__':
    asyncio.run(main())

进入全屏模式 退出全屏模式

您注意到,如果需要,我们可以在一个查询中创建用户及其相关帖子。由于 prisma 以隐式方式为我们管理关系,查询很简单。

这个查询的结果就是创建的用户模型,它是一个pydantic模型,所以

您可以使用它的方法来内省数据。如果您不了解 pydantic,它是一个强大的数据验证库。我有一个关于它的一些功能的教程。

[

凯文特乌达

](https://lewoudar.medium.com/hidden-powers-of-pydantic-558a8109e45)[

pydantic 的隐藏力量。发现 pydantic 的一些关键特性... |通过凯文 Tewouda |中等的

凯文·特乌达· 2022 年 3 月 11 日·

中号标志lewoudar.Medium

](https://lewoudar.medium.com/hidden-powers-of-pydantic-558a8109e45)

如果你好奇,你可以在模块prisma.models中查看生成模型的定义。

注意:如果您不针对 sqlite,您可以像这样批量创建多个对象

# not possible in sqlite
users = await db.user.create_many(
    data=[
        {'firstname': 'Kevin', 'lastname': 'Bogard', 'email': 'kevin@bogard.com'},
        {'firstname': 'Rolland', 'lastname': 'Beaugosse', 'email': 'rolland@beaugosse.com'},
    ]
)

进入全屏模式 退出全屏模式

现在让我们看看如何使用 prisma 和 python 客户端查询对象。

# I don't put all the imports, you have them in the previous example :)
from datetime import datetime, timedelta

# returns the first user of the database
user = await db.user.find_first()
print(user.json(indent=2))

# returns the user where email is rolland@beaugosse.com and include post information
user = await db.user.find_unique(
    where={'email': 'rolland@beaugosse.fr'},
    include={'posts': True}
)
print(user.json(indent=2))

# lists posts from the first user
posts = await db.post.find_many(
    where={
        'user': {
            'is': {
                'email': 'kevin@bogard.com',
            }
        }
    }
)
for post in posts:
    print(post.json(indent=2))

# find posts
# where title contains "Prisma" or creation date is less than 1 hour
# order by creation date descendant
# and include user information
posts = await db.post.find_many(
    where={
        'OR': [
            {
                'title': {
                    'contains': 'Prisma'
                }
            },
            {
                'created_at': {
                    'gt': datetime.utcnow() - timedelta(hours=1)
                }
            }
        ]
    },
    include={'user': True},
    order={'created_at': 'desc'}
)
for post in posts:
    print(post.json(indent=2))

进入全屏模式 退出全屏模式

使用 prisma 进行的查询非常易于理解且功能强大。要了解有关您可以做什么的更多信息,请查看 prisma 客户端参考和官方文档。

自省

如果您已经有一个数据库并想利用 prisma 怎么办? Prisma 用一个命令来支持您,该命令可以检查数据库并在您的模式文件中生成模型。具体来说,您必须使用 datasourcegenerator 部分定义一个基本文件,如下所示:

datasource db {
  provider = "sqlite"
  // replace the url with correct one in your case
  url      = "file:db.sqlite"
}

generator client {
  provider  = "prisma-client-py"
}

进入全屏模式 退出全屏模式

并运行命令:

$ prisma db pull

进入全屏模式 退出全屏模式

Prisma 将使用从数据库中检索到的元数据自动填充模型。当然,它不是

完美,您可能需要调整一些信息,但这是一个好的开始。有关 prisma introspection 的更多信息可以在官方文档的这个部分中找到。

迁移

prisma 的另一个重要特点是它的迁移系统。这允许更流畅的数据库体验,例如允许我们在 Web 应用程序中进行增量更改。当您创建/更新/删除模型时,您可以创建一个稍后将在生产数据库中应用的迁移文件。例如,由于到目前为止我们已经创建了两个模型,我们可以使用以下命令创建迁移文件:

$ prisma migrate dev --name "create user and post models"

进入全屏模式 退出全屏模式

注意:您应该注意到 CLI 会询问您是否允许继续,因为它会清除您本地的所有数据

数据库。这是正常的,因为这是我们的第一次迁移,并且只在我们的本地数据库上完成。

现在我们有一个 migrations 文件夹,其中包含包含不同迁移文件的子文件夹。一旦我们完成了所有

必要的测试并确定我们的更改,我们可以使用以下命令将其应用于我们的生产数据库:

# be sure the url in the datasource section of your prisma file
# has the correct value
$ prisma migrate deploy

进入全屏模式 退出全屏模式

注意:与Django migrations或alembic等其他迁移系统不同,您没有升级或降级数据库的系统。

总是使用新的迁移更新数据库。因此,如果要回滚迁移,则必须创建一个新的迁移文件。

现在重新创建一些用户和帖子(您可以使用我们的第一个示例)。假设我们现在要计算帖子被查看的次数,所以我们在模型中添加:

...
model Post {
  id         String   @id @default(uuid())
  title      String
  content    String
  published  Boolean  @default(false)
  // we have a new field to count the number of times
  // a post is viewed
  views      Int      @default(0)
  ...
}

进入全屏模式 退出全屏模式

您可以使用prisma migrate创建新的迁移:

$ prisma migrate dev --name "add views field in post model"

进入全屏模式 退出全屏模式

注意:这次没有删除数据库,因为我们已经进行了迁移。 😉

有关迁移的更多信息可以在官方文档的这个部分中找到。

关系

对我来说,prisma 中最好的功能可能是我们可以轻松查询深度嵌套和链接的模型,这要归功于它如何为我们处理后台关系。对此,它支持以下关系:

  • 1-n(一对多)关系

  • 1-1(一对一)关系

  • n-m(多对多)关系

  • 字段模型引用自身的自我关系

如果这些条款对您没有影响(尤其是前三个),我邀请您阅读这篇文章。

我们已经看到了一对多的关系,其中一个用户有 0 到多个帖子,而一个帖子只有一个用户。现在让我们看看如何定义一对一和多对多关系。

一对一关系

假设我们要在 profile 表中扩展用户信息,我们将在模式中添加以下模型并更新User模型。

model User {
 ...
 // we add an optional profile field
  profile    Profile?

  @@map("user")
}

model Profile {
  id      String @id @default(uuid())
  bio     String
  user    User   @relation(fields: [user_id], references: [id])
  // the @unique attribute is what make the difference between a
  // 1-n relation and a 1-1 relation
  user_id String @unique

  @@map("profile")
}

进入全屏模式 退出全屏模式

除了两件事之外,它与我们定义 user<->post 关系的方式并没有真正的不同:

  • User模型中的profile字段在类型旁边有一个?符号,因为我们希望它是可选的。这是因为我们已经有一些用户在数据库中,所以如果这个字段是必填项,那么就可以更新数据库。当然,如果它是一个新项目,您可以根据需要将其设为强制性。我只想说它与一对一关系的定义无关。

  • Profile模型中,user_id字段标记为@unique。如果您查看Post模型以及我们如何定义相同的字段,您会注意到它是唯一的区别,这个属性是创建一对一关系的原因在UserProfile之间。

现在您可以使用以下命令升级数据库:

$ prisma migrate dev --name "add profile model"

进入全屏模式 退出全屏模式

并开始玩它:

user = await db.user.find_first()
profile = await db.profile.create(
    data={
        'bio': "I'm cool",
        'user': {
            # we link the profile to its user
            'connect': {
                'id': user.id
            }
        }
    }
)
print(profile.json(indent=2))

# we get the same profile
profile = await db.profile.find_first(
    where={
        'user': {
            'is': {
                'email': user.email
            }
        }
    },
    include={'user': True}
)
print(profile.json(indent=2))

进入全屏模式 退出全屏模式

多对多关系

现在,假设我们想将一些类别附加到帖子中,我们可以将架构更新为如下所示:

model Post {
  ...
  categories Category[] @relation(references: [id])

  @@map("post")
}

model Category {
  id    String @id @default(uuid())
  name  String
  posts Post[] @relation(references: [id])

  @@map("category")
}

进入全屏模式 退出全屏模式

并使用以下命令迁移数据库:

$ prisma migrate dev --name "add category model"

进入全屏模式 退出全屏模式

如果查看迁移文件,您会注意到 prisma 创建了一个 glue 表来处理这种关系。多亏了 prisma,现在我们可以做以下事情。

# we create a user, post and category together!
user = await db.user.create(
    data={
        'firstname': 'foo',
        'lastname': 'bar',
        'email': 'foo@bar.com',
        'posts': {
            'create': [
                {
                    'title': 'Prisma is so good',
                    'content': 'oh yeah!',
                    'categories': {
                        'create': [
                            {'name': 'prisma'},
                            {'name': 'orm'}
                        ]
                    }
                }
            ]
        }
    }
)
print(user.json(indent=2))

# we fetch the categories
categories = await db.category.find_many(
    where={
        'posts': {
            'every': {
                'title': {
                    'contains': 'Prisma'
                }
            }
        }
    },
    include={'posts': True}
)
for category in categories:
    print(category.json(indent=2))

进入全屏模式 退出全屏模式

有时,您想在 n-m 关系中添加一些属性,在这种情况下,您必须手动创建中间表。更多信息请参见官方文档的部分。

我也不会解释自我关系,因为本教程已经很长了!随意在文档的部分中了解更多信息。

cli

我想总结一些你在使用 prisma 时经常使用的命令。

  • prisma format:顾名思义,它格式化架构文件并验证其内容。

  • prisma db pull:从数据库中获取模型并生成客户端。

  • prisma generate:生成客户端,在您想要修改模型而不推送数据库中的更改的拉策略中很有用。

  • prisma db push:当您测试应用程序时,这是您应该用来重置数据库并生成客户端的命令。

  • prisma migrate dev --name <name>:当您确定您的更改时,在db push之后使用。它将创建一个迁移文件,将其应用于您的本地环境并生成客户端。

  • prisma migrate deploy:在生产环境中应用迁移文件。

  • prisma py version:显示有关 python 客户端、支持的 prisma 版本、二进制文件等的调试信息...

总结

好的,在本教程的最后,我将列出我看到的 prisma 的优点和缺点。

优点

  • 简单整洁的 api 到CRUD数据。

  • 自动完成功能可在支持语言服务器协议和pyrightlikeVS Code的编辑器上更快地编写查询。不幸的是,Pycharm对TypedDict的支持有限,它现在不适用于。

  • 迁移系统。

  • 通过读取数据库上的元数据来生成模型和客户端的自省功能(这真的很酷!)。

  • 支持许多关系数据库,甚至是我在学习 prisma 时发现的一个相对较新的数据库,CockroachDB。它还支持 MongoDB,这是一个 NoSQL 数据库。

缺点

  • 即使它支持许多关系数据库,列表中也有一个明显的缺失:Oracle。如果您为习惯于使用此数据库的银行或大公司工作,则可能会受到限制。如果您想关注此主题,则有一个正在进行的问题。

  • 我们不能比较同一张表的两个字段,我们可以用 Django 中的F 表达式来做。如果您想关注此主题,则有一个正在进行的问题。解决方法是使用原始查询。

  • 数据库支持存在一些不一致之处,其中某些字段在某些数据库中实现,但其他字段则没有,例如在 sqlite 上未实现的JSON字段(这里存在一个持续的问题),甚至在实现它的地方,查询的方式从一个数据库到另一个数据库不同。这不是我对 ORM 的期望。它应该是与数据库无关的

  • 我们没有对模型的 mixins 支持,以避免在模型之间重复字段,如日期时间字段(创建_at,更新_at)。

反正prisma是一个比较新的项目,用起来很爽。我鼓励您尝试一下并支持 python 客户端(至少在GitHub上打个星)。

对于不喜欢 SQL 的我来说,能够从数据库中检索模型并使用 prisma 进行查询是一股新鲜空气。 🤣

这就是本教程的全部内容,敬请期待我们将一起探索的下一个ORM! 😉


如果您喜欢我的文章并想继续和我一起学习,请不要犹豫在这里关注我并订阅我的时事通讯😉

Logo

Python社区为您提供最前沿的新闻资讯和知识内容

更多推荐