[Rust] SeaORMでDBアクセスする
Introduction
RustでORMといえばDieselの一択でしたが、
しばらく見ないうちに他のプロダクトもけっこうでてきてました。
本稿ではその中の1つ、SeeORMを使ってみます。
SeaORM?
SeaORMは、Rustと各種RDBを接続するライブラリです。
コードからDBにアクセスするライブラリとしてだけでなく、Migration機能や
SeaORM用のRustエンティティ生成などの機能をもっています。
最初から非同期&動的な扱いが可能で、
使いやすさを重視しているライブラリみたいです。
今回はMySQLをつかって
SeaORMのMigration機能をつかってスキーマを定義し、
エンティティの生成とCRUDアクセスをやってみましょう。
Environment
今回試した環境は以下のとおりです。
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 12.4
- Rust : 1.70.0
- MySQL : 8.0.32
Setup
まずはMySQLの準備をします。
CLIでデータベースを作成しましょう。
% mysql -u<ユーザー名> -p ・ ・ mysql > create database my_db;
次にCargoでプロジェクトの作成をします。
% cargo new seaorm-example && cd seaorm-example
Cargo.tomlで依存ライブラリの設定。
[dependencies] futures = "0.3.28" sea-orm = { version = "0.11.3", features = [ "sqlx-mysql", "runtime-async-std-native-tls", "macros" ] }
今回はMySQLを使うのでsqlx-mysqlを指定します。
runtimeについてはこのあたりをご確認ください。
とりあえずmain.rsを↓のように記述します。
use futures::executor::block_on; use sea_orm::{Database, DbErr}; const DATABASE_URL: &str = "mysql://<ユーザー名>:<パスワード>@localhost:3306"; const DB_NAME: &str = "my_db"; async fn run() -> Result<(), DbErr> { let db = Database::connect(DATABASE_URL).await?; println!("{:?}",db); Ok(()) } fn main() { if let Err(err) = block_on(run()) { panic!("{}", err); } }
実行して問題なく終了すればセットアップはOKです。
% cargo run Compiling seaorm v0.1.0 Finished dev [unoptimized + debuginfo] target(s) in 1.14s Running `target/debug/seaorm` SqlxMySqlPoolConnection
Migration Database
次に、sea-orm-cliでMigrationしてみます。 CargoでCLIツールをインストールしましょう。
% cargo install sea-orm-cli
プロジェクトのルートでmigrate initを実行して初期化します。
あたらしくmgrationディレクトリ以下にRustプロジェクトが作成されました。
% sea-orm-cli migrate init Initializing migration directory... Creating file `./migration/src/lib.rs` Creating file `./migration/src/m20220101_000001_create_table.rs` Creating file `./migration/src/main.rs` Creating file `./migration/Cargo.toml` Creating file `./migration/README.md` Done!
現在のディレクトリ構成はこんな感じです。
% tree -I target . ├── Cargo.lock ├── Cargo.toml ├── migration │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── lib.rs │ ├── m20220101_000001_create_table.rs │ └── main.rs └── src └── main.rs
migrationファイルを作成してDBの更新をします。
今回は↓のようにBookとAuthorのシンプルなテーブルを2つ作成しましょう。
ファイル名は
m
という形式にする必要があるみたいです。
migration/Cargo.tomlも先程のCargo.tomlと同じライブラリを設定します。
[dependencies.sea-orm-migration] version = "0.11.0" features = [ "sqlx-mysql", "runtime-async-std-native-tls", ]
作成するテーブルに対応したmigrationファイルを作成します。
まずはAuthor用のファイル。
idとnameだけもつシンプルなテーブルです。
//seaorm/migration/src/m20230621_000001_create_author_table.rs 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(Author::Table) .if_not_exists() .col( ColumnDef::new(Author::Id) .integer() .not_null() .auto_increment() .primary_key(), ) .col(ColumnDef::new(Author::Name).string().not_null()) .to_owned(), ) .await } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(Author::Table).to_owned()) .await } } #[derive(Iden)] pub enum Author { Table, Id, Name, }
Book用のmigrationファイルです。
Authorとの外部キーを設定している分、少しだけコードが多いです。
//seaorm/migration/src/m20230621_000002_create_book_table.rs use sea_orm_migration::prelude::*; use super::m20230621_000001_create_author_table::Author; #[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(Book::Table) .if_not_exists() .col( ColumnDef::new(Book::Id) .integer() .not_null() .auto_increment() .primary_key(), ) .col(ColumnDef::new(Book::Name).string().not_null()) .col(ColumnDef::new(Book::AuthorId).integer().not_null()) .foreign_key( ForeignKey::create() .name("fk-book-author_id") .from(Book::Table, Book::AuthorId) .to(Author::Table, Author::Id), ) .to_owned(), ) .await } async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { manager .drop_table(Table::drop().table(Book::Table).to_owned()) .await } } #[derive(Iden)] enum Book { Table, Id, Name, AuthorId }
lib.rsで↑のmigrationファイルをmodして対象モジュールとして
Vecに格納します。
pub use sea_orm_migration::prelude::*; mod m20230621_000001_create_author_table; mod m20230621_000002_create_book_table; pub struct Migrator; #[async_trait::async_trait] impl MigratorTrait for Migrator { fn migrations() -> Vec<Box<dyn MigrationTrait>> { vec![ Box::new(m20230621_000001_create_author_table::Migration), Box::new(m20230621_000002_create_book_table::Migration) ] } }
migrate refreshコマンドをプロジェクトのルートで実行します。
% DATABASE_URL="mysql://<ユーザー名>:<パスワード>@localhost:3306/my_db" sea-orm-cli migrate refresh Running `cargo run --manifest-path ./migration/Cargo.toml -- refresh -u mysql:・・・ Compiling migration v0.1.0 (/seaorm/migration) Finished dev [unoptimized + debuginfo] target(s) in 3.37s Running `migration/target/debug/migration refresh -u 'mysql:・・・/my_db'` Rolling back all applied migrations No applied migrations Applying all pending migrations Applying migration 'm20230621_000001_create_author_table' Migration 'm20230621_000001_create_author_table' has been applied Applying migration 'm20230621_000002_create_book_table' Migration 'm20230621_000002_create_book_table' has been applied
MySQLのCLIで確認してみましょう。
% mysql -u<ユーザー名> -p ・ ・ mysql> use my_db; mysql> show tables; +------------------+ | Tables_in_my_db | +------------------+ | author | | book | | seaql_migrations | +------------------+ mysql> show create table book; | book | CREATE TABLE `book` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, `author_id` int NOT NULL, PRIMARY KEY (`id`), KEY `fk-book-author_id` (`author_id`), CONSTRAINT `fk-book-author_id` FOREIGN KEY (`author_id`) REFERENCES `author` (`id`) )
テーブルが作成され、外部キーも設定されてます。
Generate Entity from Database
migrationでMySQLにテーブルができたので、
次はRustプログラムで使用するエンティティを生成します。
CLIのgenerate entityを使えば生成されます。
% sea-orm-cli generate entity \ -u mysql://<ユーザー名>:<パスワード>@localhost:3306/my_db \ -o src/entities sea-orm-cli generate entity \ -u mysql://・・・/my_db \ -o src/entities Generating author.rs > Column `id`: i32, auto_increment, not_null > Column `name`: String, not_null Generating book.rs > Column `id`: i32, auto_increment, not_null > Column `name`: String, not_null > Column `author_id`: i32, not_null
ディレクトリ構成は↓のようになりました。
% tree -I target . ├── Cargo.lock ├── Cargo.toml ├── migration │ ├── Cargo.lock │ ├── Cargo.toml │ ├── README.md │ └── src │ ├── lib.rs │ ├── m20230621_000001_create_author_table.rs │ ├── m20230621_000002_create_book_table.rs │ └── main.rs └── src ├── entities │ ├── author.rs │ ├── book.rs │ ├── mod.rs │ └── prelude.rs └── main.rs 4 directories, 14 files
Write code to access DB
ではmain.rsにCRUD操作を記述していきます。
まずは必要モジュールのimportなど。
mod entities; use futures::executor::block_on; use sea_orm::*; use entities::{prelude::*, *}; use sea_orm::InsertResult; use entities::author::ActiveModel as AuthorModel; use entities::book::ActiveModel as BookModel; use crate::book::Model; const DATABASE_URL: &str = "mysql://<ユーザー名>:<パスワード>@localhost:3306/"; const DB_NAME: &str = "my_db";
run関数の中に各種処理を記述していきます。
まずはAuthorとBookをinsertする処理です。
ActiveModelを使ってプロパティを設定してinsert関数を実行します。
async fn run() -> Result<(), DbErr> { let db:DatabaseConnection = Database::connect(DATABASE_URL.to_owned() + DB_NAME).await?; let author_res = insert_author(&db,"Donald E. Knuth").await?; println!("author_res : {:?}",author_res); let book_res = insert_book(&db,author_res.last_insert_id,"The Art of Computer Programming").await; println!("book : {:?}",book_res); Ok(()) } async fn insert_author(db:&DatabaseConnection,name:&str) -> Result<InsertResult<AuthorModel>, DbErr>{ let author = author::ActiveModel { name: ActiveValue::Set(name.to_owned()), ..Default::default() }; let res = Author::insert(author).exec(db).await?; println!("{}", type_of(&res)); Ok(res) } async fn insert_book(db:&DatabaseConnection,author_id:i32,name:&str) -> Result<InsertResult<BookModel>, DbErr>{ let book = book::ActiveModel { name: ActiveValue::Set(name.to_owned()), author_id: ActiveValue::Set(author_id), ..Default::default() }; let res = Book::insert(book).exec(db).await?; println!("{}", type_of(&res)); Ok(res) }
selectはこんなかんじです。
async fn run() -> Result<(), DbErr> { ・・・・・・・・・・・・・・ //insert処理の後 select(&db).await; let book_id = book_res.unwrap().last_insert_id; println!("book_id : {:?}",book_id); Ok(()) } async fn select(db:&DatabaseConnection) { // find all models let books: Vec<book::Model> = Book::find().all(db).await.unwrap(); for item in &books { println!("find all = {:?}", item); } // find and filter let filter_books: Vec<book::Model> = Book::find() .filter(book::Column::Name.contains("Computer")) .all(db) .await.unwrap(); for item in &filter_books { println!("filter = {:?}", item); } }
PK検索、update、deleteは下記です。
どの処理も直感的にわかりやすいものになってます。
async fn run() -> Result<(), DbErr> { ・・・・・・・・・・・・・・ //insert処理の後 let book = find_book_by_id(&db,book_id).await; println!("find by id = {:?}", book); let active_model_book = book.unwrap().into_active_model(); update(&db,&active_model_book); let book2 = find_book_by_id(&db,book_id).await; let active_model_book2 = book2.unwrap().into_active_model(); let delete = delete(&db,&active_model_book2).await.unwrap(); println!("delete= {:?}", delete); Ok(()) } async fn find_book_by_id(db:&DatabaseConnection,id:i32) -> Option<book::Model> { Book::find_by_id(id).one(db).await.unwrap() } async fn delete(db:&DatabaseConnection,book:&book::ActiveModel) -> Result<DeleteResult, DbErr>{ Ok(book.clone().delete(db).await.unwrap()) } async fn update(db:&DatabaseConnection,book:&book::ActiveModel) -> Result<Model, DbErr>{ let mut b= book.clone(); b.name = Set("update book".to_owned()); let res = b.update(db).await; println!("{}", type_of(&res)); res }
実行すると↓のような感じでCRUD処理が実行されます。
%cargo run &sea_orm::executor::insert::InsertResult<seaorm::entities::author::ActiveModel> author_res : InsertResult { last_insert_id: 39 } &sea_orm::executor::insert::InsertResult<seaorm::entities::book::ActiveModel> book_res : Ok(InsertResult { last_insert_id: 34 }) find all = Model { id: 34, name: "The Art of Computer Programming", author_id: 39 } book_id : 34 find by id = Some(Model { id: 34, name: "The Art of Computer Programming", author_id: 39 }) delete= DeleteResult { rows_affected: 1 }
シンプルなCRUDを実行してみました。
チュートリアルやcookbookには
いろいろな使いかたやクエリの記述方法が書いてあるのでご確認ください。
Sumarry
今回はRustのORM、SeaORMを使ってみました。
シンプルで直感的、ツールも整っていて使いやすいと思います。
じゃあDieselかSeaORMどっちがいいのって話になりますが、
これは一概にどっちともいえません。
ここで比較されてますが、
どっちを選ぶかはプロジェクトの仕様によって異なるとの結論です。
Dieselは堅牢な機能とパフォーマンスに優れており、
コンパイル時のチェック、安全性、シンプルさを優先する場合は、Diesel。
SeaORMは多くのDBエンジンと遅延読み込みをサポートしており、
直感的な操作と生産性が高いです。
それらを考慮したうえで選択しましょう。