RustのORM SeaORMを使ってみる

RustのSeaORMというORMを使ってSQLiteにレコード追加したりしてみる
軽く調べてみたところdieselが安パイ的な記事を見かけたものの若干情報が古い&非同期処理対応してないっぽいので今回は見送った

クレートのインストール

Cargo.tomlを編集する
SQLiteを使う場合は↓のようになる(詳細はこちらのドキュメントを参照)

[dependencies]
sea-orm = { version = "^0", features = [ "sqlx-sqlite", "runtime-tokio-native-tls", "macros" ] }
tokio = {version = "1.27.0", features = ["full"]} # 必須じゃないけど非同期処理使いたいので入れておく

DBにコネクションを貼る

SeaORMを使ってSQLiteのDBにコネクションを貼ってみる

use sea_orm::Database;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;

    Ok(())
}

sqlite://sample.db?mode=rwcSQLiteのDBファイルのパスとモードを指定している
mode=rwcというクエリパラメータでレコードの読み書きを許可し、さらにsample.dbファイルが存在しない場合は新規作成することを表している
※このURLの仕様についてはこちらを参照

Entityとマッピング

ORM特有のDBのレコードとプログラムのオブジェクトを結びつける儀式を行う

Entityの実装

src/entities/users.rsusersテーブルの定義を書いてみよう

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

#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)]
#[sea_orm(table_name = "users")]
pub struct Model {
    #[sea_orm(primary_key)]
    pub id: i32,
    pub name: String,
    pub age: i32,
}

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

impl ActiveModelBehavior for ActiveModel {}

そうしたら、main関数でDBにusersテーブルがなければsrc/entities/users.rsの定義に従って作成するようにする

use sea_orm::*;
mod entities;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    Ok(())
}

実装し終わったらcargo runで実行しsample.dbにusersテーブルができているか確認
SQLiteのファイルはVSCodeSQLite Viewerという拡張機能で閲覧している

CRUD操作

Create

レコードの新規作成処理を実装する

use sea_orm::*;
mod entities;
use entities::users::ActiveModel as UserModel;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    create(&db).await?;

    Ok(())
}

async fn create(db: &DatabaseConnection) -> Result<(), Box<dyn std::error::Error>> {
    UserModel {
        name: Set("hoge".to_owned()),
        age: Set(20),
        ..Default::default()
    }
    .save(db)
    .await?;

    Ok(())
}

EntityのActiveModelsave()メソッドが生えているのでこれを使う
上記のコードを実行すると期待通りのレコードが追加されている

Read

今度はレコードを一覧取得する

use entities::users::Model;
use sea_orm::*;
mod entities;
use entities::users::ActiveModel as UserModel;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    create(&db).await?;

    let users = find_all(&db).await?;
    println!("users: {}", users.len());

    Ok(())
}

async fn create(db: &DatabaseConnection) -> Result<(), Box<dyn std::error::Error>> {
    UserModel {
        name: Set("hoge".to_owned()),
        age: Set(20),
        ..Default::default()
    }
    .save(db)
    .await?;

    Ok(())
}

async fn find_all(db: &DatabaseConnection) -> Result<Vec<Model>, Box<dyn std::error::Error>> {
    let users = Users::find().all(db).await?;

    Ok(users)
}

Entityfind()でSELECT文を発行し、さらにall()を実行することでSELECT * FROM...というSQLが実行されて一覧取得するようになっている
子のコードを実行するとレコード数が標準出力される

WHERE句を使う

何らかの条件付きで検索してみる
今回は↓のようなテーブルからname = hogeというユーザーだけ取得する

use entities::users::Model;
use sea_orm::*;
mod entities;
use entities::users::ActiveModel as UserModel;
use entities::users::Column as UserColumn;
use entities::users::Entity as Users;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let db = Database::connect("sqlite://sample.db?mode=rwc").await?;
    let backend = db.get_database_backend();
    let schema = Schema::new(backend);
    let statement = backend.build(schema.create_table_from_entity(Users).if_not_exists());
    db.execute(statement).await?;

    let user = find_by_name("hoge".to_owned(), &db).await?;
    println!("found user name: {}", user.unwrap().name);

    Ok(())
}

async fn find_by_name(
    name: String,
    db: &DatabaseConnection,
) -> Result<Option<Model>, Box<dyn std::error::Error>> {
    let user = Users::find()
        .filter(UserColumn::Name.eq(name))
        .one(db)
        .await?;

    Ok(user)
}

Columnにattributeと各種条件(eqとか)があるのでそれを使ってfilterを作成する
今回はnameが一致したものを一つだけ取り出す条件

Update

name=hogeのレコードのageを30に更新する処理

async fn update(
    name: String,
    age: i32,
    db: &DatabaseConnection,
) -> Result<(), Box<dyn std::error::Error>> {
    let user = find_by_name(name, db).await?;
    let mut user: UserModel = user.unwrap().into();
    user.age = Set(age);
    user.update(db).await?;

    Ok(())
}

実行するとage=30に更新される

Delete

name=hogeのレコードを削除する

async fn delete_by_name(
    name: String,
    db: &DatabaseConnection,
) -> Result<(), Box<dyn std::error::Error>> {
    let user = find_by_name(name, db).await?.unwrap();
    user.delete(db).await?;

    Ok(())
}

実行するとname=hogeのレコードが消えているはず