SeaORMスターターキットを作りました

# 目的
SeaORMの情報が少ないのと、どんな感じに動くのか雰囲気を知りたい人は多いのではと思って、スターターキットを作成しました。

今回は、SeaORMを動かしてみるということに特化した設計のため、クリーンアーキテクチャは無視しています。まずは「こいつ、動くぞ。。。！」を体感してもらえると幸いです。さあ、サイド7からスタートしましょう。


# 環境
Windows11
Ubuntu -> Ubuntuの場合を参照
Docker

# リポジトリ
https://github.com/Ometeor-Zheero-OMZ/sea-orm-starter

# 手順
## まずは SeaORM を動かしてみよう
### ローカルにクローンする。
```bash
git clone git@github.com:Ometeor-Zheero-OMZ/sea-orm-starter.git
```

***

### .env.exampleから.envを作成する
```
cp .env.example .env
```

***

### SeaORMのCLIをインストール
```bash
cargo install sea-orm-cli
```

***

### コンテナを作成
```bash
docker compose up -d
```

```bash
make start
```

一応 Makefile で各コマンドを作成しているので、お好みで使用してください。

***

### マイグレーション実行
Dockerfileにmigrationをコピーするように記述していますので、
コンテナ内でマイグレーションします。

```
docker exec -it backend sea-orm-cli migrate up -u postgres://postgres:postgres@db/sea-orm-starter
```

```bash
make migrate-up
```

10秒ほどビルドされた後に、
下記のようになればOK

```bash
Applying all pending migrations
Applying migration 'm20241231_132216_create_user_table'
Migration 'm20241231_132216_create_user_table' has been applied
```

***

`client.http` で簡単に APIテストができるようにしていますので、さっそく確認してみましょう。

![before-create-user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2701698/e465b1e4-a2ca-ca2e-ed76-4bbe8290a9b3.png)

まずは、ユーザー取得。
マイグレーションした直後のため、userテーブルにレコードは入っていません。

![create-user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2701698/8df45fa0-5b9f-df3b-4d4a-4c5fbdaaf242.png)

ユーザー登録でレコードを挿入することができます。

![after-create-user.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2701698/8154fb7f-5588-437d-a10f-186d47a9d722.png)

再度、ユーザー取得をすることで、追加したユーザー情報を取得することができます。

## 新しくSeaORMの環境を作るにあたっての重要事項

### マイグレーションの初期化（スターターキットでは実行済み）
今回のスターターキットではすでに初期化済みですが、新規プロジェクトでは、マイグレーションの初期化をする必要があります。cargo new projectの後に実行しておくといいでしょう。

```bash
sea-orm-cli migrate init
```

```bash
make migrate-init
```

***

### マイグレーションファイルの作成

table_name は作成するテーブル名を記述する必要があります。
```bash
sea-orm-cli migrate generate create_<table_name>_table
```

例：user テーブルを作成
```bash
sea-orm-cli migrate generate create_user_table
```

例：make コマンドで user テーブルを作成
```bash
make gen table_name=user
```

以下のようになればOK
```bash
$ make gen table_name=user
sea-orm-cli migrate generate create_user_table
Generating new migration...
Creating migration file `./migration\src\m20241231_220934_create_user_table.rs`
Adding migration `m20241231_220934_create_user_table` to `./migration\src\lib.rs`
```

これにより、migrationディレクトリがRustの新規プロジェクトとして `src` と同階層に生成されます。
- migration/src
  - main.rs
  - m20241231_132216_create_user_table.rs
  - lib.rs
- migration/Cargo.lock
- migration/Cargo.toml
- migration/README.md

***

**main.rs**
```rust
use sea_orm_migration::prelude::*;

#[async_std::main]
async fn main() {
    cli::run_cli(migration::Migrator).await;
}
```
以降、main.rsは編集しなくとも、マイグレーションファイルの生成とリンクはされますので、構造だけ確認しておきましょう。

***

**m20241231_132216_create_user_table.rs**
```rust
use sea_orm_migration::prelude::*;

#[derive(DeriveMigrationName)]
pub struct Migration;

#[async_trait::async_trait]
impl MigrationTrait for Migration {
    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager
            .create_table(
                Table::create()
                    .table(User::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(User::UserId)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(User::CognitoId).string().not_null())
                    .col(ColumnDef::new(User::Username).string().not_null())
                    .col(ColumnDef::new(User::ProfilePictureUrl).string())
                    .col(ColumnDef::new(User::TeamId).integer())
                    .to_owned(),
            )
            .await?;

        manager
            .create_index(
                Index::create()
                    .name("idx_user_cognito_id")
                    .table(User::Table)
                    .col(User::CognitoId)
                    .unique()
                    .to_owned(),
            )
            .await?;

        manager
            .create_index(
                Index::create()
                    .name("idx_user_username")
                    .table(User::Table)
                    .col(User::Username)
                    .unique()
                    .to_owned(),
            )
            .await?;

        Ok(())
    }

    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
        manager.drop_table(Table::drop().table(User::Table).to_owned()).await
    }
}

#[derive(Iden)]
pub enum User {
    Table,
    UserId,
    CognitoId,
    Username,
    ProfilePictureUrl,
    TeamId,
}
```

これは、マイグレーションファイルの生成後にテーブル定義を書いたものです。

```rust
#[derive(Iden)]
pub enum User {
    Table,
    UserId,
    CognitoId,
    Username,
    ProfilePictureUrl,
    TeamId,
}
```
これはテーブルのフィールド決定するものです。`enum` で定義します。
ここで重要なのは、`Iden` トレイトを `derive` することです。

```rust
        manager
            .create_table(
                Table::create()
                    .table(User::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(User::UserId)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(User::CognitoId).string().not_null())
                    .col(ColumnDef::new(User::Username).string().not_null())
                    .col(ColumnDef::new(User::ProfilePictureUrl).string())
                    .col(ColumnDef::new(User::TeamId).integer())
                    .to_owned(),
            )
            .await?;
```

Laravelなどのマイグレーションに慣れている方であれば、難しくはないはずです。
重要なのは、`.table(User::Table)` で記述する `User` は先ほど定義した `enum` の `User` ですので、他モジュールの `User` 構造体や `enum` を記述しないようにしましょう。

`manager`: テーブルを作成するためのインスタンス
`.create_table()`: 新規テーブルを作成 - テーブル情報をラップします。
`Table::create()`: 新規テーブルを作成 - メソッドチェーンでテーブル情報を追加していく。
`.table(User::Table)`: `enum` で定義した `User` に関するテーブルを指定する。
`.if_not_exists()`: 同名テーブルがない場合にテーブルを作成する。
`.col(ColumnDef::new())`: フィールドを定義する。

メソッドチェーンで、全てのフィールドを定義していけば、`.to_owned()` 所有型に変換します。データを所有権で管理することで、SeaORMの最大の特徴である **非同期処理** が実現します。データオブジェクトが複数スレッドでアクセスされたり、バックグラウンドタスクで使用されることを考慮して、所有型である必要があります。

さらに、`enum` で `User` を定義する理由は、恐らくですがコンパイラがエラーを吐き続けることによって、フィールド定義に漏れが出ないように強制してくれるように設計されたと、私は思っています。

***
テーブル作成時のメソッドは公式ドキュメントを見て書いていただければと思います。
[(SeaORM Docs) Create Table](https://www.sea-ql.org/SeaORM/docs/0.4.x/generate-database-schema/create-table/)

***

**lib.rs**
```rust
pub use sea_orm_migration::prelude::*;

mod m20241231_132216_create_user_table;

pub struct Migrator;

#[async_trait::async_trait]
impl MigratorTrait for Migrator {
    fn migrations() -> Vec<Box<dyn MigrationTrait>> {
        vec![
            Box::new(m20241231_132216_create_user_table::Migration),
        ]
    }
}
```

こちらも特に手動で手を加えることは少ないと思いますが、`sea-orm-cli` でマイグレーションファイルを作成した際に、自動でマイグレーションファイルを `mod` し、`vec!` 内に、ボックス化したマイグレーションファイルの`Migration` トレイトが自動で指定されます。マイグレーションファイルを生成する毎に、それぞれ異なるスキーマの型として生成されるため、トレイトオブジェクトで扱うようになっています。

***

### エンティティ生成 (スターターキットでは生成済み)
既存のデータベースからテーブル情報を基にsrc/entities/内でエンティティを生成します。

```bash
sea-orm-cli generate entity -u postgres://postgres:postgres@localhost:5432/sea-orm-starter -o src/entities
```

```bash
make gen-entity
```

これにより、以下のファイルが生成されます。

- src/entities/prelude.rs
- src/entities/mod.rs
- src/entities/user.rs

***

**src/entities/prelude.rs**
```rust
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.3

pub use super::user::Entity as User;
```

※エンティティ生成毎に上書きされます。マイグレーションとは異なり、破壊的に更新されるため注意です。といっても、モジュールを公開しているだけなので、そこまで痛くない。

***

**src/entities/mod.rs**
```rust
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.3

pub mod prelude;

pub mod user;
```

※エンティティ生成毎に上書きされます。マイグレーションとは異なり、破壊的に更新されるため注意です。といっても、モジュールを公開しているだけなので、そこまで痛くない。

***

**src/entities/user.rs**
```rust
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.3

use sea_orm::entity::prelude::*;

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "user")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub user_id: i32,
    #[sea_orm(unique)]
    pub cognito_id: String,
    #[sea_orm(unique)]
    pub username: String,
    pub profile_picture_url: Option<String>,
    pub team_id: Option<i32>,
}

#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
pub enum Relation {}

impl ActiveModelBehavior for ActiveModel {}
```

既存テーブル情報をそのままエンティティとして生成できます。

※エンティティ生成毎に上書きされます。マイグレーションとは異なり、破壊的に更新されるため注意です。このファイルに関しては、さすがに痛いので特に注意です。

### Ubuntuの場合
手順は全く同じですが、もしかすると sea-orm-cli のインストールに失敗するかもしれません。

大抵の問題は OpenSSLのライブラリ群がインストールされていなかったり、環境パスが設定されていない場合に起こります。

インストールに失敗した場合は下記の手順を試してみてください：
#### パッケージ・ライブラリをインストール

```bash
sudo apt update
```

```bash
sudo apt install -y pkg-config libssl-dev build-essential
```

#### OpenSSL をインストール

```bash
sudo apt install -y openssl
```

#### 環境変数を設定
```bash
export OPENSSL_LIB_DIR=/usr/lib/x86_64-linux-gnu
```

```bash
export OPENSSL_INCLUDE_DIR=/usr/include/openssl
```

```bash
export OPENSSL_STATIC=false
```

#### sea-orm-cli をインストール

```bash
cargo install sea-orm-cli --locked
```

# 終わりに
今回は SeaORMをさっさと使いたい方または、新規プロジェクトでの参考にされたい方に向けて記事を書きました。

記事作成には慣れていないため、読みづらい箇所があるかと思いますが、最後までお読みいただき大変嬉しく思います。

次回は AxumとNext.jsでフルスタック開発スターターキットを公開しようかなと思います。
気になる方は**いいね**をいただけるとやる気が出ます。

また、Rustでゲーム開発もしてますので、ある程度いいものが作れたらみんなに見てもらいたいな🐮🤚