Rustでもメタプログラミングでto_stringしたい

はじめに

Rustでは#[derive(Debug)]すれば勝手にDebugトレイトが生えますが、まぁ一回くらいは自分で実装してみてもいいんでね?という事で実装します。

C#Scalaでは実行時に型情報が手に入るので、その型情報を使用してインスタンスに対してリフレクションを介してフィールドから情報を抜きます。

しかし、Rustはコンパイル後はマシン語になってしまうため、手続き型マクロを使用してコンパイルプロセスの途中に介入してコードをあれこれ生成します。

github.com

ワークスペース構成

今回は以下のようなワークスペース構成となっています。

  • tostring
    • 今回実装するマクロを呼び出しているアプリケーションクレート
  • tostring_macro
    • マクロを定義するクレート
    • 実際は tostring_macro_internals の実装を呼び出しているだけ
  • tostring_macro_internals
    • マクロを実装しているクレート

Rustの手続きマクロを実装するクレートはCargo.tomlに以下のような記述をするのですが、そうするとコンパイル時にしか呼べなくなってしまうという制約があるらしいので、マクロの宣言と実装を分けるのがベストプラクティスっぽいです。

[lib]
proc-macro = true

tostring_macro

手続き型マクロを宣言します。以上です

#[proc_macro_derive(ToString)]
pub fn derive(input: TokenStream) -> TokenStream {
    // マクロの実装を呼び出すだけ
    // proc_macro から proc_macro2 の TokenStream に変換する
    tostring_macro_internals::derive(input.into()).into()
}

tostring_macro_internals

マクロを実装します。

synquoteproc-macro2は手続き型マクロの三種の神器らしいのでとりあえず入れておきましょう。

[dependencies]
syn = { version = "1.0", features = ["full"] }
quote = { version = "1.0" }
proc-macro2 = { version = "1.0" }

また、proc_macroクレートはマクロクレート内でしか使用できないため、ここでのTokenStreamproc-macro2のものを使用しています。

TokenStremは文字通りトークン列であって、javaagentのようにASTが降ってくるわけではないのでsyn::parse2でパースするのが一番手っ取り早いです。

let input: DeriveInput = syn::parse2(input).unwrap();

あとはフィールド定義をいい感じに取得して

let src_fields;
if let syn::Data::Struct(syn::DataStruct { fields, .. }) = input.data {
    src_fields = fields;
} else {
    return error(input.ident.span(), "Currently you can just derive CustomDebug on structs").into();
}

構造体名を取得して、

let src_ident = input.ident;
let src_ident_str = src_ident.to_string();

フォーマッタで出力する際のメソッド呼び出しを生成して、

let formatter_fn = match &src_fields {
    Fields::Named(_) => {
        quote! { debug_struct( #src_ident_str ) }
    }
    Fields::Unnamed(_) => {
        quote! { debug_tuple( #src_ident_str ) }
    }
    Fields::Unit => {
        quote! { debug_struct( #src_ident_str ) }
    }
};

各フィールドを出力するためのコードを生成して、

let mut formatter_field_args = vec![];
let pattern = "{:?}";

for (i, field) in src_fields.iter().enumerate() {
    let field_ident = &field.ident;

    if let Some(ident) = field_ident {
        let ident_str = (*ident).to_string();
        formatter_field_args.push(quote! { #ident_str, &format_args!( #pattern , &self.#ident ) });
    } else {
        let i = proc_macro2::Literal::usize_unsuffixed(i);
        formatter_field_args.push(quote! { &self.#i });
    }
}

トレイト全体を生成するコードを吐き出したら完成です。

(quote! {
    impl ::std::fmt::Debug for #src_ident {
        fn fmt(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
            formatter.#formatter_fn
                #(  .field(#formatter_field_args)   )*
                .finish()
        }
    }
}).into()

あとはアプリケーションコードの構造体に#[derive(ToString)]を貼り付けたら完成です。

#[derive(ToString)]
pub struct Struct {
    i: i32,
}

#[derive(ToString)]
pub struct Hoge(i32);

ツールチェインがnightlyであれば以下のコマンドでマクロが展開後のコードが表示できます。

cargo rustc -- -Z unstable-options -Z unpretty=expanded -Z macro-backtrace
pub struct Struct {
    i: i32,
}
impl ::std::fmt::Debug for Struct {
    fn fmt(&self, formatter: &mut ::std::fmt::Formatter)
        -> ::std::fmt::Result {
        formatter.debug_struct("Struct").field("i",
                &::core::fmt::Arguments::new_v1(&[""],
                        &[::core::fmt::ArgumentV1::new_debug(&&self.i)])).finish()
    }
}

pub struct Hoge(i32);
impl ::std::fmt::Debug for Hoge {
    fn fmt(&self, formatter: &mut ::std::fmt::Formatter)
        -> ::std::fmt::Result {
        formatter.debug_tuple("Hoge").field(&self.0).finish()
    }
}

おわりに

Rustの手続き型マクロはどちらかというとCodeDOMやIL Generatorというよりテンプレートを使用してコードを生成する方法に近いので、C#の実行時メタプログラミングに慣れている人からすると微妙にやりずらいかもしれません。

ただ、入力は別にRustのコードに限らなくてもいいのでアイデアと気力があればいろいろ出来そうなので夢が広がりますね。

おわり

Entity Frameworkを使ったプログラマの末路・・・

はじめに

最近取り組んでいたEntity Framework Coreを使っているプロジェクトがひと段落したので、今後EFを使おうか検討している人向けに1つの事例として残しておこうと思います。

結論としてはキャッチーなタイトルとは裏腹に、今回のプロジェクトではメリットがデメリットを上回ったケースとなりました。

プロジェクトの概要

もともとはOracleに格納していたデータをPostgreSQLに移行に移行する際に、そのデータアクセス部分を書き直した感じになります。 そこのデータアクセスにEFを採用したイメージです。

採用した技術スタックはざっくりと以下の感じです。

また、データベースの規模感は以下の通りで、そんなに大きくはありません。 データも多いテーブルでも100万行程度でした。

  • テーブル:15程度
  • カラム:200程度

採用に至った経緯とか

概要でも書いていますが、今回のプロジェクトのデータベースは既存データベースの移植になります。

最終的には上の通りのデータベース規模になりましたが、オリジナルはテーブルは倍の30テーブル、カラムに至っては1000を超えるご機嫌な仕様となっていました。 また、期間もあまり長くなく手作業でクエリとDapperからデータを受けるクラスを書いていると間に合わなくなる可能性がありました。

そこで、オリジナルであるOracleのデータベースのテーブル定義からPostgreSQLのテーブル・ビューのDDLC#側のクラス定義を生成し、クエリはEFを使用して錬成するというアプローチを取るようにしたのが採用の経緯です。

もちろんEFなりO/Rマッパーなりの懸念はありましたが、克服できる or 影響を無視できるという算段のあっての採用です。

  • 複雑な操作をするとすぐに異形なSQLが錬成される
    • 予備調査でプロジェクトで使用するクエリのほとんどが非常に単純な操作で済むという見立てがあった
    • 少しでも複雑な処理はすべて生のSQLを書く覚悟を持つ
  • EFを使うと遅くなる
    • そもそもそこまで処理時間がクリティカルに効いてくる処理ではない
    • フルスキャンが走ってないかなどはデータベースの統計情報を確認してヤバそうなクエリは早めに何とかする

また、今回は新規でコードベースを起こすプロジェクトだったため、厄介なN+1問題を回避するためにLazy Loadingは全面的に禁止にしました。

得られた知見とか

DbContextは投げ捨てるもの

ネット上のサンプルなどは分かりやすさのために単一のDbContextインスタンスを使用して操作する例が多いですが、基本的にはDbContextは使い捨てたほうがいいです。

DbContextは変更のトラッキングのために内部にクエリ結果のキャッシュを保持します。そのため、一度結果セットを取得してしまうとソート順を変えて再取得しようとしても前回の結果をそのまま返してきます。 キャッシュの破棄なども出来るみたいですが、そんなことをするくらいなら処理毎にDbContextを作り直したほうが変な事故を起こしずらいです。(1敗)

セッションなどの単位でコネクションやトランザクションを生成して、そのコネクションを使用してEFのDbContextを生成・使用するイメージですね。 そのため、個人的にはEFを使うならIoCコンテナはほぼ必須だと思います。

EF(O/Rマッパ)はSQLが書けない人のためのフレームワークではない

この辺なんかはちょっと調べると死ぬほど記事が出てきますが、まさにその通りだと思います。

あくまでEFはC#LINQにのっとってクエリを生成しているだけで、生成されたクエリそのものの品質はプログラマが担保しなくてはいけません。 かつ、EFは割とカジュアルに異形クエリを錬成してくださりやがるのでカジュアルに死ねます。

そのため、生成されたクエリがインデックスを使用するかなどの『好ましい』クエリか否かを判断できるくらいのスキルは必要かもしれません。

EFでは基本的に頑張らない

上で書いている内容とだいぶ重複しますが、EFでは頑張れば頑張るほど異形クエリを吐き出してくれるため、基本的に単純なCRUDにとどめる勇気が必要です。

そのうえで、ちょっと凝ったことをしたいのであれば躊躇せずに生SQLを書いたほうが後々幸せになれるかもしれません。

もちろん、全部LINQ経由で書けば固有の製品に依存しないコードが書けるという意見もあるかと思います。 じゃあプログラムさえ対応させれば明日そのOracleをPostgresに変換出来んの?と聞かれた場合、『Yes』と答えられる環境は多くは無いでしょう。*1

ウェッブ系ならともかく生産性アプリケーションの場合はプログラムよりもデータ(ベース)のほうが寿命が長いんだからそんなこと考えてもしょうがなくない?というお気持ちです。

生成されたSQLの確認は必須

前提としてEF側のクエリログの設定は必須です。じゃないと生成されたSQLの確認が出来ませんし、本番に乗っかってからパフォーマンスに問題が出るとつらみポイントが高いです。

ログに吐かれたSQLexplain analyzeをくっつけて実行計画を見ておくのは無駄ではないと思います。

また、PostgreSQL側のpg_statio_*系のビューを見て、フルスキャンが走っていないかも確認しておくとより確実です。

まとめ

別にEFの機能を全部使いこなさないといけないというわけではないので、自分の欲しい機能を自分のコントロールできる範囲でつまみ食いする程度でも別に問題ないと思います。

あと、生成されたSQLは絶対に確認しましよう

*1:というかOracleユーザDBLINKとかMVIEWとか節操なく使いすぎでしょ