こんにちは、ナナオです。

前回、KoyebでDiscord botを構築してみました。

今回はこれに無料で使えると話題のNeonを組み合わせてみようと思います。

Neonとは

Neonはサーバーレスで動くPostgresのサービスです。

Neon Serverless Postgres — Ship faster

DBのブランチ戦略やオートスケーリングに長けており、なにより無料で使えるプランがあるのが個人開発者には魅力的な点です。

アカウント登録 ~ プロジェクト作成

まずはNeonにサインアップします。

サインアップしたらプロジェクトの作成画面に遷移します。

プロジェクト名を入力し、リージョンは日本がないので、とりあえずデフォルトのN. Virginiaにしておきます。

プロジェクトが作成されました。

NeonのDBに接続してみます。

Webコンソール上からアクセスしてみましょう。

左側のタブからTablesを選択します。

neondbというDBがあるのと、publicスキーマがあるだけで、テーブルは何もありません。

マイグレーション管理(alembic)の導入

discord botの実装にはpythonを使用しているので、マイグレーションにもpythonを使用します。

まずはプロジェクトにSQLAlchemyとalembicを導入します。

uv add SQLAlchemy alembic psycopg2

alembicでマイグレーション用のディレクトリを作成します。

uv run alembic init migrations

作成されたalembic.iniのデータベースURLの設定を、環境変数を参照するように書き換えます。

# database URL.  This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = %(DATABASE_URL)s

.envにはDATABASE_URL環境変数を設定します。

NeonのプロジェクトダッシュボードからConnectをクリックし、表示されたconnection stringをコピーします。

env.pyの冒頭を以下のように修正します。

import os
from logging.config import fileConfig

from alembic import context
from dotenv import load_dotenv
from sqlalchemy import engine_from_config, pool

load_dotenv()

# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config

# この行を追加!
config.set_section_option("alembic", "DATABASE_URL", os.environ["DATABASE_URL"])

# ...中略...

マイグレーションの土台は完成しました。

モデルを実装していきます。

(ちなみに使っているSQLAlchemyは2.0です)

以下のようにアプリケーションのディレクトリを構築します。

└── src
    └── mokumoku_bot
        ├── db
        │   └── __init__.py
        │   └── base.py
        └── model
            └── history.py

base.pyの実装は以下の通り。

from sqlalchemy.orm import DeclarativeBase

class Base(DeclarativeBase):
    ...

モデルの実装は公式のQuickStartを参考に実装します。

ORM Quick Start — SQLAlchemy 2.0 Documentation

今回はbotの特定コマンドを実行したユーザーと日時を持つテーブルを作成します。

import datetime as dt
from typing import Literal

from sqlalchemy import String
from sqlalchemy.orm import Mapped, mapped_column

from mokumoku_bot.db.base import Base


class History(Base):
    __tablename__ = "history"

    user_id: Mapped[str] = mapped_column(String(19), primary_key=True)
    user_name: Mapped[str] = mapped_column(nullable=False)
    cmd: Mapped[Literal["start", "end"]] = mapped_column(primary_key=True)
    created_at: Mapped[dt.datetime] = mapped_column(primary_key=True)

db/__init__.pyでHistoryテーブルとBaseクラスを参照するようにしておきます。

ここでHisotryテーブルを参照するようになっていないとマイグレーションファイルの生成時にHistoryテーブルが作成されません。

from mokumoku_bot.db.base import Base
from mokumoku_bot.model.history import History

実装出来たら、env.pyを以下のように編集します。

from mokumoku_bot.db import Base

# ...中略...

# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
target_metadata = Base.metadata

これでマイグレーションファイルを作成します。

uv run alembic revision --autogenerate -m "create initial table"

以下のようなマイグレーションファイルが生成されます。

興味深いのはcmdのリテラルがenumとして生成されている部分ですね。

ちゃんとリテラルも考慮して実装されているんですね。すごい。

"""create initial table

Revision ID: f9372abd0049
Revises: 
Create Date: 2026-01-26 13:21:28.739946

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision: str = 'f9372abd0049'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
    """Upgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.create_table('history',
    sa.Column('user_id', sa.String(length=19), nullable=False),
    sa.Column('user_name', sa.String(), nullable=False),
    sa.Column('cmd', sa.Enum('start', 'end', native_enum=False), nullable=False),
    sa.Column('created_at', sa.DateTime(), nullable=False),
    sa.PrimaryKeyConstraint('user_id', 'cmd', 'created_at')
    )
    # ### end Alembic commands ###


def downgrade() -> None:
    """Downgrade schema."""
    # ### commands auto generated by Alembic - please adjust! ###
    op.drop_table('history')
    # ### end Alembic commands ###

マイグレーションの適用

先ほどのマイグレーションファイルを適用しましょう。

❯ uv run alembic upgrade head
INFO  [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO  [alembic.runtime.migration] Will assume transactional DDL.
INFO  [alembic.runtime.migration] Running upgrade  -> f9372abd0049, create initial table

無事適用できたようです。

ダッシュボードを確認してみます。

マイグレーションの履歴テーブルと、先ほど作成した履歴テーブルが作成されていますね!

感想

Neonに関してはログインしてプロジェクト作ったら即使えたので、alembicの解説の方が長くなってしまいました。

それだけ手間が少なく使えるわけですから、すごいサービスだなぁと思います。

参考

https://zenn.dev/shimakaze_soft/articles/4c0784d9a87751

alembicのmigrationでDB情報を環境変数から取得できるようにする備忘録 #Python - Qiita