RailsライクなRustのWebフレームワークLocoでCRUD処理を実装してみた

## 読み飛ばしてください

どうも限界派遣SESのnikawamikanです。
アドカレ24日目です。

今回は、RustのWebフレームワークLocoを試してみましたが、公式チュートリアルをなぞっているだけなので気になる方は公式サイトを見るのをオススメします。

https://loco.rs

## Locoとは

LocoとはRuby on RailsみたいにCLIで部品を生成して、Webアプリケーションを作成できるRustのWebフレームワークです。
名前の由来もRailsのlocomotiveから来ているようで、Railsのような使い勝手を提供してくれるようです。

しかし筆者はRailsを使ったことがないので、どれだけRailsっぽいのかはわかりませんが、CLIで部品を生成して、CRUDの実装が簡単にできるのは便利そうです。

なお、公式サイトによると、Node.jsの10倍速いらしいです。

## 環境構築

まずはRust環境ですが、例に漏れずDevContainerを使って環境構築しました。

開発コンテナの構成ファイルをGUIからポチポチ作っていくと、Rust & PostgreSQLの環境が用意されているので、これを利用します。

![image-1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/0dee0800-82eb-b5ab-5c39-99f2681a3206.png)


設定ファイルが出力されたら、以下のようなファイル構成になっていると思います。

```
.devcontainer
├── devcontainer.json
├── docker-compose.yml
├── Dockerfile
└── .env
```

`devcontainer.json`の`postCreateCommand`にLocoのインストールコマンドを追加します。
こうすることで、コンテナが起動したときに自動でLocoがインストールされるようになります。
dbのポートも開放しておくと便利かと思います。

```json:devcontainer.json
{
	"name": "Rust and PostgreSQL",
	"dockerComposeFile": "docker-compose.yml",
	"service": "app",
	"workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}",
	"forwardPorts": [
		"db:5432"
	],
	"postCreateCommand": "cargo install loco && cargo install sea-orm-cli",
}
```

また、Locoではデータベースを使って開発する場合、ローカルの開発環境のDB設定を`postgres://loco:loco@localhost:5432/myapp_development`のように設定する必要があるので、`.env`ファイルを作成しておきます。
`POSTGRES_DB`はアプリケーション名+`_development`になるので、適宜変更してください。

```bash
POSTGRES_USER=loco
POSTGRES_PASSWORD=loco
POSTGRES_DB=myapp_development
POSTGRES_HOSTNAME=localhost
POSTGRES_PORT=5432
```

あとは、コンテナを起動すれば自動で開発環境が構築されます。

## チュートリアルに沿ってアプリケーションを作成

今回はチュートリアルに沿って色々ためしてみます。

### プロジェクトの作成

コンテナが起動したら、アプリケーションを作成します。
コンソールから以下のコマンドを実行すると対話形式でアプリケーションの設定ができます。
4つの質問に答えるだけでアプリケーションを作成できます。

```bash
loco new
```

今回は以下のように設定しました。

```
✔ ❯ App name? · myapp
✔ ❯ What would you like to build? · Saas App with server side rendering
✔ ❯ Select a DB Provider · Postgres
✔ ❯ Select your background worker type · Async (in-process tokio async tasks)

🚂 Loco app generated successfully in:
/workspaces/loco_project/testes

- database: You've selected `postgres` as your DB provider (you should have a postgres instance to connect to)
```

これで、プロジェクトが生成されるので、`cd`コマンドで移動して、アプリケーションを起動します。

```bash
cd myapp
cargo loco start
```

結構ビルドに時間がかかるみたいなので、気長に待ちましょう。

ビルドが完了すると、以下のようなログが出力されます。
かわいいですね。

```
                      ▄     ▀                     
                                 ▀  ▄             
                  ▄       ▀     ▄  ▄ ▄▀           
                                    ▄ ▀▄▄         
                        ▄     ▀    ▀  ▀▄▀█▄       
                                          ▀█▄     
▄▄▄▄▄▄▄  ▄▄▄▄▄▄▄▄▄   ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄ ▀▀█    
 ██████  █████   ███ █████   ███ █████   ███ ▀█   
 ██████  █████   ███ █████   ▀▀▀ █████   ███ ▄█▄  
 ██████  █████   ███ █████       █████   ███ ████▄
 ██████  █████   ███ █████   ▄▄▄ █████   ███ █████
 ██████  █████   ███  ████   ███ █████   ███ ████▀
   ▀▀▀██▄ ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀  ▀▀▀▀▀▀▀▀▀▀ ██▀  
       ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀    
                https://loco.rs

environment: development
   database: automigrate
     logger: debug
compilation: debug
      modes: server

listening on http://localhost:5150
```

`http://localhost:5150`にアクセスしてみると、以下のような画面が表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/e11b2295-6317-d6f9-e5d5-b4e95b30ec51.png)

これで起動まで確認できました。

### コントローラーの作成


次にコントローラーを作成してみます。

```bash
cargo loco generate controller guide --api
```

これで`src/controller/guide.rs`が生成されます。

```rs
#![allow(clippy::missing_errors_doc)]
#![allow(clippy::unnecessary_struct_initialization)]
#![allow(clippy::unused_async)]
use loco_rs::prelude::*;
use axum::debug_handler;

#[debug_handler]
pub async fn index(State(_ctx): State<AppContext>) -> Result<Response> {
    format::text("hello")
}

pub fn routes() -> Routes {
    Routes::new()
        .prefix("api/guides/")
        .add("/", get(index))
}
```

これで、`localhost:5150/api/guides`にアクセスすると、`hello`と表示されるようになります。

```bash
$ curl http://localhost:5150/api/guides
hello
```

### モデルの作成

ガイドに沿って`Article`モデルを作成してみます。

```bash
cargo loco generate model article title:string body:text
```

これで、`src/model/_entities/article.rs`と`src/model/article.rs`が生成されます。
`src/model/_entities/article.rs`はBDスキーマを定義しているファイルで、これは変更してはいけないようです。

ロジックは`src/model/article.rs`に書くようです。

自動で生成されたファイルは以下のようになっています。

```rs: src/model/article.rs
use sea_orm::entity::prelude::*;
use super::_entities::articles::{ActiveModel, Entity};
pub type Articles = Entity;

#[async_trait::async_trait]
impl ActiveModelBehavior for ActiveModel {
    // extend activemodel below (keep comment for generators)

    async fn before_save<C>(self, _db: &C, insert: bool) -> std::result::Result<Self, DbErr>
    where
        C: ConnectionTrait,
    {
        if !insert && self.updated_at.is_unchanged() {
            let mut this = self;
            this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into());
            Ok(this)
        } else {
            Ok(self)
        }
    }
}
```

また同時にマイグレーションが行われているようで、DBを確認すると`articles`テーブルが作成されていました。

![image-3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/f9b9659a-98c6-9460-bf69-e747337ff2e8.png)


これで、モデルの作成が完了しました。

### CRUDの実装

先ほどのMODELを使ってCRUDの実装をしてみます。

これもコマンド一発で生成できます。

```bash
cargo loco generate scaffold article
```

これで、`src/controller/article.rs`が生成されますが、Updateの実装は自分で行う必要があるので、以下のように実装します。

```rs
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Params {
    pub title: Option<String>,
    pub content: Option<String>,
}

impl Params {
    fn update(&self, item: &mut ActiveModel) {
        item.title = Set(self.title.clone());
        item.content = Set(self.content.clone());
    }
}
```

これだけで、CRUDの実装が完了します。殆どのコードが自動で生成されているので、非常に簡単に実装できます。

自動で生成されたルーティングは以下のようになっています。

```rs
pub fn routes() -> Routes {
    Routes::new()
        .prefix("api/articles/")
        .add("/", get(list))
        .add("/", post(add))
        .add(":id", get(get_one))
        .add(":id", delete(remove))
        .add(":id", put(update))
        .add(":id", patch(update))
}
```

### 動作確認

試しにデータを登録して、取得してみます。
```bash
curl -X POST -H "Content-Type: application/json" -d '{
    "title":"Locoくんすごいね",
    "content":"俺、コード全然かいてねえよ・・・。かなしい・・・。"
}' localhost:5150/api/articles

curl localhost:5150/api/articles
[{"created_at":"2024-12-24T14:52:36.440264Z","updated_at":"2024-12-24T14:52:36.440264Z","id":5,"title":"Locoくんすごいね","content":"俺、コード全然かいてねえよ・・・。かなしい・・・。"}]
```

ついでに、Updateも試してみます。

```bash
curl -X PUT -H "Content-Type: application/json" -d '{
    "title":"Locoくんすごいね",
    "content":"コードかかなくても動いてるよ！すごいね！"
}' localhost:5150/api/articles/5

curl localhost:5150/api/articles/5
{"created_at":"2024-12-24T14:52:36.440264Z","updated_at":"2024-12-24T14:55:18.474649Z","id":5,"title":"Locoくんすごいね","content":"コードかかなくても動いてるよ！すごいね！"}
```

最後に、Deleteも試してみます。

```bash
curl -X DELETE localhost:5150/api/articles/5

curl localhost:5150/api/articles/5
{"error":"not_found","description":"Resource was not found"}
```

簡単にCRUDの実装ができましたね。


## まとめ

今回は、RustのWebフレームワークLocoを試してみました。
正直、この記事読むより公式のチュートリアルのほうがわかりやすいので、興味がある方は公式サイトを見てみてください。

本当はもうすこし具体的なアプリ開発をしたかったのですが、`cargo loco`コマンドを実行する度にコンパイルが走るので、思ったよりも時間がかかってしまいました。

それでは。


## 参考

https://loco.rs