😺

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

に公開
15
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だとその辺りが厄介になりそうなところかもですね。
僕も勉強になりました。ありがとうございました。