Что вам понадобится
- Бот discord.js, написанный на TypeScript. Наш тип Node.js Bot запускает через tsx главный файл с расширением
.ts, поэтому ничего не нужно компилировать заранее: Как запустить Discord-бота на TypeScript.
- Тариф для ботов PlayNHost с типом Node.js Bot. В каждый тариф входит одна база данных.
- Node.js и npm на вашем компьютере, чтобы один раз написать миграцию.
1. Создайте базу данных и переменные
- Откройте сервер бота в панели, нажмите Базы данных, затем Новая база данных и Создать базу данных.
- Откроется страница новой базы (позже её можно открыть в разделе Ваши базы данных). Не закрывайте её и в разделе Готовый конфиг выберите Generic URL в списке Конфиг для: это строка подключения
mysql://..., которая нужна командной строке Prisma.
- Откройте страницу Бот, вкладку Окружение, нажмите Вставить файл .env и вставьте шесть строк со своими значениями. В
DATABASE_URL вставьте Generic URL в точности как он показан: пароли наших баз могут содержать символы вроде @ и +, которые Prisma требует в URL в процентной кодировке, а Generic URL уже закодирован.
DB_HOST=your-host
DB_PORT=3306
DB_NAME=your-database
DB_USER=your-username
DB_PASSWORD=your-password
DATABASE_URL=mysql://your-username:your-password@your-host:3306/your-database
- Переменные
DB_PASSWORD и DATABASE_URL получают метку Секрет: они сохраняются только для записи. Нажмите Импортировать 6 переменных.
Сам бот подключается по пяти значениям DB_, которые передаются адаптеру по одному, как в quickstart Prisma. DATABASE_URL нужен только командам prisma, которые выполняются во время установки.
2. Файлы проекта
package.json
{
"name": "my-discord-bot",
"private": true,
"type": "module",
"scripts": {
"postinstall": "prisma generate && prisma migrate deploy"
},
"dependencies": {
"@prisma/adapter-mariadb": "^7",
"@prisma/client": "^7",
"discord.js": "^14"
},
"devDependencies": {
"prisma": "^7"
}
}
prisma7.config.ts (Prisma 7.10 и новее используют это имя, чтобы оно не конфликтовало с prisma.config.ts из Prisma 8)
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env.DATABASE_URL,
},
});
prisma/schema.prisma
generator client {
provider = "prisma-client"
output = "../generated/prisma"
}
datasource db {
provider = "mysql"
}
model Warning {
id Int @id @default(autoincrement())
guildId String @db.VarChar(20)
userId String @db.VarChar(20)
reason String @db.VarChar(200)
createdAt DateTime @default(now())
@@index([guildId, userId])
}
index.ts
import { Client, Events, GatewayIntentBits, InteractionContextType, MessageFlags, REST, Routes, SlashCommandBuilder } from "discord.js";
import { PrismaMariaDb } from "@prisma/adapter-mariadb";
import { PrismaClient } from "./generated/prisma/client";
// One client for the whole bot. The values come from the Environment pane.
const adapter = new PrismaMariaDb({
host: process.env.DB_HOST,
port: Number(process.env.DB_PORT),
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
connectionLimit: 5,
});
const prisma = new PrismaClient({ adapter });
const commands = [
new SlashCommandBuilder()
.setName("warn")
.setDescription("Warn a member and count their warnings")
.setContexts(InteractionContextType.Guild)
.addUserOption((o) => o.setName("user").setDescription("Who to warn").setRequired(true))
.addStringOption((o) => o.setName("reason").setDescription("Why").setRequired(true).setMaxLength(200))
.toJSON(),
];
const client = new Client({ intents: [GatewayIntentBits.Guilds] });
client.once(Events.ClientReady, async (c) => {
await new REST().setToken(process.env.DISCORD_TOKEN!).put(Routes.applicationCommands(c.user.id), { body: commands });
console.log(`Logged in as ${c.user.tag}`);
});
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== "warn" || !interaction.guildId) return;
const user = interaction.options.getUser("user", true);
const reason = interaction.options.getString("reason", true);
try {
await prisma.warning.create({ data: { guildId: interaction.guildId, userId: user.id, reason } });
const total = await prisma.warning.count({ where: { guildId: interaction.guildId, userId: user.id } });
await interaction.reply(`${user} has ${total} warning(s).`);
} catch (err) {
console.error(err);
if (!interaction.replied) await interaction.reply({ content: "Could not save the warning. Try again later.", flags: MessageFlags.Ephemeral }).catch(console.error);
}
});
client.login(process.env.DISCORD_TOKEN);
setContexts(InteractionContextType.Guild) и проверка !interaction.guildId не дают использовать /warn в личных сообщениях. try/catch здесь важен: без него discord.js передаёт неудавшийся запрос в своё событие error, а если там никто не слушает, Node.js останавливает бота.
3. Напишите первую миграцию на своём компьютере
В папке проекта на вашем компьютере (в Windows создайте папки prisma\migrations\0_init вручную вместо mkdir -p):
npm install --ignore-scripts
mkdir -p prisma/migrations/0_init
npx prisma migrate diff --from-empty --to-schema prisma/schema.prisma --script > prisma/migrations/0_init/migration.sql
migrate diff сравнивает пустую базу с вашей схемой и пишет SQL, не подключаясь ни к какой базе. --ignore-scripts пропускает postinstall, который попытался бы обратиться к базе. Файл начинается с -- CreateTable и CREATE TABLE `Warning`.
4. Загрузите файлы и запустите
- В разделе Файлы загрузите
package.json, prisma7.config.ts, index.ts и папку prisma с schema.prisma и migrations. Не загружайте node_modules и generated: сервер соберёт обе.
- На странице Бот, на вкладке Код, укажите в поле Файл запуска значение
index.ts.
- Нажмите Запустить и откройте Консоль. Установка выполнит ваш
postinstall: вы увидите Generated Prisma Client (7.10.1), затем All migrations have been successfully applied., затем [PlayNHost] Starting your bot и Logged in as ....
В консоли также появится npm warn install-scripts 2 packages had install scripts blocked для prisma и @prisma/engines. Версия npm в нашем образе Node.js 22 блокирует скрипты установки зависимостей, пока они не разрешены. Ваш собственный postinstall всё равно выполняется, и в нашем тесте Prisma сгенерировал клиент, применил миграции и выполнил запросы без этих скриптов.
Почему не prisma migrate dev?
prisma migrate dev сначала создаёт временную shadow-базу, чтобы проверить ваши миграции. Пользователь базы вашего бота может работать внутри своей базы, но не может создавать новые, поэтому на наших серверах команда останавливается с ошибкой:
Prisma Migrate could not create the shadow database. Please make sure the database user has permission to create databases. Read more about the shadow database (and workarounds) at https://pris.ly/d/migrate-shadow
Original error: Error code: P1010
User was denied access on the database `prisma_migrate_shadow_db_...`
prisma migrate deploy только применяет файлы миграций и не требует shadow-базы, поэтому postinstall использует именно его. Нельзя и направить migrate dev на базу бота со своего компьютера: база принимает соединения только с серверов PlayNHost и из phpMyAdmin.
Как менять схему потом
- На своём компьютере выполните
npx prisma migrate dev --name и имя изменения, на собственной MySQL или MariaDB, например в Docker: задайте DATABASE_URL в терминале для этой базы, потому что prisma7.config.ts читает её из окружения. Команда создаст новую папку в prisma/migrations.
- Загрузите новую папку миграции и изменённый
schema.prisma.
- Сервер выполняет
npm install, а вместе с ним ваш postinstall, только когда изменились package.json, package-lock.json или версия Node.js, когда отсутствует node_modules, или при первом запуске больше чем через сутки после последней установки. Чтобы применить миграцию сразу, измените package.json (например, поле version) или удалите папку node_modules в разделе Файлы, затем перезапустите бота.
Память: тяжёлая часть это установка
Мы установили этот проект с нуля в контейнере с лимитом 512 MB, как у нашего самого маленького тарифа для ботов. Установка, включая prisma generate и prisma migrate deploy, заняла 41 секунду и в одном запуске достигла пика 386 MB памяти (без файлового кэша), а во втором 417 MB, и ни в одном не было принудительной остановки из-за нехватки памяти. Установка укладывается в тариф на 512 MB с запасом меньше 100 MB, поэтому проект побольше или больше пакетов могут не поместиться. Та же установка без Prisma (только discord.js и mysql2) достигла пика 147 MB.
Лимиты и ошибки
- 20 одновременных соединений на пользователя базы, считая все процессы с одним и тем же логином. Держите
connectionLimit небольшим; для бота хватает и 5.
- Запрос, который выполняется дольше 120 секунд, останавливается с ошибкой 1969
Query execution was interrupted (max_statement_time exceeded).
- 500 MB на базу данных: на 500 MB запись останавливается, пока вы не удалите строки. Резервные копии сервера не включают базу данных. Время от времени экспортируйте её из phpMyAdmin.
- Проблемы с подключением имеют те же причины, что и с mysql2 (неверный пароль, неверный хост или порт, отсутствующая переменная в разделе Окружение, подключение с вашего компьютера), но сообщения другие: адаптер MariaDB использует драйвер
mariadb, а не mysql2, и мы не записывали его сообщения. Причины перечислены в статье Как использовать базу данных MySQL с ботом discord.js.
Если нужен простой SQL с mysql2 или aiomysql: discord.js (mysql2), discord.py (aiomysql). Общий обзор: Как использовать базу данных с Discord-ботом. Нужен сервер для бота с базой данных в комплекте? Посмотрите тарифы для ботов.