Diesel 2.x への migration の記録
皆さんこんにちは。
最近はカービィのエアライダーばかりプレイしている @shnsprk です。
22 年振りのエアライドにワクワクしています。好きなライダーはスターマンです。
自分の所属している会社では 2017 年から Rust を使っています。
さて、Rust で書かれたマイクロサービスはいくつかあるのですが、ORM として使っている Diesel では 2.0 にて 破壊的変更(breaking change)がありました。
ORM の変更のためコード改修箇所が多く必要でしたが、先日、すべてのマイクロサービスで 2.x 系にアップグレード完了したので、備忘録も兼ねて記事を残しておきます。
なお、バージョンは 1.4 から 2.3 まで一気に上げています。
移行のモチベーション
Diesel 1.4 起因の Warning
Diesel 1.4 系 はそれなりに古いバージョンでしたので、Rust 自体のバージョンアップをしたり、edition を上げたりすると、大量の Warning を出すようになってきました。そのままではもちろん開発のノイズになってしまうので、早く上げる必要がありました。
また、一部の Warning は将来的には Error となるものも含まれており、その観点でも早めに対応が必要でした。
古い内部 crate が残り続けてしまう
Diesel が古いため、Diesel の内部 crate のバージョンも当然古いままとなっており、これらがいつまでも Cargo.lock に含まれている状態でした。
また、これらの古い crate が原因の Warning も発生してしまっていました。
Diesel 1.4 系のためだけに複数バージョンの crate が含まれてしまっているケースも多くありましたので、これらを一掃したいという気持ちがありました。
参考とすべきドキュメント
公式のマイグレーションガイドがあります。
基本はこちらを参考に作業を進めます。
書き換えが必要となる箇所
概ね以下の箇所の変更が必要でした。
(実コード次第では書き換えが不要のケースもあります。)
- connection の可変参照(mut)への変更
- Derive attributes の変更
- diesel_migration
- null の扱いの一部変更
- カスタム SQL タイプの実装変更
- ToSql, FromSql の実装変更
- DataLoader の書き換え
いくつかの変更について以降でコメントしていきます。
公式のマイグレーションガイドを見るだけで済む項目は、こちらでは省略します。
connection の可変参照(mut)への変更
Diesel2.x では、connection がすべて可変参照へと変更になりました。
そのため、下記のように connection を引き回す箇所はすべて mut や &mut に書き換えが必要です。
- let connection = PgConnection::establish_connection("…")?;
- let result = some_query.load(&connection)?;
+ let mut connection = PgConnection::establish_connection("…")?;
+ let result = some_query.load(&mut connection)?;
変更自体はシンプルですが、書き換え箇所の数が膨大でした。
移行作業前半では、今ほど AI agent で優秀でなかったのでスムーズに書き換えできず、正規表現での置換や、Cursor の Tab, Tab, Tab で補完、ジャンプを繰り返して書き換えていました。
移行作業後半では AI agent に丸投げでほぼ対応できました。これから移行するという人はかなり楽になったことと思われます。
Derive attributes の書き換え
#[derive(Debug, PartialEq, Eq, Validate)]
-#[table_name = "articles"]
+#[diesel(table_name = articles)]
pub struct Article {
pub id: i64,
今なら AI agent に書き換えを丸投げすればいいので簡単ですね。
(移行作業当初は正規表現で置換処理しました。)
diesel_migration
sqldef を使っていたため、こちらの移行作業は不要でした。
スキーマ管理に sqldef を使っていれば、MySQL や PostgreSQL などの複数の SQL を使っている環境でもほぼ同じ書きっぷりで管理できるのでオススメです。
クエリの書き換え
deprecated になった any() の書き換えのほか、ne_all() などの一部のメソッドは、発行される SQL が変わっており、SQL のパフォーマンスに影響が出ている箇所がありました。
これらは explain や debug_query を利用して、一つずつ解消させていきました。
DataLoader の書き換え
Diesel の他に、Actix Web と async-graphql を組み合わせて API サーバーを構成していました。
N+1 問題に対応するためには、DataLoader でまとめて取得できるようにする必要があります。
この辺りも mutable な connection を使うように書き換えました。
Optimizing N+1 queries - Async-graphql Book
pub struct UserLoader {
context: AppContext
}
impl Loader<User> for UserLoader {
type Value = User;
type Error = Error;
async fn load(
&self,
keys: &[i64],
) -> Result<HashMap<i64, Self::Value>, Self::Error> {
let conn = &mut self.context.db_con().borrow_mut();
let users = User::find_by_ids(conn, keys)?;
Ok(users
.into_iter()
.map(|x| (x.id, x))
.collect())
}
}
こんな感じで Connection を引き回すように書き換えました。
その他
他、細々した Error や Warning がありましたが、Rust のコンパイラが出力したヘルプなどを参考に修正し、無事に migration を完了させることができました。
昔に比べて Rust コンパイラのエラーメッセージが親切になったので、修正内容がイメージしやすくなりましたね。
コンパイルエラーが解消されていれば動作はほぼ保証されるところが Rust を使っていてありがたいところです。
migration 計画の策定と、掛かったコスト
計画の策定
Rust + Diesel なサービスとしては以下のようなサービスが稼働していました。
A. REST API のサービス
B. GraphQL(juniper, juniper-eager-loading) を利用しているサービス
C. GraphQL(async-graphql) のサービス
先述のとおり、古くから Rust を利用していたため、現在では開発が停止してしまった crate も数多くあります。
juniper-eager-loading もその一つで、当時は N+1 問題を解決させるために作られた crate でしたが、後に juniper 自体に DataLoader の仕組みが実装されたため開発が停止されました。
そして juniper-eager-loading は Diesel 2.x とは共存できないため、まずは、juniper-eager-loading を外すところから始める必要がありました。
GraphQL のライブラリとして、juniper, async-graphql いずれも利用実績がありましたが、後発の async-graphql の方が書きやすかったのもあり、juniper-eager-loading を外しつつ、 async-graphql へ統一することにしました。
最終的に、コード量なども鑑みて、下記の順序で移行することにしました。
- B. サービスの juniper, juniper-eager-loading を辞め、 async-graphql へ統一する
- C. サービスを Diesel 2.x へアップグレードする
- A. サービスを Diesel 2.x へアップグレードする
- B. サービスを Diesel 2.x へアップグレードする
掛かったコスト
1 つ目の Diesel 2.x 移行作業は かなり大変でした。 Rust のコンパイルエラーの数が膨大過ぎて、AI も活用しながら進めたにも関わらず、書き換える箇所は実コード・テストケースもそれなりに多く、1 ヶ月ほど掛かりました。
書き換えるものはある程度似通っていたので、この時に Cursor を使い始めましたのを覚えています。
一度移行できてしまったら、他のサービスは大体同じような書き換えで完了した割とカンタンでした。
今年は Cursor のアップグレードが多く、なにかと便利になっていったので書き換えもかなり楽になり、最後のサービスの移行は特にスムーズでした。
全体としては 1 人で 2 ヶ月半ほどで移行完了できました。
〆
移行作業開始前までは、移行はしたいものの大変でなかなか腰が重い...という状態でしたが、サービス単位で少しずつ移行を進め、無事に移行完了することができました。
古いバージョンのまま使い続けてしまうことで、改修箇所が増えていってしまいより移行コストが高まっていく......ということを痛感したので、このようなケースではなるべく早めに移行に着手すると良いと感じました。
この記事が何らかの参考になれば幸いです。
それではよい Rust ライフを!
Discussion