😺

RustでAPIを開発してみたら結構辛かった話

2023/02/20に公開
14

はじめに

皆様こんにちは、株式会社プラハのAwataです。
今日は、以前書いたリーダーの振り返り記事で軽く触れていた、RustでのAPI開発についての記事を書いていこうと思います。

結論RustでWebは辛い!という話なんですが、約5か月くらいRustでWeb開発をしたので、今後の参考になるようなことを書いていこうと思います。
ぜひ最後までお付き合いください。

TL;DR

  • RustでWeb開発はまだ早いかもしれない。
  • RustでDDDはやりやすい。ただしDIがやりにくい場合があるので、そこは要注意。
  • Rustはモジュールの仕組みが協力なので、モジュラモノリスはやりやすい。
  • サンプルリポジトリはこちら
  • Rustはやっぱり難しいけど人気の理由も少し分かった気がする

そもそもなぜRustでやってみようとなったのか

前例が少ない中、どうしてRustで開発しようと思ったのか気になる方も多いと思いますので、最初はRustを選択した理由について書いていこうと思います。

結論だけ先に伝えておくと、これはチャレンジです。

技術的要素以外の背景があった

今回のアプリケーションにおいて、機能要件的にも非機能要件的にもRustは必要ありませんでした。

技術的な要件で言うと

  • ある程度の規模までなら苦しまずに開発ができる
  • 静的型付けがある

といった程度で、言葉を選ばずに言うなら何の言語でも開発して運用ができたと思っています。

ただし、技術的要素以外に、以下のような背景がありました。

エンジニア採用や社外への情報公開の際に、目に留まりやすいような技術を使って欲しい!

この考え方に賛否両論(否が多めかもしれない)あることは承知していますが、現状の株式会社アガルートの技術スタックだと、求めているようなエンジニアを採用するのは難しいということがありました。
そのため、まずは目に留まるところから始めてみようという背景がありました。

また、新しい技術スタックを取り入れることで、以下のような相乗効果にも期待していました。

  • 新しい技術を取り入れやすい企業文化ということをアピールできる
  • 新技術のキャッチアップを常にやっている学習意欲の高い人が多い企業文化ということをアピールできる

いくつの候補を上げてみた

ここまでに書いたもろもろの背景を考慮すると、Rust, Kotlin, Goの3つが候補に上がりました。
ここでは簡単に比較してみましょう。

Rust

  • メリット
    • モジュールの仕組みが強い(後で詳しく書いています)
    • 社内メンバーのやってみたいという気持ちが一番強い
    • かなり尖った技術選定なので、何らかのアピールにはなる(良い方か悪い方かはさておき)
  • デメリット
    • 言語の習得難易度が高い
    • 前例が少ないので情報も少ない
    • エコシステムに不安がある
    • そもそも作れるか分からない

Kotlin

  • メリット
    • Springという強いフレームワークがある
    • 比較的習得難易度が低い
  • デメリット
    • 社内にJVMを好まない人が多く、社内ではあまり人気がなさそうだった
    • アピールになるか?という点では最も弱そう
    • 経験者が誰もいなかったので、どうせ同じ新技術使うならRustの方が...みたいな雰囲気もあった

Go

  • メリット
    • 言語仕様が簡単なのでコードリーディングが容易
    • マイクロサービスへの変更が他に比べると容易になるかも
  • デメリット
    • 言語でできることが少なく、ライブラリ等が必須になりそう(mapがないとかそういう系)
    • モジュールの仕組みが弱く、規模が大きくなった時に不安
    • プライベート関数、パブリック関数などを厳密に制御しづらく、紳士協定に従って実装しましょうみたいな雰囲気になりそうな懸念がある

これらのメリットデメリットや、みんなのやりたい気持ちなど色々と考慮して議論しました。
また、ミニマムのアプリケーションを1週間くらいかけてをRustで開発してみて、「まあしんどいけど、なんとかはなりそう」という雰囲気が漂ってきました。
そして、最終的に以下のような結論になりました。

よし、Rustでやってみよう!無理ならTypeScriptで死ぬ気で作り直そう!

そしてここから果てしない旅が始まるのでした...。

工夫したところ

辛かった話なんですが、辛い!辛い!と書いても誰も嬉しくないと思うので、その辛さを乗り越えるために工夫したところを紹介しようと思います。
これで辛さが2割くらい削減されたような気がします。

統合テストと単体テストを別々に動かせるようにした

自分は統合テストは実行コストが高いため、単体テストとは別々で動かしたいと考えています。

そして、Rustには簡単に単体テストや統合テストを書ける仕組みが用意されています。
単体テストは同じファイルに気軽に書けて、統合テストも特殊な書き方を覚える必要もなく簡単に書けるため、とても良い仕組みだと思っています。
しかし、統合テストだけを動かすコマンドは自分の調べた限りでは用意されていませんでした。

そこで、フィーチャーフラグを使って、統合テストだけを動かせるようにしました。

手順は以下の通りです。

1. Cargo.toml に以下のようにフィーチャーフラグを定義する

[package]
# 省略

[dependencies]
# 省略

[features]
integration_test = []

2. testsディレクトリに配置している統合テストに、以下のようにフィーチャーフラグを指定する

#[cfg(feature = "integration_test")]
mod test {
  // 省略
}

3. テスト実行時にフィーチャーフラグを指定する

cargo test --features integration_test

Fromトレイトを積極的に使って変換処理を楽にした

今回の設計では、doman/usecase/presentation/infra/scenario と言った具合にレイヤーが多く存在しています。
そして、レイヤー間でのデータのやり取りをする際は、DTO的な構造体を定義して使っています。
そしれ、それぞれの構造体は同じようなプロパティを持っていることが多く、詰め替え作業が割と面倒です。

※ Rustは公称型のため、例え全てのプロパティが同じでも必ず詰め替え作業が必要です

これまでの自分なら、以下のようなfrom_xxxといったメソッドを定義していたと思います。

// イメージを共有するためだけのコードなのでコンパイルできません

let usecase_result = usecase::execute();
PresentationResult::from_usecase_result(usecase_result);

しかし、このコードはあまりRustらしくないですね。
Rustでは、Fromトレイトが用意されているため、それを使うと以下のように書けます。

// イメージを共有するためだけのコードなのでコンパイルできません

let usecase_result = usecase::execute();
PresentationResult::from(usecase_result);

これだけ見ると、なんかちょっと短くなっただけじゃんと思うかもしれませんが、Fromトレイトを他の構造体にも実装してあげれば、他の構造体も同じようにfromメソッドを通じて変換できるようになります。

? 演算子を積極的に使ってエラーハンドリングを楽にした

Rustには例外は存在せず、基本的にはResult型を使ってエラーハンドリングを行います。

パニックも存在しますが、これは原則回復不能なエラーに対して使うため、ハンドリングすることはあまりありません。

サンプルリポジトリのこちらに以下のようなコードがありますが、ここで?演算子を使わずに書くと以下のようになります。

pub async fn execute(
  context: web::Data<RequestContext>,
  user_id: web::Path<String>,
) -> Result<HttpResponse, ApiError> {
  let request = FindOneUserRequest {
    id: user_id.to_string(),
  };

  let response: Result<Result<Option<UserResponse>, ApiError>, BlockingError> = web::block(move || {
    let conn = context.get_connection();

    find_one_user::execute(conn, request)
  })
  .await;

  match response {
    Ok(response_2) => match response_2 {
      Ok(response_3) => match response_3 {
        Some(response_3) => Ok(HttpResponse::Ok().json(UserScenarioResponse::from(response_3))),
        None => Ok(HttpResponse::Ok().body("User not found")),
      },
      Err(_) => Ok(HttpResponse::Ok().body("User not found")),
    },
    Err(_) => Ok(HttpResponse::Ok().body("User not found")),
  }
}

地獄のようにmatch式がネストしていますね。
web::blockの戻り値が、Result<T, BlockingError>で、find_one_user::executeの戻り値がResult<Option<UserResponse>, ApiError>で、これが先ほどのTに入るため、このような型になってしまいます。

これを簡略化するために?演算子を使う必要があり、?演算子を使えるようにするには(このコードにおいては)Fromトレイトを通って、ApiErrorに変換されるように定義しておく必要があります。
そしてその変換処理のこちらにあるため、上記のコードは以下のようになります。

pub async fn execute(
  context: web::Data<RequestContext>,
  user_id: web::Path<String>,
) -> Result<HttpResponse, ApiError> {
  let request = FindOneUserRequest {
    id: user_id.to_string(),
  };

  let response = web::block(move || {
    let conn = context.get_connection();

    find_one_user::execute(conn, request)
  })
  .await??;

  match response {
    Some(response) => Ok(HttpResponse::Ok().json(UserScenarioResponse::from(response))),
    None => Ok(HttpResponse::Ok().body("User not found")),
  }
}

match式のネストがなくなり、コードが簡潔になりました。
もっと詳しく知りたい方はこちらを読んでみることをおすすめします

認証済みのリクエストしかアクセスできないような制御をする仕組みを用意した

前提として「APIサーバーの前にBFFがあり、APIはプライベートネットワークに置いてあり、BFFからのリクエストしか受け付けないように設定されている」という状態です。
そして今回は、BFFからのリクエストに認証済みのユーザー情報を付与し、APIサーバーではその情報が正しく設定されているかどうかを検証するという構成にしました。
こちらのやり方をかなり参考にしていますので、ぜひこちらも併せて読んでみてください。

まずはFromRequestトレイトを使って、ヘッダーから必要な情報を取得してAuthUserという認証済みユーザーを表す構造体に変換する処理を実装します。

まずはAuthUser構造体を定義しましょう

pub struct AuthUser {
  pub id: String,
  pub mail_address: String,
  pub role: String,
}

impl AuthUser {
  pub fn is_admin(&self) -> bool {
    self.role == "admin"
  }
  pub fn is_user(&self) -> bool {
    self.role == "user"
  }
}

そしてFromRequestトレイトを実装します。
ここでヘッダーの中身を見て、送られてきたIDは本当に存在するのか?など色々なチェックをすることも可能です。

impl FromRequest for AuthUser {
  type Error = ApiError;
  type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;

  fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
    let request = req.clone();
    Box::pin(async move {
      let role = match request.headers().get("auth-user-role") {
        Some(id) => id.to_str(),
        None => return Err(ApiError::new(StatusCode::FORBIDDEN, "forbidden".to_owned())),
      }
      .unwrap();
      if role != "user" && role != "admin" {
        return Err(ApiError::new(StatusCode::FORBIDDEN, "forbidden".to_owned()));
      }
      let id = match request.headers().get("auth-user-id") {
        Some(id) => id.to_str(),
        None => return Err(ApiError::new(StatusCode::FORBIDDEN, "forbidden".to_owned())),
      }
      .unwrap();
      let mail_address = match request.headers().get("auth-user-email") {
        Some(id) => id.to_str(),
        None => return Err(ApiError::new(StatusCode::FORBIDDEN, "forbidden".to_owned())),
      }
      .unwrap();
      Ok(AuthUser {
        id: id.to_string(),
        mail_address: mail_address.to_string(),
        role: role.to_string(),
      })
    })
  }

  fn extract(req: &HttpRequest) -> Self::Future {
    Self::from_request(req, &mut Payload::None)
  }
}

そして、このAuthUserscnearioに定義されている関数の引数に設定します。
これによってactix-webが自動的にAuthUserを取得してくれるようになり、FromRequestの変換中にエラーが起きた際は自動的にエラーレスポンスが返ります。

pub async fn execute(req_user: AuthUser) -> Result<HttpResponse, ApiError> {
  // 省略
}

本番用のコードでは、AuthUser以外にAdminUser, NormalUserのように更に権限に応じた構造体を定義することで、権限に応じたアクセス制限が簡単にできるようにしてあります。
サンプルリポジトリではコードが増えすぎるし(ちょっと疲れてきてた)ので省略しましたが、もし気になるけどやり方が分からない!という方はコメントで教えてください!
(元気があれば追記します)

scenario層を用意した

scenario層は複数のモジュールのpresentation層に定義されてある関数を呼び出して処理を進める層です。
複数のpresentation層の関数を呼んでも良いですし、1つだけでも良いです。
そして、必要であればトランザクションの管理も行います。

ここまで読んでいて勘の良い人は、なんかこれ何かの役割と似てるな?と思ったかもしれませんが、scenario層はマイクロサービスにおけるSagaパターンのオーケストレーター的な役割を果たします。

これ以外にも、**ルーティングをどうやって一元で管理するか?**という問題もscenario層は解決してくれます。
scenario層が存在しない場合、各モジュールでルーティングを定義する形式になってしまい、他のモジュールでそのルーティングが使われていないかを都度確認しなくてはならなくなります。
そのため、例えpresentation層の関数を呼ぶだけであっても必ずscenario層を用意して、「HTTPエンドポイントと紐づけるのはscenario層の関数だけ」というルールで運用しています。

良かったところ

続いてRustの良かったところです。
良かったところも多くあったんですが、それでもやっぱり辛いということは、つまりそういうことなのです。

モジュールの可視性を細かく設定できる

あまり細かく書くとこれだけで1つの記事になってしまいそうなので、箇条書きで良かったと感じたところを書いてみます。

  • ルートからモジュールを宣言していく必要があるので、モジュールの階層構造が明確になる
  • 基本がprivateなので、知らず知らず公開してしまっているものがない
  • 公開範囲を細かく設定できるので、全公開が全非公開か!みたいな2択を迫られない
    • pub: 外部クレートにも公開
    • pub(super): 親モジュールには公開
    • pub(crate): 現在のクレート内には公開

もっと詳しく知りたい方はまずこちらを読むことをおすすめします。

構造体の定義と独自実装、トレイトの実装を分けて書けるので見通しが良い

これは自分の経験が浅いからかもしれないのですが、過去に使っていたクラスベースの言語では、以下のすべてが同じクラスに書かれることが多かったです

  • プロパティ
  • 独自のメソッド
  • インターフェースを継承したメソッド(Rustだとトレイトの実装になります)

この状態になると、まずプロパティを上の方に書いて、次に独自のメソッドを書いて、最後にインターフェースを継承したメソッドを書く、みたいな暗黙の了解が生まれることが多かったです。
そのため、コードに手を入れる時も若干気を使ったり、、、という感じでした。

しかし、Rustではこれらを全て別々に書くことができます。

具体的なコードはこんな感じですね。

// 構造体の定義
pub struct UserId {
  pub value: String,
}

// 独自の振る舞いの定義
impl UserId {
  pub fn restore(value: String) -> Self {
    let value = Ulid::from_string(&value).unwrap().to_string();
    Self { value }
  }
}

// トレイトの実装
impl Default for UserId {
  fn default() -> Self {
    let value = Ulid::new().to_string();
    Self { value }
  }
}

// トレイトの実装
impl TryFrom<String> for UserId {
  type Error = String;

  fn try_from(value: String) -> Result<Self, Self::Error> {
    let ulid = Ulid::from_string(&value);
    match ulid {
      Ok(value) => Ok(Self {
        value: value.to_string(),
      }),
      Err(err) => Err(format!("can't convert to AdminId. error: {err}")),
    }
  }
}

どうでしょう?めっちゃ読みやすくないですか?(急に感覚的な話を出して申し訳ないですが)

静的解析が強いので、脳死で従うだけでも統一感のあるコードに近づいていく

Rustには、コードの静的解析を行うツールがいくつかあります。
自分が導入したのは、有名どころの以下2つです。

JavaScript界隈に例えていうならば、rustfmtはprettier、clippyはESLintです。
導入や設定もとても簡単なので、チームで開発する際は必ず入れておいて間違いないと思います。

苦労したところ

最後に苦労したところです。
全体的に、それRustが悪い訳じゃなくて、まだWeb開発で使われてないだけやん?という感じです。

ライフタイムが今でも理解できていない気がしているくらい難しかった

Rustの勉強をしていると、これらの言葉をよく聞きますよね

これらも非常に難しい概念だと思いましたが、自分にとってライフタイムは群を抜いて難しかったです。
今もライフタイムとは?と言われるとうまく説明できる自信はありません。

関数定義の場合はまだ分かりやすいですが、特に構造体の定義において、ライフタイムをどう定義するかが難しかったです。

具体例をいい感じに書けず申し訳ないのですが、構造体に参照を保持させる場合は特に注意してほしいです。
可能なら構造体を使わず関数でうまく書ける方法を探すことを自分はおすすめしたいです。

可変参照を色々なところで使わざるを得ない状況になってしまった

Rustの可変参照は、同時に1つしか存在できないという制約があります。
今回はdieselというORMを使ったのですが、このORMが提供しているDBとのコネクションオブジェクトが可変参照でした。
そしてDDDのやり方を参考にしていたので、複数のリポジトリのコンストラクタにこのコネクションオブジェクトを渡す必要がありました。
また、生成されたリポジトリも必ず1つのユースケースやドメインサービスで使用されるとは限らず、複数のコンストラクタに渡される場合があります。
そうすると、コード上の様々な場所で可変なDBのコネクションオブジェクトが使われることになってしまいます。
(ここはもっとうまく設計できたかなと思いますが、今の自分にはこれらを解決できる設計が思い浮かびませんでした)

例えば、以下のようなコードです。

// conn は &mutなオブジェクト
let conn = get_connection();
let hoge_repository = HogeRepository::new(conn);
let fuga_repository = FugaRepository::new(conn);

let foo_usecase = FooUsecase::new(hoge_repository, fuga_repository);

こちらのコードは、connが2つのリポジトリに渡されていることになり、コンパイルエラーになります。
そのため、今回は苦肉の策として、RcRefCellを組み合わせて無理やり可変参照を複数の場所から参照できるようにしました。

ただしこの書き方をした場合、実行時までエラーに気づけない可能性があります。
これまではコンパイラが「可変参照を複数のところで使っているよ!直してね!」と怒ってくれていましたが、この書き方だとコンパイラは何も言ってくれません。
しかし、実装が間違っていて複数の場所から可変参照が使われた場合、実行時エラーとなってしまいます。

今回はこれ以外の解決方法を出せなかったのでこの書き方を採用しましたが、もし他に良い方法があれば教えていただきたいです。

構造体をJSON化したいだけなのに、コードがいっぱい必要だった

例えばJavaScriptでJSON文字列化したい場合、JSON.stringifyを使えば簡単にできます。
プロパティにDateオブジェクトがあったとしても、特に意識することなく使えますね。

しかし、Rustはそう簡単には行きません。
具体的にどういう手順を踏んだかを書いてみます。

今回はserdeというクレートを使ってJSON化の処理を実装しました。

1. Cargo.tomlにserdeの依存関係を追加する

[package]
# 省略

[dependencies]
# 省略
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

2. JSON化したい構造体の定義

今回は、scenario層で使われているレスポンスの構造体を例に挙げてみます。

pub struct UserScenarioResponse {
  pub id: String,
  pub first_name: String,
  pub last_name: String,
  pub mail_address: String,
  pub age: i16,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

3. JSON化したい構造体にSerializeトレイトを実装する

今回はderiveマクロを使って、自動生成してみましょう。
独自のシリアライズ処理を実装したい場合は、deriveマクロを使わずに自分で実装することもできます。
(試しに一度やってみましたが、まあまあ大変だったので自分はもうやりたくないですw)

#[derive(Serialize)]
pub struct UserScenarioResponse {
  pub id: String,
  pub first_name: String,
  pub last_name: String,
  pub mail_address: String,
  pub age: i16,
  pub created_at: DateTime<Utc>,
  pub updated_at: DateTime<Utc>,
}

4. DateTime<Utc>は以下のようにフォーマットを指定する必要がある

追記

こちらのコメントでもっと簡単なやり方を教えて頂きました!
そのため、以下に記載されているやり方は冗長な方法になりますのでご注意ください!

今回はDateTimeがエラーを起こしていましたが、他の型でも同様のエラーが出る可能性があります。
また、構造体のプロパティに構造体があった場合、その構造体もSerializeトレイトを実装する必要があります。

pub mod date_format {
  use chrono::{DateTime, TimeZone, Utc};
  use serde::{self, Deserialize, Deserializer, Serializer};

  const FORMAT: &str = "%Y-%m-%d %H:%M:%S";

  pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
  where
    S: Serializer,
  {
    let s = format!("{}", date.format(FORMAT));
    serializer.serialize_str(&s)
  }
  pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
  where
    D: Deserializer<'de>,
  {
    let s = String::deserialize(deserializer)?;
    Utc
      .datetime_from_str(&s, FORMAT)
      .map_err(serde::de::Error::custom)
  }
}

#[derive(Serialize)]
pub struct UserScenarioResponse {
  pub id: String,
  pub first_name: String,
  pub last_name: String,
  pub mail_address: String,
  pub age: i16,
  #[serde(with = "date_format")]
  pub created_at: DateTime<Utc>,
  #[serde(with = "date_format")]
  pub updated_at: DateTime<Utc>,
}

これでやっとシリアライズができるようになりました!
一度実装してしまえば他の構造体にも流用できるのですが、やはり面倒だなと感じました。

(自分的に)使いやすいORMがなかった

今回はdiesel, sea-orm, sqlxあたりを検討して、情報量の多さや実績のあるdieselを採用しました。
他のORMを細かく試した訳ではないのですが、やはりエコシステムの弱さは感じました。

RailsのActiveRecordまでとは言いませんが、もう少し使いやすいORMがあればいいなと思いました。
Rustの言語仕様上仕方ない部分があるとは思いますが、やはりコード量が多くなったり大量の構造体の定義が必要になることが多々ありました。

どこまで簡単になるのかは分かりませんが、Web開発においてORMの強さはかなり重要だと思うので、もっと進歩すると嬉しいなと感じました。

(自分的に)テストが書きやすいとは言えなかった

これもRustが悪いというより、Web開発におけるエコシステムがまだ充実していないということだと感じました。
DBまで繋げて統合テストを書きたい時に、マイグレーションの実行やテストデータの投入など、処理を挟むみたいなことがあまりできないイメージです。

例えばTypeScriptで開発した際は、Jestを使って色々なテストを簡単に書くことができました。
このあたりは今後に期待したいです。

まとめ

今回Rustを書いてみて、自分はかなり好きな部類に入ったので、今後もっともっと人気になっていくと嬉しいなと思います。
そのためにも、この記事を参考にしてみなさんがWeb開発でもRustをどんどん採用してくれると嬉しいです。

GitHubで編集を提案
PrAha

Discussion

kbwokbwo

(以下で言うORMはいわゆるActiveRecord的なものを指します)
ORMについて、多様性に欠けるという点ではエコシステムが充実していないというのはその通りだと思いますが、発展が遅く未熟というよりは、Rust開発者がORMを好まない傾向にあるためORM開発のモチベーションが比較的少ないというのもあるのではないかなと思っています。
明確なエビデンスがあるわけではないですが、Redditを観察しているとRustのORMやDB周りの話では毎回sqlx推しが集まっている印象があります。それもdieselが非同期処理をサポートしていないなどによる消去法的選択肢というよりは、ORMを使うことによる短期的メリットより、sqlxのような薄いながらもコンパイル時チェックなど本当に解決してほしい問題を解決してくれるものを推すようなコメントが多いように思います。
ただ、私もsqlx推しのためそのバイアスがかかっていますし、"dieselは使いたくないけど、かといってRustのエコシステムが未熟なことを認めたくない"だけのRust開発者もいるかもしれないので、上記の印象は眉唾ものですが、こういう視点もあるというコメントでした。

Kyosuke AwataKyosuke Awata

コメントありがとうございます!
Rust開発者の傾向まではチェックできていなかったので、大変参考になります!

鏡華鏡華

chronoはserdeのSerialize/Deserialize実装を提供しています
https://github.com/chronotope/chrono/blob/main/src/datetime/serde.rs

Rustは言語機能としてfeature-gateを内蔵しており、ライブラリなどにおいても使う側が必要な機能だけを有効にすることができます.
serdeでのシリアライズ機能は、chronoの利用者のすべてが必要としているわけではないが、あると便利、みたいなものですよね.
なのでデフォルトでは無効にしつつ、featureを指定した場合に有効になる、のように実装されています.

Cargo.tomlで

[dependencies]
chrono = { version = "0.4.23", features = ["serde"] }

などとすればSerialize/Deserialize出来るようになるはずです

Kyosuke AwataKyosuke Awata

ありがとうございます!
こちらのやり方の方が簡単なので、もっと簡単なやり方もあるよ!と追記しようと思います!

鏡華鏡華

可変参照を色々なところで使わざるを得ない状況になってしまった

これは内部可変性といわれるパターンで解消できます.
記事内に書かれているRefCellも内部可変性を実現するための構造体のうちの一つです.
RcやRefCellはSendやSyncを実装していない(スレッドを跨ぐマルチスレッド環境では安全に扱えない)ので、Webサーバーなどマルチスレッドな環境では安全に扱えるArc<Mutex<T>>などが頻出します.

magicantmagicant

ユースケースやドメインサービスにこのコネクションオブジェクトを渡す必要がありました。

普通、データベースへのコネクションは処理のスレッドごとに別々に用意するものじゃないでしょうか。複数の処理で同時に同じコネクションを無理やり使い回すと、あるトランザクションの途中で別のトランザクションの処理が混じるみたいなカオスな状況になりそうな気がします。

コネクションを毎回何度も接続したり切断したりするのは非効率なのでコネクションプールを使って管理するのが一般的かと思いますがいかがでしょう。

Kyosuke AwataKyosuke Awata

コメントありがとうございます!

普通、データベースへのコネクションは処理のスレッドごとに別々に用意するものじゃないでしょうか。複数の処理で同時に同じコネクションを無理やり使い回すと、あるトランザクションの途中で別のトランザクションの処理が混じるみたいなカオスな状況になりそうな気がします。

コネクションを毎回何度も接続したり切断したりするのは非効率なのでコネクションプールを使って管理するのが一般的かと思いますがいかがでしょう。

その通りだと思います!
そして今回もそのような作りになっています!

具体的には以下のような流れになります(詳細はサンプルコードを読んで頂けると分かりやすいかと思います)

  1. scenarioでコネクションプールからコネクションオブジェクトを取得
  2. scenarioからpresentationに定義されている関数を呼び出す
  3. presentationの関数では、ユースケースのインスタンスを生成する(必要ならリポジトリやドメインサービスを生成してユースケースに渡す。
  4. ...省略...

↑このリポジトリやドメインサービスを作る際に可変参照なコネクションオブジェクトが必要になります

magicantmagicant

返信ありがとうございます。そして本文のコードをうまく読み解けていなかったようですみません。

コネクションプールは既に使っておられて、コネクションプールからコネクションを取り出すまでは順調にできているのですね。それでいて

RcとRefCellを組み合わせて無理やり可変参照を複数の場所から参照できるようにしました

ということが必要だったということは、ユースケースやドメインサービスの中にコネクションの参照を保持しているということでしょうか。コネクションを保持するオブジェクトが複数あるのであれば一つのコネクションを共有するために Rc<RefCell> のパターンが必要になってくるのも腑に落ちます。

ただ自分ならオブジェクト内に保持するのではなくて必要になるたびに毎回関数の引数で渡す道を選びそうだなと思いました。その方がコンパイル時チェックに頼れる範囲が広いので。

白山風露白山風露

From トレイトの話をするなら Into トレイトの話を入れてほしかった感。 From トレイトの嬉しい点は From トレイトを実装すると Into トレイトが自動的に実装されて、 into() で気軽に型変換できることだと思うので、 X::from(y) の形で使用することだけだと本当になんかちょっと短くなっただけでX::from_y(y) を実装するのと大差なくなってしまう

Takaaki FuruseTakaaki Furuse

(マジレスでなく、ジョークとして読んでください。)

RobynというPython製のフレームワークがありまして・・・
これを使うという反則技もあります。
下ではPyO3というライブラリーを使ってAPIレベルからPythonコードをRustに変換してる感じです。

https://robyn.tech/

フラワーフラワー

突然の質問失礼致します。
気になったのですが、ユースケース層やドメイン層にDBコネクションオブジェクトを持たせる理由は何でしょうか?
基本的なDDDのプラクティスであれば、ドメインロジックとDBやFW等の詳細を分離するため、DBコネクションはインフラストラクチャ層で実装するものと理解しています。ユースケース層やドメイン層にDBコネクションを渡した場合、この分離ができなくなると思うのですが、もし何か理由があれば教えていただきたいです。(自分の理解が誤ってるかもしれないので、その場合は指摘いただけると幸いです。)

Kyosuke AwataKyosuke Awata

これは記事の内容が誤ってました、申し訳ありません。

正しくは「複数のリポジトリのコンストラクタにコネクションオブジェクトを渡す必要がある」でした。
ユースケースやドメインサービスは、生成されたリポジトリを受け取る形になりますね。

記事も併せて修正しました!ありがとうございます!
具体的なコードはこちらが分かりやすいかと思います。

フラワーフラワー

いえいえ、回答ありがとうございます!そして、返信遅くなり申し訳ありません。
DBコネクションをリポジトリに渡すということであれば、自分のDDDの理解とずれてないので納得できました。

余談ですが、ソースコード読ませていただきました。なるほど、複数の機能でDBコネクションオブジェクトを使いまわしたいから、それぞれの機能のリポジトリにDBコネクションを渡さなければならず、複数のリポジトリのコンストラクタにコネクションオブジェクトを渡す必要があるということですね。
それで、DBコネクションは可変参照だから shared xor mutableで複数オブジェクトで共有できないと。

確かにGCありの言語なら、DB接続はインスタンス変数に持たせて使いまわしたりすることが常套手段だと思いますが、Rustだとその辺りが厄介になりそうなところかもですね。
僕も勉強になりました。ありがとうございました。