• トップ
  • ブログ一覧
  • Rust/ActixWeb で データベース(SeaORM 利用)の準備
  • Rust/ActixWeb で データベース(SeaORM 利用)の準備

    はじめに

    Rust のお勉強、前々回/前回の続きです。
    今回はデータベースの準備がメインとなります。

    今回の動作環境

    毎度の前回からの動作環境変更点。

    バージョン(括弧内は前回)
    Rust1.81(1.78)
    actix-web4.9.0(4.5.1)

     

    データベース接続

    言わずもがな。
    少々乱暴な解釈かもですが、アプリ/API は総じてデータベースのラッパーで、なので CRUD 操作してなんぼなんですよね。

    actix の公式サイトでは Diesel というクレートを利用して DB 操作例が記載されてました。

    Databases | Actix Web

    なのでそれ使ってみよー、と思ったですが下記一文(英語)が目に止まりまして.

    The current versions of Diesel (v1/v2) does not support asynchronous operations, so it is important to use the web::block function to offload your database operations to the Actix runtime thread-pool.

    抜き出してみる。
    Diesel does not support asynchronous.

    訳してみる。
    Diesel は非同期操作をサポートしていない。

    oh.

    actix、非同期処理が売りな web フレームワークなのに。

    まぁはい。切り替えて非同期処理をサポートしてる DB ライブラリ(クレート)を探してみる。

    非同期処理をサポートしてる DB ライブラリ(クレート)

    調べましたら以下の 2 つがよく利用されている非同期対応の DB ライブラリ のようでした。

    sqlx
    • ☆: 12.8k(24/8末時点)
    • ORM ではない
    • 複数 RDBMS 対応
    • マイグレーション機能有り
    SeaORM
    • ☆: 6.9k(24/8末時点)
    • 上記 sqlx をベースに作られた ORM

    個人的に ORM は苦手でして。生成/実行される SQL の確認が面倒ですし、N+1 対応とか頭使うしなので。
    ですが今回は SeaORM を使ってみることにしました。

    決め手はエンティティの自動生成です(自動生成してくれるみたいです)。

    今回作る CRUD 処理の概要と準備

    何でも良いのですが、オラクルでお馴染みの dept/emp テーブル  を作成し、それの CRUD 処理を書いていきます。
    理由は特にないです。強いて言えば懐かしさ?

    その他使用する RDBMS などまとめ。

    • RDBMS
      • postgres(16.4)
    • テーブル
      • 2テーブル
        • dept(部署)
        • emp(社員)
      • リレーション
        • dept 1 : N emp
    • API
      • deptのCRUD処理
      • empのCRUD処理

    また準備として SeaORM のクレートを追加します。
    使用する RDBMS、非同期ランタイムによって features に指定する値が異なるようです。
    ( ドキュメント を参考に設定)

     

    1cargo add sea-orm --features "sqlx-postgres, runtime-tokio-native-tls macros"

     

    準備完了。ではやっていきます。

    マイグレーション定義

    まずはテーブルを作成します。マイグレーション 機能があるので利用します。

    ドキュメント - Migration

    バイナリパッケージが必要とのことでインストール。

    1cargo install sea-orm-cli
    2
    3# 確認
    4which sea-orm-cli
    5/usr/local/cargo/bin/sea-orm-cli
    6
    7see-orm-cli --version
    8sea-orm-cli 1.0.1

    続いて初期化。

    1sea-orm-cli migrate init
    2
    3# 結果
    4Initializing migration directory...
    5Creating file `./migration/src/lib.rs`
    6Creating file `./migration/src/m20220101_000001_create_table.rs`
    7Creating file `./migration/src/main.rs`
    8Creating file `./migration/Cargo.toml`
    9Creating file `./migration/README.md`
    10Done!

     

    自動生成された Cargo.toml(./migration/Cargo.toml) に下記を追加する。

    1[dependencies.sea-orm-migration]
    2version = "1.0.0"
    3features = [
    4  # Enable at least one `ASYNC_RUNTIME` and `DATABASE_DRIVER` feature if you want to run migration via CLI.
    5  # View the list of supported features at https://www.sea-ql.org/SeaORM/docs/install-and-config/database-and-async-runtime.
    6  # e.g.
    7  # "runtime-tokio-rustls",  # `ASYNC_RUNTIME` feature
    8  # "sqlx-postgres",         # `DATABASE_DRIVER` feature
    9  "runtime-tokio-native-tls", # ココと
    10  "sqlx-postgres",            # ココ追加
    11]

    自動生成されたマイグレーションファイルはサンプルなので、今回作成するテーブル用のファイルを作成します。

     

    1### deptテーブル
    2sea-orm-cli migrate generate dept_table
    3
    4# 結果
    5Generating new migration...
    6Creating migration file `./migration/src/m20240830_034748_dept_table.rs`
    7Adding migration `m20240830_034748_dept_table` to `./migration/src/lib.rs`
    8
    9
    10### empテーブル
    11sea-orm-cli migrate generate emp_table
    12
    13Generating new migration...
    14Creating migration file `./migration/src/m20240830_034805_emp_table.rs`
    15Adding migration `m20240830_034805_emp_table` to `./migration/src/lib.rs`

    マイグレーションファイルに定義を記載する。

    その前に最初に自動生成された m20220101_000001_create_table.rs は削除します。
    また、./migration/src/lib.rs に定義された m20220101_000001_create_table の記載を削除します。
    これらのせいでマイグレーション実行時エラーになりまして。邪魔なの。

    ドキュメント と チュートリアル を参考に定義してみる。

    dept

     

    1#[async_trait::async_trait]
    2impl MigrationTrait for Migration {
    3    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    4        // Replace the sample below with your own migration scripts
    5        // todo!();
    6
    7        manager
    8            .create_table(
    9                Table::create()
    10                    .table(Dept::Table)
    11                    .if_not_exists()
    12                    .col(ColumnDef::new(Dept::Deptno).integer().primary_key())
    13                    .col(ColumnDef::new(Dept::Dname).string().not_null())
    14                    .col(ColumnDef::new(Dept::Loc).string().not_null())
    15                    .to_owned(),
    16            )
    17            .await
    18    }
    19
    20    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    21        // Replace the sample below with your own migration scripts
    22        // todo!();
    23
    24        manager
    25            .drop_table(Table::drop().table(Dept::Table).to_owned())
    26            .await
    27    }
    28}
    29
    30#[derive(DeriveIden)]
    31pub enum Dept {
    32    Table,
    33    Deptno,
    34    Dname,
    35    Loc,
    36}

    emp

    1#[async_trait::async_trait]
    2impl MigrationTrait for Migration {
    3    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    4        // Replace the sample below with your own migration scripts
    5        // todo!();
    6
    7        manager
    8            .create_table(
    9                Table::create()
    10                    .table(Dept::Table)
    11                    .if_not_exists()
    12                    .col(ColumnDef::new(Dept::Deptno).integer().primary_key())
    13                    .col(ColumnDef::new(Dept::Dname).string().not_null())
    14                    .col(ColumnDef::new(Dept::Loc).string().not_null())
    15                    .to_owned(),
    16            )
    17            .await
    18    }
    19
    20    async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
    21        // Replace the sample below with your own migration scripts
    22        // todo!();
    23
    24        manager
    25            .drop_table(Table::drop().table(Dept::Table).to_owned())
    26            .await
    27    }
    28}
    29
    30#[derive(DeriveIden)]
    31pub enum Dept {
    32    Table,
    33    Deptno,
    34    Dname,
    35    Loc,
    36}

    enum に項目を定義し、それを利用して項目の型や制約の定義をするようです。

    では実行します。

    マイグレーション実行

     

    1sea-orm-cli migrate -u postgres://(接続文字列)
    2
    3# 結果
    4Applying all pending migrations
    5Applying migration 'm20240830_034748_dept_table'
    6Migration 'm20240830_034748_dept_table' has been applied
    7Applying migration 'm20240830_034805_emp_table'
    8Migration 'm20240830_034805_emp_table' has been applied

    実行終了。きちんと作成されたか確認します。

    データベース確認(psql コマンド)

     

    1 # \dt;
    2             List of relations
    3 Schema |       Name       | Type  | Owner
    4--------+------------------+-------+--------
    5 public | dept             | table | xxuser
    6 public | emp              | table | xxuser
    7 public | seaql_migrations | table | xxuser
    8(3 rows)

    できる子。seaql_migrations はバージョン管理ですね。

    続いてテーブル定義確認。

     

    1# \d dept;
    2                     Table "public.dept"
    3 Column |       Type        | Collation | Nullable | Default
    4--------+-------------------+-----------+----------+---------
    5 deptno | integer           |           | not null |
    6 dname  | character varying |           | not null |
    7 loc    | character varying |           | not null |
    8Indexes:
    9    "dept_pkey" PRIMARY KEY, btree (deptno)
    10Referenced by:
    11    TABLE "emp" CONSTRAINT "fk_deptno" FOREIGN KEY (deptno) REFERENCES dept(deptno) ON UPDATE CASCADE ON DELETE CASCADE
    12
    13
    14# \d emp;
    15                           Table "public.emp"
    16  Column  |            Type             | Collation | Nullable | Default
    17----------+-----------------------------+-----------+----------+---------
    18 empno    | integer                     |           | not null |
    19 ename    | character varying           |           |          |
    20 job      | character varying           |           |          |
    21 mgr      | integer                     |           |          |
    22 hiredate | timestamp without time zone |           |          |
    23 sal      | integer                     |           |          |
    24 comm     | integer                     |           |          |
    25 deptno   | integer                     |           |          |
    26Indexes:
    27    "emp_pkey" PRIMARY KEY, btree (empno)
    28Foreign-key constraints:
    29    "fk_deptno" FOREIGN KEY (deptno) REFERENCES dept(deptno) ON UPDATE CASCADE ON DELETE CASCADE

    うーん、何か一部データ型が思ってたのと違う。キー値にシーケンス設定されてない。emp テーブルの項目に not null 制約定義忘れてる。

    さささっとやりすぎました、反省。

    もっとよく調べて定義してみます。

    データ型の定義メソッドは こちら を参照したら大分見当をつけることができました。
    この手の情報はドキュメントに詳細にまとめておいて欲しいですよね、と思いました。
    (とか言っといて実はドキュメントがあったケースは多々ありまして。すみません。先に謝ってみる)

    修正後

    dept

     

    1        manager
    2            .create_table(
    3                Table::create()
    4                    .table(Dept::Table)
    5                    .if_not_exists()
    6                    .col(
    7                        ColumnDef::new(Dept::Deptno)
    8                            .integer()
    9                            .auto_increment()
    10                            .primary_key(),
    11                    )
    12                    .col(ColumnDef::new(Dept::Dname).string_len(14).not_null())
    13                    .col(ColumnDef::new(Dept::Loc).string_len(13).not_null())
    14                    .to_owned(),
    15            )
    16            .await

    emp

    1        manager
    2            .create_table(
    3                Table::create()
    4                    .table(Emp::Table)
    5                    .if_not_exists()
    6                    .col(
    7                        ColumnDef::new(Emp::Empno)
    8                            .integer()
    9                            .auto_increment()
    10                            .primary_key(),
    11                    )
    12                    .col(ColumnDef::new(Emp::Ename).string_len(10).not_null())
    13                    .col(ColumnDef::new(Emp::Job).string_len(9).not_null())
    14                    .col(ColumnDef::new(Emp::Mgr).integer())
    15                    .col(ColumnDef::new(Emp::Hiredate).date_time().not_null())
    16                    .col(ColumnDef::new(Emp::Sal).decimal_len(7, 2).not_null())
    17                    .col(ColumnDef::new(Emp::Comm).decimal_len(7, 2))
    18                    .col(ColumnDef::new(Emp::Deptno).integer().not_null())
    19                    .foreign_key(
    20                        ForeignKey::create()
    21                            .name("fk_deptno")
    22                            .from(Emp::Table, Emp::Deptno)
    23                            .to(Dept::Table, Dept::Deptno)
    24                            .on_delete(ForeignKeyAction::Cascade)
    25                            .on_update(ForeignKeyAction::Cascade),
    26                    )
    27                    .to_owned(),
    28            )
    29            .await

    マイグレーション再実行(リフレッシュ)する。

    1sea-orm-cli migrate refresh -u postgres://(接続文字列)
    2
    3# 結果
    4Rolling back all applied migrations
    5Rolling back migration 'm20240830_034805_emp_table'
    6Migration 'm20240830_034805_emp_table' has been rollbacked
    7Rolling back migration 'm20240830_034748_dept_table'
    8Migration 'm20240830_034748_dept_table' has been rollbacked
    9Applying all pending migrations
    10Applying migration 'm20240830_034748_dept_table'
    11Migration 'm20240830_034748_dept_table' has been applied
    12Applying migration 'm20240830_034805_emp_table'
    13Migration 'm20240830_034805_emp_table' has been applied

    テーブル定義はどうだろう?

    1# \d dept;
    2                                     Table "public.dept"
    3 Column |         Type          | Collation | Nullable |               Default
    4--------+-----------------------+-----------+----------+--------------------------------------
    5 deptno | integer               |           | not null | nextval('dept_deptno_seq'::regclass)
    6 dname  | character varying(14) |           | not null |
    7 loc    | character varying(13) |           | not null |
    8Indexes:
    9    "dept_pkey" PRIMARY KEY, btree (deptno)
    10Referenced by:
    11    TABLE "emp" CONSTRAINT "fk_deptno" FOREIGN KEY (deptno) REFERENCES dept(deptno) ON UPDATE CASCADE ON DELETE CASCADE
    12
    13# \d emp;
    14                                         Table "public.emp"
    15  Column  |            Type             | Collation | Nullable |              Default
    16----------+-----------------------------+-----------+----------+------------------------------------
    17 empno    | integer                     |           | not null | nextval('emp_empno_seq'::regclass)
    18 ename    | character varying(10)       |           | not null |
    19 job      | character varying(9)        |           | not null |
    20 mgr      | integer                     |           |          |
    21 hiredate | timestamp without time zone |           | not null |
    22 sal      | numeric(7,2)                |           | not null |
    23 comm     | numeric(7,2)                |           |          |
    24 deptno   | integer                     |           | not null |
    25Indexes:
    26    "emp_pkey" PRIMARY KEY, btree (empno)
    27Foreign-key constraints:
    28    "fk_deptno" FOREIGN KEY (deptno) REFERENCES dept(deptno) ON UPDATE CASCADE ON DELETE CASCADE

    良い感じ!

    ドキュメント 読んでましたら DDL 文の直定義もできるようでした。
    なので例えば postgres で PostGIS を使う場合の拡張型を DDL 直記載するで対応できますね。

    エンティティ作成

    テーブルできましたので、エンティティを生成してみます。
    src ディレクトリ配下に entities ディレクトリを作成し、その中にエンティティを配置するよう指定します。
    下記コマンドで作成したテーブルから生成させることができるみたいです。便利。

    1sea-orm-cli generate entity \
    2 -u postgres://(接続文字列) \
    3 -o src/entities
    4
    5# 結果
    6Connecting to Postgres ...
    7Discovering schema ...
    8... discovered.
    9Generating dept.rs
    10    > Column `deptno`: i32, auto_increment, not_null
    11    > Column `dname`: String, not_null
    12    > Column `loc`: String, not_null
    13Generating emp.rs
    14    > Column `empno`: i32, auto_increment, not_null
    15    > Column `ename`: String, not_null
    16    > Column `job`: String, not_null
    17    > Column `mgr`: Option
    18    > Column `hiredate`: DateTime, not_null
    19    > Column `sal`: Decimal, not_null
    20    > Column `comm`: Option
    21    > Column `deptno`: i32, not_null
    22Writing src/entities/dept.rs
    23Writing src/entities/emp.rs
    24Writing src/entities/mod.rs
    25Writing src/entities/prelude.rs
    26... Done.

    瞬で実行終わりました。

    1% tree src/entities
    2src/entities
    3├── dept.rs
    4├── emp.rs
    5├── mod.rs
    6└── prelude.rs

    できてる。

    生成したエンティティの中身。

    dept

    1//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
    2
    3use sea_orm::entity::prelude::*;
    4
    5#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
    6#[sea_orm(table_name = "dept")]
    7pub struct Model {
    8    #[sea_orm(primary_key)]
    9    pub deptno: i32,
    10    pub dname: String,
    11    pub loc: String,
    12}
    13
    14#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
    15pub enum Relation {
    16    #[sea_orm(has_many = "super::emp::Entity")]
    17    Emp,
    18}
    19
    20impl Related for Entity {
    21    fn to() -> RelationDef {
    22        Relation::Emp.def()
    23    }
    24}
    25
    26impl ActiveModelBehavior for ActiveModel {}

    emp

    1//! `SeaORM` Entity, @generated by sea-orm-codegen 1.0.1
    2
    3use sea_orm::entity::prelude::*;
    4
    5#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
    6#[sea_orm(table_name = "dept")]
    7pub struct Model {
    8    #[sea_orm(primary_key)]
    9    pub deptno: i32,
    10    pub dname: String,
    11    pub loc: String,
    12}
    13
    14#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
    15pub enum Relation {
    16    #[sea_orm(has_many = "super::emp::Entity")]
    17    Emp,
    18}
    19
    20impl Related for Entity {
    21    fn to() -> RelationDef {
    22        Relation::Emp.def()
    23    }
    24}
    25
    26impl ActiveModelBehavior for ActiveModel {}

    素晴らしいですね。この自動生成されたエンティティを利用して DB とやりとりするのでしょうね。多分。

     

    さ CRUD 処理しますか。
    と思ったですが、マイグレーションだけで長くなっちゃいました。
    疲れました。
    CRUD 処理については次回書きますね。

    まとめ

    Rust の DB 関連ライブラリは非同期処理対応かを確認すること。
    しかし今回はほぼコード書かなかったですね。
    最近多いですよね自動生成。
    いや結構前からあるですね自動生成。

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background