皆様こんにちは、株式会社プラハのAwataです。
今日は、以前書いたリーダーの振り返り記事で軽く触れていた、RustでのAPI開発についての記事を書いていこうと思います。
結論RustでWebは辛い!という話なんですが、約5か月くらいRustでWeb開発をしたので、今後の参考になるようなことを書いていこうと思います。
ぜひ最後までお付き合いください。
前例が少ない中、どうしてRustで開発しようと思ったのか気になる方も多いと思いますので、最初はRustを選択した理由について書いていこうと思います。
結論だけ先に伝えておくと、これはチャレンジです。
今回のアプリケーションにおいて、機能要件的にも非機能要件的にもRustは必要ありませんでした。
技術的な要件で言うと
といった程度で、言葉を選ばずに言うなら何の言語でも開発して運用ができたと思っています。
ただし、技術的要素以外に、以下のような背景がありました。
エンジニア採用や社外への情報公開の際に、目に留まりやすいような技術を使って欲しい!
この考え方に賛否両論(否が多めかもしれない)あることは承知していますが、現状の株式会社アガルートの技術スタックだと、求めているようなエンジニアを採用するのは難しいということがありました。
そのため、まずは目に留まるところから始めてみようという背景がありました。
また、新しい技術スタックを取り入れることで、以下のような相乗効果にも期待していました。
ここまでに書いたもろもろの背景を考慮すると、Rust, Kotlin, Goの3つが候補に上がりました。
ここでは簡単に比較してみましょう。
これらのメリットデメリットや、みんなのやりたい気持ちなど色々と考慮して議論しました。
また、ミニマムのアプリケーションを1週間くらいかけてをRustで開発してみて、「まあしんどいけど、なんとかはなりそう」という雰囲気が漂ってきました。
そして、最終的に以下のような結論になりました。
よし、Rustでやってみよう!無理ならTypeScriptで死ぬ気で作り直そう!
そしてここから果てしない旅が始まるのでした...。
辛かった話なんですが、辛い!辛い!と書いても誰も嬉しくないと思うので、その辛さを乗り越えるために工夫したところを紹介しようと思います。
これで辛さが2割くらい削減されたような気がします。
自分は統合テストは実行コストが高いため、単体テストとは別々で動かしたいと考えています。
そして、Rustには簡単に単体テストや統合テストを書ける仕組みが用意されています。
単体テストは同じファイルに気軽に書けて、統合テストも特殊な書き方を覚える必要もなく簡単に書けるため、とても良い仕組みだと思っています。
しかし、統合テストだけを動かすコマンドは自分の調べた限りでは用意されていませんでした。
そこで、フィーチャーフラグを使って、統合テストだけを動かせるようにしました。
手順は以下の通りです。
Cargo.toml に以下のようにフィーチャーフラグを定義する[package]# 省略[dependencies]# 省略[features]integration_test=[]#[cfg(feature ="integration_test")]modtest{// 省略}cargotest--features integration_test今回の設計では、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型を使ってエラーハンドリングを行います。
※パニックも存在しますが、これは原則回復不能なエラーに対して使うため、ハンドリングすることはあまりありません。
サンプルリポジトリのこちらに以下のようなコードがありますが、ここで?演算子を使わずに書くと以下のようになります。
pubasyncfnexecute( 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に変換されるように定義しておく必要があります。
そしてその変換処理のこちらにあるため、上記のコードは以下のようになります。
pubasyncfnexecute( 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構造体を定義しましょう
pubstructAuthUser{pub id:String,pub mail_address:String,pub role:String,}implAuthUser{pubfnis_admin(&self)->bool{self.role=="admin"}pubfnis_user(&self)->bool{self.role=="user"}}そしてFromRequestトレイトを実装します。
ここでヘッダーの中身を見て、送られてきたIDは本当に存在するのか?など色々なチェックをすることも可能です。
implFromRequestforAuthUser{typeError=ApiError;typeFuture=Pin<Box<dynFuture<Output=Result<Self,Self::Error>>>>;fnfrom_request(req:&HttpRequest, _payload:&mutPayload)->Self::Future{let request= req.clone();Box::pin(asyncmove{let role=match request.headers().get("auth-user-role"){Some(id)=> id.to_str(),None=>returnErr(ApiError::new(StatusCode::FORBIDDEN,"forbidden".to_owned())),}.unwrap();if role!="user"&& role!="admin"{returnErr(ApiError::new(StatusCode::FORBIDDEN,"forbidden".to_owned()));}let id=match request.headers().get("auth-user-id"){Some(id)=> id.to_str(),None=>returnErr(ApiError::new(StatusCode::FORBIDDEN,"forbidden".to_owned())),}.unwrap();let mail_address=match request.headers().get("auth-user-email"){Some(id)=> id.to_str(),None=>returnErr(ApiError::new(StatusCode::FORBIDDEN,"forbidden".to_owned())),}.unwrap();Ok(AuthUser{ id: id.to_string(), mail_address: mail_address.to_string(), role: role.to_string(),})})}fnextract(req:&HttpRequest)->Self::Future{Self::from_request(req,&mutPayload::None)}}そして、このAuthUserをscnearioに定義されている関数の引数に設定します。
これによってactix-webが自動的にAuthUserを取得してくれるようになり、FromRequestの変換中にエラーが起きた際は自動的にエラーレスポンスが返ります。
pubasyncfnexecute(req_user:AuthUser)->Result<HttpResponse,ApiError>{// 省略}本番用のコードでは、AuthUser以外にAdminUser,NormalUserのように更に権限に応じた構造体を定義することで、権限に応じたアクセス制限が簡単にできるようにしてあります。
サンプルリポジトリではコードが増えすぎるし(ちょっと疲れてきてた)ので省略しましたが、もし気になるけどやり方が分からない!という方はコメントで教えてください!
(元気があれば追記します)
scenario層は複数のモジュールのpresentation層に定義されてある関数を呼び出して処理を進める層です。
複数のpresentation層の関数を呼んでも良いですし、1つだけでも良いです。
そして、必要であればトランザクションの管理も行います。
ここまで読んでいて勘の良い人は、なんかこれ何かの役割と似てるな?と思ったかもしれませんが、scenario層はマイクロサービスにおけるSagaパターンのオーケストレーター的な役割を果たします。
これ以外にも、**ルーティングをどうやって一元で管理するか?**という問題もscenario層は解決してくれます。scenario層が存在しない場合、各モジュールでルーティングを定義する形式になってしまい、他のモジュールでそのルーティングが使われていないかを都度確認しなくてはならなくなります。
そのため、例えpresentation層の関数を呼ぶだけであっても必ずscenario層を用意して、「HTTPエンドポイントと紐づけるのはscenario層の関数だけ」というルールで運用しています。
続いてRustの良かったところです。
良かったところも多くあったんですが、それでもやっぱり辛いということは、つまりそういうことなのです。
あまり細かく書くとこれだけで1つの記事になってしまいそうなので、箇条書きで良かったと感じたところを書いてみます。
privateなので、知らず知らず公開してしまっているものがないpub: 外部クレートにも公開pub(super): 親モジュールには公開pub(crate): 現在のクレート内には公開もっと詳しく知りたい方はまずこちらを読むことをおすすめします。
これは自分の経験が浅いからかもしれないのですが、過去に使っていたクラスベースの言語では、以下のすべてが同じクラスに書かれることが多かったです
この状態になると、まずプロパティを上の方に書いて、次に独自のメソッドを書いて、最後にインターフェースを継承したメソッドを書く、みたいな暗黙の了解が生まれることが多かったです。
そのため、コードに手を入れる時も若干気を使ったり、、、という感じでした。
しかし、Rustではこれらを全て別々に書くことができます。
具体的なコードはこんな感じですね。
// 構造体の定義pubstructUserId{pub value:String,}// 独自の振る舞いの定義implUserId{pubfnrestore(value:String)->Self{let value=Ulid::from_string(&value).unwrap().to_string();Self{ value}}}// トレイトの実装implDefaultforUserId{fndefault()->Self{let value=Ulid::new().to_string();Self{ value}}}// トレイトの実装implTryFrom<String>forUserId{typeError=String;fntry_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つのリポジトリに渡されていることになり、コンパイルエラーになります。
そのため、今回は苦肉の策として、RcとRefCellを組み合わせて無理やり可変参照を複数の場所から参照できるようにしました。
ただしこの書き方をした場合、実行時までエラーに気づけない可能性があります。
これまではコンパイラが「可変参照を複数のところで使っているよ!直してね!」と怒ってくれていましたが、この書き方だとコンパイラは何も言ってくれません。
しかし、実装が間違っていて複数の場所から可変参照が使われた場合、実行時エラーとなってしまいます。
今回はこれ以外の解決方法を出せなかったのでこの書き方を採用しましたが、もし他に良い方法があれば教えていただきたいです。
例えばJavaScriptでJSON文字列化したい場合、JSON.stringifyを使えば簡単にできます。
プロパティにDateオブジェクトがあったとしても、特に意識することなく使えますね。
しかし、Rustはそう簡単には行きません。
具体的にどういう手順を踏んだかを書いてみます。
今回はserdeというクレートを使ってJSON化の処理を実装しました。
[package]# 省略[dependencies]# 省略serde={version="1.0",features=["derive"]}serde_json="1.0"今回は、scenario層で使われているレスポンスの構造体を例に挙げてみます。
pubstructUserScenarioResponse{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>,}Serializeトレイトを実装する今回はderiveマクロを使って、自動生成してみましょう。
独自のシリアライズ処理を実装したい場合は、deriveマクロを使わずに自分で実装することもできます。
(試しに一度やってみましたが、まあまあ大変だったので自分はもうやりたくないですw)
#[derive(Serialize)]pubstructUserScenarioResponse{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>,}DateTime<Utc>は以下のようにフォーマットを指定する必要があるこちらのコメントでもっと簡単なやり方を教えて頂きました!
そのため、以下に記載されているやり方は冗長な方法になりますのでご注意ください!
今回はDateTimeがエラーを起こしていましたが、他の型でも同様のエラーが出る可能性があります。
また、構造体のプロパティに構造体があった場合、その構造体もSerializeトレイトを実装する必要があります。
pubmoddate_format{usechrono::{DateTime,TimeZone,Utc};useserde::{self,Deserialize,Deserializer,Serializer};constFORMAT:&str="%Y-%m-%d %H:%M:%S";pubfnserialize<S>(date:&DateTime<Utc>, serializer:S)->Result<S::Ok,S::Error>whereS:Serializer,{let s=format!("{}", date.format(FORMAT)); serializer.serialize_str(&s)}pubfndeserialize<'de,D>(deserializer:D)->Result<DateTime<Utc>,D::Error>whereD:Deserializer<'de>,{let s=String::deserialize(deserializer)?;Utc.datetime_from_str(&s,FORMAT).map_err(serde::de::Error::custom)}}#[derive(Serialize)]pubstructUserScenarioResponse{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>,}これでやっとシリアライズができるようになりました!
一度実装してしまえば他の構造体にも流用できるのですが、やはり面倒だなと感じました。
今回はdiesel,sea-orm,sqlxあたりを検討して、情報量の多さや実績のあるdieselを採用しました。
他のORMを細かく試した訳ではないのですが、やはりエコシステムの弱さは感じました。
RailsのActiveRecordまでとは言いませんが、もう少し使いやすいORMがあればいいなと思いました。
Rustの言語仕様上仕方ない部分があるとは思いますが、やはりコード量が多くなったり大量の構造体の定義が必要になることが多々ありました。
どこまで簡単になるのかは分かりませんが、Web開発においてORMの強さはかなり重要だと思うので、もっと進歩すると嬉しいなと感じました。
これもRustが悪いというより、Web開発におけるエコシステムがまだ充実していないということだと感じました。
DBまで繋げて統合テストを書きたい時に、マイグレーションの実行やテストデータの投入など、処理を挟むみたいなことがあまりできないイメージです。
例えばTypeScriptで開発した際は、Jestを使って色々なテストを簡単に書くことができました。
このあたりは今後に期待したいです。
今回Rustを書いてみて、自分はかなり好きな部類に入ったので、今後もっともっと人気になっていくと嬉しいなと思います。
そのためにも、この記事を参考にしてみなさんがWeb開発でもRustをどんどん採用してくれると嬉しいです。
\ PrAha Challenge 第11期生 募集中!/
☆☆☆
\ エンジニア募集中! /
バッジを受け取った著者にはZennから現金やAmazonギフトカードが還元されます。
(以下で言うORMはいわゆるActiveRecord的なものを指します)
ORMについて、多様性に欠けるという点ではエコシステムが充実していないというのはその通りだと思いますが、発展が遅く未熟というよりは、Rust開発者がORMを好まない傾向にあるためORM開発のモチベーションが比較的少ないというのもあるのではないかなと思っています。
明確なエビデンスがあるわけではないですが、Redditを観察しているとRustのORMやDB周りの話では毎回sqlx推しが集まっている印象があります。それもdieselが非同期処理をサポートしていないなどによる消去法的選択肢というよりは、ORMを使うことによる短期的メリットより、sqlxのような薄いながらもコンパイル時チェックなど本当に解決してほしい問題を解決してくれるものを推すようなコメントが多いように思います。
ただ、私もsqlx推しのためそのバイアスがかかっていますし、"dieselは使いたくないけど、かといってRustのエコシステムが未熟なことを認めたくない"だけの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出来るようになるはずです
可変参照を色々なところで使わざるを得ない状況になってしまった
これは内部可変性といわれるパターンで解消できます.
記事内に書かれているRefCellも内部可変性を実現するための構造体のうちの一つです.
RcやRefCellはSendやSyncを実装していない(スレッドを跨ぐマルチスレッド環境では安全に扱えない)ので、Webサーバーなどマルチスレッドな環境では安全に扱えるArc<Mutex<T>>などが頻出します.
ユースケースやドメインサービスにこのコネクションオブジェクトを渡す必要がありました。
普通、データベースへのコネクションは処理のスレッドごとに別々に用意するものじゃないでしょうか。複数の処理で同時に同じコネクションを無理やり使い回すと、あるトランザクションの途中で別のトランザクションの処理が混じるみたいなカオスな状況になりそうな気がします。
コネクションを毎回何度も接続したり切断したりするのは非効率なのでコネクションプールを使って管理するのが一般的かと思いますがいかがでしょう。
コメントありがとうございます!
普通、データベースへのコネクションは処理のスレッドごとに別々に用意するものじゃないでしょうか。複数の処理で同時に同じコネクションを無理やり使い回すと、あるトランザクションの途中で別のトランザクションの処理が混じるみたいなカオスな状況になりそうな気がします。
コネクションを毎回何度も接続したり切断したりするのは非効率なのでコネクションプールを使って管理するのが一般的かと思いますがいかがでしょう。
その通りだと思います!
そして今回もそのような作りになっています!
具体的には以下のような流れになります(詳細はサンプルコードを読んで頂けると分かりやすいかと思います)
scenarioでコネクションプールからコネクションオブジェクトを取得scenarioからpresentationに定義されている関数を呼び出すpresentationの関数では、ユースケースのインスタンスを生成する(必要ならリポジトリやドメインサービスを生成してユースケースに渡す。↑このリポジトリやドメインサービスを作る際に可変参照なコネクションオブジェクトが必要になります
返信ありがとうございます。そして本文のコードをうまく読み解けていなかったようですみません。
コネクションプールは既に使っておられて、コネクションプールからコネクションを取り出すまでは順調にできているのですね。それでいて
RcとRefCellを組み合わせて無理やり可変参照を複数の場所から参照できるようにしました
ということが必要だったということは、ユースケースやドメインサービスの中にコネクションの参照を保持しているということでしょうか。コネクションを保持するオブジェクトが複数あるのであれば一つのコネクションを共有するために Rc<RefCell> のパターンが必要になってくるのも腑に落ちます。
ただ自分ならオブジェクト内に保持するのではなくて必要になるたびに毎回関数の引数で渡す道を選びそうだなと思いました。その方がコンパイル時チェックに頼れる範囲が広いので。
From トレイトの話をするならInto トレイトの話を入れてほしかった感。From トレイトの嬉しい点はFrom トレイトを実装するとInto トレイトが自動的に実装されて、into() で気軽に型変換できることだと思うので、X::from(y) の形で使用することだけだと本当になんかちょっと短くなっただけでX::from_y(y) を実装するのと大差なくなってしまう
(マジレスでなく、ジョークとして読んでください。)
RobynというPython製のフレームワークがありまして・・・
これを使うという反則技もあります。
下ではPyO3というライブラリーを使ってAPIレベルからPythonコードをRustに変換してる感じです。
突然の質問失礼致します。
気になったのですが、ユースケース層やドメイン層にDBコネクションオブジェクトを持たせる理由は何でしょうか?
基本的なDDDのプラクティスであれば、ドメインロジックとDBやFW等の詳細を分離するため、DBコネクションはインフラストラクチャ層で実装するものと理解しています。ユースケース層やドメイン層にDBコネクションを渡した場合、この分離ができなくなると思うのですが、もし何か理由があれば教えていただきたいです。(自分の理解が誤ってるかもしれないので、その場合は指摘いただけると幸いです。)
これは記事の内容が誤ってました、申し訳ありません。
正しくは「複数のリポジトリのコンストラクタにコネクションオブジェクトを渡す必要がある」でした。
ユースケースやドメインサービスは、生成されたリポジトリを受け取る形になりますね。
記事も併せて修正しました!ありがとうございます!
具体的なコードはこちらが分かりやすいかと思います。
いえいえ、回答ありがとうございます!そして、返信遅くなり申し訳ありません。
DBコネクションをリポジトリに渡すということであれば、自分のDDDの理解とずれてないので納得できました。
余談ですが、ソースコード読ませていただきました。なるほど、複数の機能でDBコネクションオブジェクトを使いまわしたいから、それぞれの機能のリポジトリにDBコネクションを渡さなければならず、複数のリポジトリのコンストラクタにコネクションオブジェクトを渡す必要があるということですね。
それで、DBコネクションは可変参照だから shared xor mutableで複数オブジェクトで共有できないと。
確かにGCありの言語なら、DB接続はインスタンス変数に持たせて使いまわしたりすることが常套手段だと思いますが、Rustだとその辺りが厄介になりそうなところかもですね。
僕も勉強になりました。ありがとうございました。