Альтернативы SQLAlchemy для вашего проекта — пример Prisma

Хотя я часто использую SQLAlchemy в своей повседневной работе, я не удовлетворен его api, который показался мне немного сложным по сравнению с Django ORM, с которого я начал свой путь в python! Django ORM — это, вероятно, лучший ORM IMHO в экосистеме python. К сожалению, он привязан к проекту Django и не может быть использован в других местах. Поэтому я решил поискать альтернативы SQLAlchemy и, начиная с этой статьи, я представлю вам те, которые мне больше всего понравились.

Эта библиотека может быть особенно полезна для инженеров данных / data scientist’ов, потому что она имеет более чистый api для получения данных из баз данных.

Введение

Prisma — это недавний TypeScript ORM, использующий отличный от своих предшественников подход и фокусирующийся на безопасности типов благодаря TypeScript.

Но не волнуйтесь, в этом руководстве мы не будем говорить о TypeScript, я представлю вам неофициальный клиент python, созданный для этого проекта. Чтобы представить его вкратце, я позаимствую его слова:

Prisma Client Python — это ORM нового поколения, построенный поверх Prisma, который был разработан с нуля для простоты использования и корректности.

Установка

Для установки вы можете использовать pip или poetry с помощью следующей команды:

$ pip install prisma
# or
$ poetry add prisma
Войти в полноэкранный режим Выйти из полноэкранного режима

Клиент prisma python поставляется с CLI, который фактически встраивает официальный prisma CLI с некоторыми дополнительными командами. Чтобы убедиться, что он установлен, введите в оболочке следующее:

$ 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
...
Войти в полноэкранный режим Выйти из полноэкранного режима

Примечание: вы можете заметить, что при первом вызове этой команды загружаются некоторые двоичные файлы. Это нормальное явление, когда загружается node prisma cli и движки, используемые prisma. 😁

Файл схемы Prisma

В Prima все начинается с файла schema.prisma, в котором вы определяете свои бизнес-модели и отношения между ними. После этого вы вызываете команду prisma CLI для генерации клиента. Это отличается от ORM, таких как Django ORM и SQLAlchemy, которые используют модель Active Record
где мы определяем классы моделей с данными и методами для взаимодействия с базой данных. Здесь 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")
}
Войти в полноэкранный режим Выход из полноэкранного режима

Итак, давайте разберем то, что у нас есть.

источник данных

В этом разделе мы определяем, как подключиться к базе данных. Мы предоставляем:

  • provider: в нашем случае это sqlite, но prisma поддерживает MySQL, MariaDB, PostgreSQL, SQL Server, CockroachDB и MongoDB.
  • url: строка подключения со всей информацией, необходимой для подключения. Для sqlite это довольно просто, мы передаем путь к имени файла с префиксом file:. Для других баз данных обратитесь к документации prisma.

Примечание: На момент написания этой статьи CockroachDB не поддерживается python-клиентом, но это не займет много времени.

Также вы можете использовать переменные окружения, чтобы не раскрывать секреты в вашем файле схемы prisma. Например, вы можете заменить значение url в предыдущем примере на env("DATABASE_URL"). Клиент prisma будет знать, что ему нужно получить строку подключения к базе данных из переменной окружения DATABASE_URL.

генератор

В этом разделе мы указываем скрипт, который будет генерировать python-клиент, который мы сможем использовать для наших запросов.
Для нашей библиотеки python значение prisma-client-py, но вы можете создать собственный генератор, если хотите.
Внимание! Это продвинутая тема. 🙂 🙂

модель

И, наконец, у нас есть раздел определения модели. В нашем примере мы определили две модели User и Post. Эти две модели будут представлять две таблицы в базе данных db.sqlite, а атрибуты, определенные в каждой из них, будут
представлять столбцы таблицы.
Чтобы узнать обо всех типах, доступных в prisma, обратитесь к этому
раздел официальной документации. Их не так много.

Некоторые примечания:

  • Для определения первичного поля мы используем атрибут @id и обеспечиваем заполнение значения uuid с помощью атрибута @default в сочетании с функцией uuid().
  • В модели User булево поле is_admin имеет значение по умолчанию false. Это означает, что каждый созданный пользователь не будет администратором. Другим возможным значением для этого булева поля является true.
  • В модели User полю email присвоен атрибут @unique, что означает, что мы не можем иметь один и тот же email дважды в таблице пользователей.
  • В моделях User и Post полю времени даты created_at присвоен атрибут default с функцией now(), что означает, что каждый раз, когда мы создаем запись, поле будет автоматически заполняться текущей меткой времени.
  • В моделях User и Post полю updated_at дата-время присвоен атрибут @updatedAt, который означает, что каждый раз, когда мы обновляем запись, поле будет автоматически заполняться текущей меткой времени.
  • Мы определили 1-n связь между моделями Post и User, которая представлена полями user и posts. Эти поля представлены не в базе данных, а на уровне призмы, чтобы легко управлять отношениями.
  • Атрибут модели @@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, например, вы хотите использовать его в синхронном веб-фреймворке, или вы data engineer / data scientist, который не привык работать в этой парадигме, вы можете сгенерировать клиент с синхронным интерфейсом. Сначала вы должны изменить секцию 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. Откройте для себя некоторые ключевые возможности pydantic…| by Kevin Tewouda | Medium

Кевин Тевуда ・・・
lewoudar.Medium

Если вам интересно, вы можете посмотреть определение сгенерированных моделей в модуле 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 прикроет вас командой, которая интроспективно исследует базу данных и генерирует модели в вашем файле схемы. Конкретно, вы должны определить базовый файл с секциями datasource и generator следующим образом:

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 можно найти в этом разделе официальной документации.

миграции

Еще одной замечательной особенностью prisma является система миграции. Она обеспечивает более плавную работу с базой данных, позволяя нам вносить постепенные изменения, например, в наше веб-приложение. Когда вы создаете / обновляете / удаляете модели, вы можете создать файл миграции, который позже будет применен к вашей производственной базе данных. Например, поскольку мы уже создали две модели, мы можем создать файл миграции с помощью следующей команды:

$ 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 до многих сообщений, а сообщение имеет только один пользователь. Теперь давайте посмотрим, как определить отношения один-к-одному и многие-ко-многим.

отношение один-к-одному

Допустим, мы хотим расширить информацию о пользователе в таблице профиля, добавим в схему следующую модель, а также обновим модель 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, за исключением двух вещей:

  • поле profile в модели User имеет знак ? рядом с типом, потому что мы хотим, чтобы оно было необязательным. Это связано с тем, что у нас уже есть некоторые пользователи в базе данных, поэтому если это поле будет обязательным, то можно будет обновить базу данных. Конечно, если это новый проект, вы можете сделать его обязательным, если хотите. Я просто хочу сказать, что это не связано с определением отношения один-к-одному.
  • В модели Profile поле user_id помечено как @unique. Если вы посмотрите на модель Post и то, как мы определяем то же самое поле, вы заметите, что это единственное различие, этот атрибут — то, что создает отношение один-к-одному между User и Profile.

Теперь вы можете обновить базу данных с помощью команды:

$ 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))
Enter fullscreen mode Выйти из полноэкранного режима

отношение «многие-ко-многим

Теперь, допустим, мы хотим прикрепить несколько категорий к посту, мы можем обновить схему примерно так:

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.

преимущества

  • Простой и аккуратный api для CRUD данных.
  • Функция автозаполнения позволяет быстрее писать запросы в редакторах, поддерживающих протокол Language Server Protocol и pyright, таких как VS Code. К сожалению, Pycharm имеет ограниченную поддержку TypedDict и пока не работает.
  • Система миграции.
  • Функция интроспекции для создания моделей и клиентов путем чтения метаданных в базе данных (это действительно круто!).
  • Поддержка многих реляционных баз данных, даже относительно новой, которую я только что обнаружил, изучая prisma, CockroachDB. Он также поддерживает MongoDB, которая является NoSQL базой данных.

недостатки

  • Несмотря на то, что он поддерживает многие реляционные базы данных, в этом списке есть одно заметное отсутствие: Oracle. Это может быть ограничивающим фактором, если вы работаете в банке или крупной корпорации, которая привыкла работать с этой базой данных. Если вы хотите следить за этой темой, существует постоянная проблема.
  • Мы не можем сравнить два поля одной таблицы, что мы можем сделать с помощью F-выражений в Django. Если вы хотите проследить за этой темой, то вопрос остается открытым. Обходным решением является использование необработанных запросов.
  • Есть некоторые несоответствия в поддержке баз данных, где некоторые поля реализованы в одних базах данных, но не в других, например, поле JSON, которое не реализовано в sqlite (здесь есть текущий вопрос), и даже там, где оно реализовано, способ запроса отличается от одной базы данных к другой. Это не то, что я ожидаю от ORM. Он должен быть независимым от базы данных.
  • У нас нет поддержки mixins для моделей, чтобы избежать повторения полей между моделями, например, полей времени (created_at, updated_at).

В любом случае, prisma — относительно новый проект, и он очень приятен в использовании. Я призываю вас попробовать и поддержать python-клиент (хотя бы звездой на GitHub).
Для меня, который не любит SQL, возможность получать модели из базы данных и делать запросы с помощью prisma — это глоток свежего воздуха. 🤣

На этом мы закончили, следите за следующими ORM, которые мы будем изучать вместе! 😉


Если вам понравилась моя статья и вы хотите продолжать учиться вместе со мной, не стесняйтесь следовать за мной здесь и подписывайтесь на мою рассылку 😉

Оцените статью
devanswers.ru
Добавить комментарий