diesel小ネタ集

κeenです。これはRust Advent Calendar 2017の記事です。 RustのORM、クエリビルダ、マイグレーションツールのdieselについて。 dieselについては公式チュートリアルを読めばだいたい使い始められるのですが、それだけでは足りないのでいくつか持ちネタを書きます。

diesel-1.0.0-beta1での情報です。

table! マクロ

チュートリアルにはinfer_schema!を使ったDSLの自動生成が書かれてますが、table!マクロを使って自分で書くこともできます。

table! {
  posts {
    id -> Integer,
    Title -> Text,
    body -> Text,
    published -> Bool
  }
}

このときの型はdieselで定義されているSQL型を書きます。 このSQL型は特定のDBMにあるものも用意されていて、例えばPostgreSQLならこの辺この辺にあります。

viewの扱い

dieselは基本的にはviewをサポートしてません。 もちろんinfer_schema!はviewは見てくれないのでどうしても使いたい場合は上記table!マクロで生成します。 実際にSQLを発行してDBMに怒られるまでtypoとかに気づかないのでつらいですね。

あとdieselの型的にはINSERTが書けてしまいます。もちろん実行時にはエラーになります。気をつけましょう。

NewType Pattern

0.99.0からQueryableがネスト可能になったので

#[derive(Queryable)]
pub struct PostId(pub i32);

#[derive(Queryable)]
pub struct Post {
    pub id: PostId,
    pub title: String,
    pub body: String,
    pub published: bool,
}

のようにidに個別の型を与えることができるようになりました。

しかしちょっと注意が必要です。

まずクエリがidのためにタプルを作らないといけなくなります。

    dsl::posts
        ...
        // idを`(dsl::id,)` のようにしていしないといけない
        .select(((dsl::id,), dsl::title, dsl::body, dsl::published))
        .get_result(conn)

また、find(id)のようにクエリビルダの引数に渡そうと思うともうちょっと工夫が必要です。 dieselのクエリビルダのDSLに混ぜるにはExpressionToSqlのトレイトを実装しないといけません。

とはいえ、どのidでも中身は同型なのでワンパターンです。これはマクロを用意すれば解決できて、

use diesel::types::{Integer, ToSql, ToSqlOutput, IsNull};
use diesel::backend::Backend;
use std::io::Write;
use std::error::Error;
macro_rules! derive_for_ids {
    ($ty: ty) => {
        // `Expression`系を自動定義。
        // このマクロはdieselで定義されている。
        expression_impls!(BigInt -> $ty);

        // `ToSql`を定義
        impl<DB: Backend> ToSql<BigInt, DB> for $ty {
            fn to_sql<W: Write>(
                &self,
                out: &mut ToSqlOutput<W, DB>,
            ) -> Result<IsNull, Box<Error + Send + Sync>> {
                ToSql::<BigInt, DB>::to_sql(&self.0, out)
            }
        }
    }
}

というマクロで一撃です。

このマクロは以下のように使います。

#[derive(Queryable, Debug)]
pub struct PostId(i32);
derive_for_ids!(PostId);

このようにしてあげれば

pub fn find_post<'a>(conn: &SqliteConnection, id: PostId) -> Option<Post> {
    use schema::posts::dsl;

    dsl::posts
        // findの引数に`PostId`の値を渡せる
        .find(id)
        .select(((dsl::id,), dsl::title, dsl::body, dsl::published))
        .get_result(conn)
        .optional()
        .expect("Error finding")
}

のようにPostIdのままDSLが使えます。

enum

dieselはSQLのenumをサポートしていません。なのでDB側ではIntegerで定義して、Rustのenumにマッピングしてあげることを考えます。

因みに、復習も兼ねて、Rustのenumは全ての列挙子が引数を取らなければ(いわゆるC-like enumであれば)

pub enum Visibility {
    Public = 0,
    Limited = 1,
    Private = 2,
}

のように数値を割り当てられますね(何も指定しなければ0始まりの連番です)。 そしてVisibility::Public as i32のように数値にキャスト可能です(逆はできません)。

これを用いて諸々のDSL化に使われているトレイトを実装します。先程と同じくExpressionToSqlQueryableですがderive(Queryable)が使えないのでちょっと大変です。

同じくワンパターンなのでマクロを定義してあげます。 enumの列挙子を全て捕捉しないといけないので先程とちょっと変わってenumの定義をラップする形にします。

use diesel::types::{Integer, ToSql, ToSqlOutput, IsNull, FromSql, FromSqlRow, HasSqlType};
use diesel::row::Row;
use diesel::Queryable;
use diesel::backend::Backend;
use std::io::Write;
use std::error::Error;

macro_rules! define_enum {
    ($(#[$meta:meta])*
     pub enum $name: ident { $($variant: ident = $val: expr,)*}) => {
        // 元のenumを定義
        $(#[$meta])*
        pub enum $name {
            $($variant = $val,)*
        }

        // `ToSql`を定義
        impl<DB: Backend> ToSql<Integer, DB> for $name {
            fn to_sql<W: Write>(
                &self,
                out: &mut ToSqlOutput<W, DB>,
            ) -> Result<IsNull, Box<Error + Send + Sync>> {
                ToSql::<Integer, DB>::to_sql(&(*self as i32), out)
            }
        }

        // `Expression`系を自動定義
        expression_impls!(Integer -> $name);

        // `Queryable`のために`FromSql`と`FromSqlRow`を定義
        impl<DB: Backend> FromSql<Integer, DB> for $name
            where i32: FromSql<Integer, DB>  {
            fn from_sql(bytes: Option<&DB::RawValue>) -> Result<Self, Box<Error + Send + Sync>> {
                use self::$name::*;
                match <i32 as FromSql<Integer, DB>>::from_sql(bytes)? {
                    $($val => Ok($variant),)*
                    s => Err(format!("invalid {} value: {}", stringify!($name), s).into()),
                }
            }
        }

        impl<DB: Backend> FromSqlRow<Integer, DB> for $name
            where i32: FromSql<Integer, DB> {
            fn build_from_row<T: Row<DB>>(row: &mut T) -> Result<Self, Box<Error + Send + Sync>> {
                use self::$name::*;
                match <i32 as FromSqlRow<Integer, DB>>::build_from_row(row)? {
                    $($val => Ok($variant),)*
                    s => Err(format!("invalid {} value: {}", stringify!($name), s).into()),
                }
            }
        }

        // `Queryable`を定義
        impl<DB> Queryable<Integer, DB> for $name
            where
            DB: Backend + HasSqlType<Integer>,
            $name: FromSqlRow<Integer, DB>,
        {
            type Row = Self;

            fn build(row: Self::Row) -> Self {
                row
            }
        }

    }
}

というマクロを用意してあげます。

このマクロは

define_enum! {
    #[derive(Debug, Clone, Copy)]
    pub enum Visibility {
        Public = 0,
        Limited = 1,
        Private = 2,
    }
}

のように使います。

そしたら以下のように構造体に含められますし、

#[derive(Queryable)]
pub struct Post {
    pub id: PostId,
    pub title: String,
    pub body: String,
    // `Visibility`が`Queryable`の中に
    pub published: Visibility,
}


#[derive(Insertable)]
#[table_name = "posts"]
pub struct NewPost<'a> {
    pub title: &'a str,
    pub body: &'a str,
    // `Visibility`が`Insertable`の中に
    pub visibility: Visibility,
}

あるいはクエリビルダの中で

    dsl::posts
        .find(id)
        // `visibility`をselect
        .select(((dsl::id,), dsl::title, dsl::body, dsl::visibility))
        .get_result(conn)

    let new_post = NewPost {
        title: title,
        body: body,
        visibility: Visibility::Private,
    };

    insert_into(posts::table)
        // `visibility`をinsert
        .values(&new_post)
        .execute(conn)

のように使えます。

終わりに

小ネタ集といいつつ3つくらいしか出てきませんでした。 まあ、その代わり内容が濃いめだったのでバランスが取れたかな?

また思いついたら随時投稿しようと思います。

Written by κeen
Later article
Rust & Swagger