К содержимому

База данных

Время поговорить о том, как сохранять данные бота в постоянном хранилище. Chiori предоставляет следующие компоненты для работы с базой данных:

  • DBModel: Представление элемента в базе данных.
  • DBTable: Отдельная таблица для базы данных.
  • ChioDB: Обёртка над базой данных Postgres.

Chiori во время запуска создаёт пул соединений через экземпляр ChioDB и подключается к базе данных Postgres. Во время загрузки плагинов в базе данных регистрируются таблицы CHioDB.

После, при помощи DI вы можете получить и использовать таблицы, которые были ранее подключены к базе данных.

Для небольшого проекта накладно тащить полноценную ORM. Все они выглядят достаточно громоздко и приносят больше неудобств (пока что). Возможно в будущем начнётся использоваться одна из ORM. Потому всё взаимодействие построек поверх asyncpg.

Для того чтобы начать хранить наши данные в базе, нужно определить модель и таблицу, которая будет её соответствовать.

Для того чтобы создать таблицу, создайте класс-наследник от DBTable.

class WarnsTable(DBTable, table='user_warns'):
def create_table(self) -> None:
"""Создаёт таблицу для базы данных."""
await self.pool.execute("CREATE TABLE IF NOT EXISTS user_warns (...)")

По традиции название класса оканчивается на Table. Параметр table определяет как будет называться таблица в базе данных.

Метод create_table вызывается, когда нужно создать таблицу в базе данных. Обычно здесь вызывается sql запрос для создания таблицы.

Для начала давайте определимся с моделью данных, которую мы собираемся хранить. Модель данных представляет из себя класс-наследник от DBModel. DBModel в свою очередь реализует метод сборки модели из asyncpg.Record.

Если вы немного запутались, давайте рассмотрим на примере.

@dataclass(slots=True)
class UserWarn(DBModel):
"""Предупреждение пользователя."""
id: int
user_id: int
guild_id: int
from_id: int
reason: str
created_at: datetime
expired_at; datetime | None

Для того чтобы взаимодействовать с базой данных, используется аттрибут pool и его методы:

  • execute: Выполнить запрос.
  • fetchrow: Выполнить запрос и получить результат.
  • fetch: Выполнить запрос и получить все результаты.
async def get_user(self, user_id: int) -> UserActive | None:
"""Получает пользователя по ID."""
cur = await self.pool.fetchrow(
"SELECT * FROM active WHERE user_id=$1", user_id
)
return None if cur is None else UserActive.from_row(cur)

Подобным образом выглядят все базовые методы для работы с базой данных. Сначала мы эти данные получаем, а после преобразуем их в модель.

Ранее мы затронули базовые методы. Всё сводится к созданию, получению, изменению, удалению.

async def select(self, guild_id: int) -> list[GuildRole]:
"""Возвращает все роли сервера."""
cur = await self.pool.fetch(
"SELECT * FROM roles_shop WHERE guild_id=$1", guild_id
)
return [GuildRole.from_row(row) for row in cur]
async def get(self, guild_id: int, role_id: int) -> GuildRole | None:
"""Возвращает роль из магазина."""
cur = await self.pool.fetchrow(
f"SELECT * FROM role_shop "
"WHERE guild_id=$1 AND role_id=$2",
guild_id,
role_id,
)
return None if cur is None else GuildRole.from_row(cur)
async def add(self, role: GuildRole) -> None:
"""Добавляет роль в магазин."""
await self.pool.execute(
"INSERT INTO roles_shop"
"(guild_id,role_id,price,require_role) VALUES($1,$2,$3,$4)",
role.guild_id,
role.role_id,
role.price,
role.require_role,
)
async def remove(self, guild_id: int, role_id: int) -> None:
"""Удаляет роль из магазина."""
await self.pool.execute(
"DELETE FROM roles_shop WHERE guild_id=$1 AND role_id=$2",
guild_id,
role_id,
)
async def set(self, role: GuildRole) -> None:
"""Обновляет роль на сервере."""
await self.pool.execute(
"UPDATE roles_shop SET require_role=$1, price=$2 "
"WHERE guild_id=$3 AND role_id=$4",
role.require_role,
role.price,
role.guild_id,
role.role_id,
)

ПОдобные методы должны быть реализованы во всех таблицах для комфортной работы.

После того как вы создали таблицу, вам обязательно нужно её зарегистрировать в клиенте. Делается это следующим образом.

plugin.add_table(WarnsTable)
# Или сразу
client.add_table(WarnsTable)

Чаще всего подключение происходит во время загрузки расширения. Отсюда и появляется необходимость в расширениях-помощниках.

@arc.loader
def loader(client: ChioClient) -> None:
"""Действия при загрузке плагина."""
plugin.set_config(WarnsConfig)
plugin.add_table(WarnsTable) # Вот и происходит подключение
client.add_plugin(plugin)

Теперь вы можете использовать созданную таблицу через DI:

@plugin.include
@arc.slash_command("warns", description="Предупреждения.")
async def user_warns(
ctx: ChioContext,
user: arc.Option[
hikari.User | None, arc.UserParams("Для какого пользователя.")
] = None,
warns: WarnsTable = arc.inject() # Наша табличка
) -> None:
"""Таблица лидеров по сообщениям."""