SeaORMでActiveEnumを使用する方法

RustにはSeaORMというとても使いやすいORMがあります。
SeaORMのDocsは非常に読みやすく、Rustにある程度慣れている人であれば容易に使用することができると思います。しかし、ActiveEnumの説明箇所が個人的にわかりにくく(私の読解力がないだけです。)、かなりの時間を浪費してしまったので、備忘録として書き記します。

# ActiveEnumの使い方

早速使い方を書いていきます。

## この記事の内容のレポジトリ

プログラムを直接読みたい方は、この記事の内容のレポジトリを用意しているので、以下のリンクからご覧ください。

https://github.com/Myxogastria0808/SeaORMActiveEnumSample

## 環境

OS: Zorin OS 17.1 x86_64
DB: PostgreSQL (Dockerを使用)
rustc: 1.80.1
cargo: 1.80.1

## sea-orm-cliの導入
```sh
cargo install sea-orm-cli
```

## projectの作成

今回は、sampleというプロジェクト名で作成します。

```sh
cargo new sample
cd sample
```

## docker-compose.yamlを用意

今回はPostgreSQLを使用します。

```docker-compose.yaml
version: "3.8"

services:
  postgres:
    container_name: sample_db
    image: postgres:latest
    volumes:
      - ./db:/var/lib/postgresql
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: test_db
    ports:
      - "5433:5432"
```

## SeaORMを導入

:::note warn
SeaORMのバージョンは、1.0.0にしてください。
:::

```sh
cargo add sea-orm@=1.0.0
```

## migrationフォルダを初期化

```sh
sea-orm-cli migrate init
```

project内のディレクトリ構造が以下のようになっていれば、成功です。

```sh
$ tree
.
├── Cargo.lock
├── Cargo.toml
├── docker-compose.yaml
├── migration
│   ├── Cargo.toml
│   ├── README.md
│   └── src
│       ├── lib.rs
│       ├── m20220101_000001_create_table.rs
│       └── main.rs
├── src
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
(以下略)
```

## テーブルの定義

今回は以下のようなテーブルを作成します。
Category columnでActiveEnumを使用します。

```mermaid
erDiagram
  Post {
    i32 Id PK "autoincrement"
    Strng Title
    String Content
    Category Category "enum { NixOS, ZorinOS, ArchLinux, Windows }"
    
  }
```

**ActiveEnumを使用するCategory columnは、Stringとして定義します。**

```rust
// migration/src/m20220101_000001_create_table.rs
use sea_orm_migration::{prelude::*, schema::*};

#[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(Post::Table)
                    .if_not_exists()
                    .col(
                        ColumnDef::new(Post::Id)
                            .integer()
                            .not_null()
                            .auto_increment()
                            .primary_key(),
                    )
                    .col(ColumnDef::new(Post::Title).string().not_null())
                    .col(ColumnDef::new(Post::Content).string().not_null())
                    .col(ColumnDef::new(Post::Category).string().not_null())
                    .to_owned(),
            )
            .await
    }

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

#[derive(DeriveIden)]
enum Post {
    Table,
    Id,
    Title,
    Content,
    Category,
}

```

`migration/Cargo.toml`の`features`を以下のように編集します。

```migration/Cargo.toml
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
name = "migration"
path = "src/lib.rs"

[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }

[dependencies.sea-orm-migration]
version = "1.0.0"
features = [
  "runtime-tokio-native-tls",  # `ASYNC_RUNTIME` feature
  "sqlx-postgres",         # `DATABASE_DRIVER` feature
]
```

## docker-compose up -d

コンテナを起動します。

```sh
docker-compose up -d
```

## Migrationの実行

以下のコマンドでMigrationを実行します。

```sh
DATABASE_URL="postgres://postgres:postgres@localhost:5433/test_db" sea-orm-cli migrate refresh
```

## Entityの生成

以下のコマンドでEntityを生成します。

```sh
sea-orm-cli generate entity \
    -u postgres://postgres:postgres@localhost:5433/test_db \
    -o entity/src
```

# Cargo.tomlの編集

`entity/Cargo.toml`を作成し、以下のように編集します。

```entity/Cargo.toml
[package]
name = "entity"
version = "0.1.0"
edition = "2021"
publish = false

[lib]
name = "entity"
path = "src/mod.rs"

[dependencies.sea-orm]
version = "1.0.0"

```

プロジェクトの直下にある`Cargo.toml`を以下の内容に編集します。

```Cargo.toml
[package]
name = "sample"
version = "0.1.0"
edition = "2021"

[workspace]
members = [".", "entity", "migration"]

[dependencies]
entity = { path = "entity" }
migration = { path = "migration" }
sea-orm = { version = "=1.0.0", features = [ "sqlx-postgres", "runtime-tokio-native-tls", "macros" ] }

```
## enumの定義

`entity/src/post.rs`にCategoryのenumを定義します。
enum名は、`CategoryEnum`にしています。Model構造体のcategoryを`String`から`CategoryEnum`に変更することを忘れないようにしましょう。

```rust
// entity/src/post.rs
//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.0

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

#[derive(Debug, Clone, PartialEq, Eq, EnumIter, DeriveActiveEnum)]
#[sea_orm(rs_type = "String", db_type = "String(StringLen::None)")]
pub enum CategoryEnum {
    #[sea_orm(string_value = "NixOS")]
    NixOS,
    #[sea_orm(string_value = "ZorinOS")]
    ZorinOS,
    #[sea_orm(string_value = "ArchLinux")]
    ArchLinux,
    #[sea_orm(string_value = "Windows")]
    Windows,
}

#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
#[sea_orm(table_name = "post")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub title: String,
    pub content: String,
    pub category: CategoryEnum,
}

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

impl ActiveModelBehavior for ActiveModel {}

```

これで、Rust側では`category`は`CategoryEnum`のみを受け入れるようになりました！

## CRUD操作

tokioを導入します。

```sh
cargo add tokio -F full
```
雑なプログラムですが、CRUD操作も書き記しておきます。

```rust
// src/main.rs
use ::entity::post::{self, CategoryEnum, Entity as Post};
use sea_orm::{
    ActiveModelTrait, ColumnTrait, Database, DatabaseConnection, EntityTrait, ModelTrait,
    QueryFilter, Set,
};

#[tokio::main(flavor = "current_thread")]
async fn main() {
    // connnect database
    let db: DatabaseConnection =
        Database::connect("postgres://postgres:postgres@localhost:5433/test_db")
            .await
            .expect("Failed to connect to db");
    //insert
    let post_model: post::ActiveModel = post::ActiveModel {
        title: Set("Title".to_string()),
        content: Set("Content".to_string()),
        category: Set(CategoryEnum::NixOS),
        ..Default::default()
    };
    let post: sea_orm::InsertResult<post::ActiveModel> =
        Post::insert(post_model).exec(&db).await.unwrap();
    println!("Inserted: {:?}", post);
    //find
    let post_data: Option<post::Model> = Post::find()
        .filter(post::Column::Category.eq(CategoryEnum::NixOS))
        .one(&db)
        .await
        .unwrap();
    println!("Found post data\n{:#?}", post_data);
    //update
    match post_data {
        Some(post_data) => {
            let mut update_post_data: post::ActiveModel = post_data.into();
            update_post_data.title = Set("Updated Title".to_string());
            update_post_data.content = Set("Updated Content".to_string());
            update_post_data.category = Set(CategoryEnum::Windows);
            let updated_post: post::Model = update_post_data.update(&db).await.unwrap();
            println!("Updated post data\n{:#?}", updated_post);
        }
        None => {
            println!("No post data found");
        }
    }
    //delete
    let delete_post_data: Option<post::Model> = Post::find()
        .filter(post::Column::Category.eq(CategoryEnum::Windows))
        .one(&db)
        .await
        .unwrap();
    match delete_post_data {
        Some(delete_post_data) => {
            let deleted_post: sea_orm::DeleteResult = delete_post_data.delete(&db).await.unwrap();
            println!("Deleted post data\n{:?}", deleted_post);
        }
        None => {
            println!("No post data found");
        }
    }
}

```

## 参考サイト

https://www.sea-ql.org/SeaORM/docs/generate-entity/enumeration/

https://www.sea-ql.org/sea-orm-cookbook/005-selct-row-matching-any-enum-values.html

https://docs.rs/sea-orm/latest/sea_orm/entity/trait.ActiveEnum.html

https://www.sea-ql.org/sea-orm-tutorial/ch01-00-build-backend-getting-started.html