Diesel で derive(Queryable) するときの落とし穴
Rust で DB 操作するときに使う crate といえば,diesel が有名です.
さまざまな traits を駆使して型安全に操作できるのが特徴で,その辺りもすべてマクロに包んであるので,基本的な使い方であれば簡単に実装することができます.
反面マクロがブラックボックスとなっているため,踏み込んだ使い方をするとハマることがあります.
先日,"自作型を load しようとしてもコンパイルエラーになる" という沼にハマったので,原因と解決策を記しておこうと思います.
環境
- Ubuntu 24.04 LTS
- MySQL 8.0.41
- Diesel v2.2.9
[package]
name = "diesel-test"
version = "0.1.0"
edition = "2024"
[dependencies]
diesel = { version = "2.2.9", default-features = false, features = ["mysql"] }
問題のコード
次のようなテーブルがあるとします.記事の ID (article_id
) とユーザの ID (user_id
) を持つ,よくありふれたテーブルです.
// @generated automatically by Diesel CLI.
diesel::table! {
articles (article_id) {
article_id -> Unsigned<Bigint>,
user_id -> Unsigned<Bigint>,
}
}
対応する models.rs
はこのようになるはずです:
use diesel::{Insertable, Queryable};
#[derive(Debug, Queryable, Insertable)]
#[diesel(primary_key(article_id), table_name = crate::schema::articles)]
pub struct Article {
article_id: u64,
user_id: u64,
}
このとき,たとえば "article_id
のリストを取得したい" とすると,次のような関数を書くことになります:
use diesel::{MysqlConnection, QueryResult};
fn list_article_ids(conn: &mut MysqlConnection) -> QueryResult<Vec<u64>> {
use crate::schema::articles;
use diesel::{QueryDsl, RunQueryDsl};
articles::table.select(articles::article_id).load(conn)
}
ここまでは普通のシナリオです.
さて,2つの u64
を区別するために,tuple struct を使った方がより型安全です:
+ use diesel::expression::AsExpression;
+ use diesel::sql_types::{BigInt, Unsigned};
use diesel::{Insertable, Queryable};
+ #[derive(Debug, Queryable, AsExpression)]
+ #[diesel(sql_type = Unsigned<BigInt>)]
+ pub struct ArticleId(u64);
+
+ #[derive(Debug, Queryable, AsExpression)]
+ #[diesel(sql_type = Unsigned<BigInt>)]
+ pub struct UserId(u64);
+
#[derive(Debug, Queryable, Insertable)]
#[diesel(primary_key(article_id), table_name = crate::schema::articles)]
pub struct Article {
- article_id: u64,
+ article_id: ArticleId,
- user_id: u64,
+ user_id: UserId,
}
このとき,list_article_ids()
の返り値を u64
から ArticleId
へ変更すると,コンパイルエラーとなります:
+ use crate::models::ArticleId;
- fn list_article_ids(conn: &mut MysqlConnection) -> QueryResult<Vec<u64>> {
+ fn list_article_ids(conn: &mut MysqlConnection) -> QueryResult<Vec<ArticleId>> {
$ cargo build
--> src/client.rs:8:55
|
8 | articles::table.select(articles::article_id).load(conn)
| ---- ^^^^ the trait `Queryable<diesel::sql_types::Unsigned<BigInt>, Mysql>` is not implemented for `ArticleId`
| |
| required by a bound introduced by this call
|
= help: the trait `Queryable<diesel::sql_types::Unsigned<BigInt>, Mysql>` is not implemented for `ArticleId`
but trait `Queryable<(_,), Mysql>` is implemented for it
= help: for that trait implementation, expected `(_,)`, found `diesel::sql_types::Unsigned<BigInt>`
= note: required for `ArticleId` to implement `FromSqlRow<diesel::sql_types::Unsigned<BigInt>, Mysql>`
= note: required for `diesel::sql_types::Unsigned<BigInt>` to implement `load_dsl::private::CompatibleType<ArticleId, Mysql>`
= note: required for `SelectStatement<FromClause<table>, query_builder::select_clause::SelectClause<columns::article_id>>` to implement `LoadQuery<'_, MysqlConnection, ArticleId>`
原因
まず状況を整理しましょう.
// -> QueryResult<Vec<ArticleId>>
articles::table.select(articles::article_id).load::<ArticleId>(conn)
最後の load()
関数は RunQueryDsl
のメソッドです.
これは,
Self: LoadQuery<MysqlConnection, ArticleId>,
を要求しています.
次に LoadQuery
は blacket implementation によって実装されています.
実装の trait bound のうち U = ArticleId
に関係する部分のみ見てみると,次の通りになっています:
T::SqlType: CompatibleType<ArticleId, Mysql>,
ArticleId: FromSqlRow<<T::SqlType as CompatibleType<ArticleId, Mysql>>::SqlType, Mysql>,
CompatibleType
というのは private な trait ですが,実装を覗いてみると
ArticleId: FromSqlRow<ST, Mysql>
のときに ST: CompatibleType<ArticleId, Mysql, SqlType = ST>
となっていそうです.
したがって,結局
ArticleId: FromSqlRow<T::SqlType, Mysql>
という trait bound に簡約されます.
さて,FromSqlRow
は ArticleId: Queryable<T::SqlType, Mysql>
のときに実装されています.
今の場合 T::SqlType = Unsigned<BigInt>
でしょうから,
ArticleId: Queryable<Unsigned<BigInt>, Mysql>
が必要だということになります.
……上記のエラーメッセージと同じ結論ですね.
しかし,おかしなことがあります.ArticleId
は,たしかに derive(Queryable)
したはずです.
#[derive(Debug, Queryable, AsExpression)]
#[diesel(sql_type = Unsigned<BigInt>)]
pub struct ArticleId(u64);
なのになぜ,ArticleId
が Queryable<Unsigned<BigInt>>
を実装していない,というエラーになるのでしょうか.
derive(Queryable)
の中身を確認するために cargo expand
してみたら,すぐに答えが見つかりました.
use diesel::deserialize::{self, FromStaticSqlRow, Queryable};
use diesel::row::{Row as _, Field as _};
use std::convert::TryInto;
impl<__DB: diesel::backend::Backend, __ST0> Queryable<(__ST0,), __DB>
for ArticleId
where
(u64,): FromStaticSqlRow<(__ST0,), __DB>,
{
type Row = (u64,);
fn build(row: Self::Row) -> deserialize::Result<Self> {
Ok(Self { 0: row.0.try_into()? })
}
}
__ST0
の周りが謎に(おそらく実装上の都合で)カッコで囲われています.
Queryable<(__ST0,), __DB> for ArticleId
ということは,ArticleId
は Queryable<Unsigned<BigInt>>
ではなく Queryable<(Unsigned<BigInt>,)>
を実装しているのではないでしょうか.
試しに,list_article_ids()
の中身を修正してみます.
- articles::table.select(articles::article_id).load(conn)
+ articles::table.select((articles::article_id,)).load(conn)
$ cargo build
Compiling diesel-test v0.1.0 (...)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.55s
……コンパイルできました.
やはり,derive(Queryable)
が Queryable<ST, DB>
ではなく Queryable<(ST,), DB>
を実装していたことが原因だったみたいです.
解決策
1つ目の解決策は,すでに見たように select()
の引数を (*,)
で囲うことです.
use crate::models::ArticleId;
use diesel::{MysqlConnection, QueryResult};
fn list_article_ids(conn: &mut MysqlConnection) -> QueryResult<Vec<ArticleId>> {
use crate::schema::articles;
use diesel::{QueryDsl, RunQueryDsl};
articles::table.select((articles::article_id,)).load(conn)
}
しかし私はこのような方法は採りたくありません.本来は x
で良いところを無駄に (x,)
と書くところが,あまりにも "work-around 然" としているからです.
2つ目の解決策は自力で Queryable
を実装することです.
+ use diesel::backend::Backend;
- #[derive(Debug, Queryable, AsExpression)]
+ #[derive(Debug, AsExpression)]
#[diesel(sql_type = Unsigned<BigInt>)]
pub struct ArticleId(u64);
+ impl<ST, DB: Backend> Queryable<ST, DB> for ArticleId
+ where
+ u64: Queryable<ST, DB>,
+ {
+ type Row = <u64 as Queryable<ST, DB>>::Row;
+
+ fn build(row: Self::Row) -> diesel::deserialize::Result<Self> {
+ <u64 as Queryable<ST, DB>>::build(row).map(Self)
+ }
+ }
+
- #[derive(Debug, Queryable, AsExpression)]
+ #[derive(Debug, AsExpression)]
#[diesel(sql_type = Unsigned<BigInt>)]
pub struct UserId(u64);
+ impl<ST, DB: Backend> Queryable<ST, DB> for UserId
+ where
+ u64: Queryable<ST, DB>,
+ {
+ type Row = <u64 as Queryable<ST, DB>>::Row;
+
+ fn build(row: Self::Row) -> diesel::deserialize::Result<Self> {
+ <u64 as Queryable<ST, DB>>::build(row).map(Self)
+ }
+ }
use crate::models::ArticleId;
use diesel::{MysqlConnection, QueryResult};
fn list_article_ids(conn: &mut MysqlConnection) -> QueryResult<Vec<ArticleId>> {
use crate::schema::articles;
use diesel::{QueryDsl, RunQueryDsl};
articles::table.select(articles::article_id).load(conn)
}
ただしこちらはスケールしないのが欠点です.
他にもっとスマートな解決策はあるのでしょうか.
Diesel に詳しい方がいればぜひ.
おわりに
ということで,derive(Queryable)
したときにエラーで躓いたお話でした.
この挙動について明確に言及したドキュメントが見つからなかったので,誰かの助けになればなと思います.
あるいは issue 案件かもしれませんが,私はここまで辿り着くのに疲れたので,issue を建てる元気は残っていません.
誰か issue してくれるといいな.
Discussion