はじめに
C#のORマッパーといえばEntity Framework(以下、EF)が有名で、データベースの接続を調べると真っ先にEFの名前が出てきます。ORMの機能を充分に備えたライブラリはEFしかありませんが、致命的な欠点があります。欠点については後述しますが、これを解決するには他のライブラリを使うほかありません。本記事では、EFを使わずにORマッパーの機能を使うライブラリとその組み合わせを紹介します。
ORMの基本的な機能
紹介する前にまずはORMで必要な機能を確認します。ORM(Object-relational mapping)は、オブジェクト関係マッピングと呼ばれ、オブジェクト思考とRDB間の非互換であるデータの違いをうまく吸収(これの逆をインピーダンスミスマッチと言います)して、簡単にデータを扱えるようにする技法です。基本的な機能は以下があります。
- SQLを意識することなくコードで記述できるクエリビルダ
- 抽出結果をオブジェクトにマッピングするマッパー
- データベースの定義を自動的に作成・管理する機能マイグレーション
C#でこの機能を満たすライブラリは残念ながらEFしかありません。ただし、EFには致命的な欠点があります。
Entity Frameworkは遅い
とにかくめちゃくちゃ遅いのです
以下はEF Core(Entity Frameworkの軽量版)とDapperという高速のORマッパーを比較した図です。各パターンごとのREADにかかる時間を計測しているのですが、1軽く5倍以上の時間がかかっています。
さらにEF CoreはEntity Frameworkを軽量にしたものなので、その二つを比較した結果がこちら
軽く2倍から5倍の速度差があります。単純計算でDapperとEFを比較したら約25倍速度差が出る計算になります。実際はここまでの速度差はないと思いますが、RDBというパフォーマンスが一番の影響するものでこの遅さは見過ごせないはずです。No Trackingなどチューニング次第で変わると思いますが、チューニング前提でライブラリを使用する難しさは計り知れないはずです。
救世主Dapper
ASP.NETで作られているStackOverflowは、57億ページビューという膨大なトラフィックによるパフォーマンス低下を悩まされ、その原因がLINQ to SQL(EFの前身)にあることが判明しました。パフォーマンスの問題を解決するべく神がかった二人のエンジニアによって、高速なORM、Dapperが発明されました。
Stack OverflowがDapperを開発した経緯はこちらで語られています。
https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2016/may/data-points-dapper-entity-framework-and-hybrid-apps
Dapperの特徴は、とにかく機能をこそぎ落として、最低限のオブジェクトマッピングしか提供しないというところです。最小限のORMということでMicro ORMと呼ばれるものの一つです。
以下はDapperの一例です。SELECT句でCustomers
を抽出し、ジェネリックでCustomer
クラスにマッピングしています。customers
はIEnumerable<Customer>
つまりCustomerクラスの配列として返ります。
var customers = db.Query<Customer>("Select * From Customers")
Dapperの問題点
とにかくシンプルなライブラリなので、SQLをコードベースで書くためのクエリビルダ、マイグレーションがありません。これではインピーダンスミスマッチの発生、DDL、DMLを手動で管理することによる事故る危険性が挙げられます。これらはDapperに備えてなく、他のライブラリを使用する必要があります。そこで私が最高だと感じたものを紹介します。
SqlKata
SQLをコードベースで書くためのクエリビルダを提供します。これは自前で作った人が多いのではないでしょうか(私も自作ライブラリ作りました😇)SqlKataの素晴らしいところは、メインの機能はクエリビルダであり、拡張のSqlKata.Execution
というDapperに依存したパッケージを利用することで作成したクエリを実行出来ることです。さらにSql Server, SQLite, MySql, PostgreSql, Oracleなど主要なDBをサポートしています。以下はクエリの例です。
var products = db.Query(nameof(Product)).Get<Product>();
// 以下と同じ
var products = db.Query<Product>("Select * From Product")
JOINにも対応しています。
db.Query("Product")
.Join("Category", "Product.CategoryId", "Category.Id")
.Select(
"Product.Id",
"Product.Name",
"Category.Id as CategoryId",
"Category.Name as CategoryName"
).Get<ProductCategory>();
Dapper単体だとマッピングも自前でやらなければいけないのでかなり楽になります。以下は上記のクエリと同じことをしています。
connection.Query<Product, Category, ProductCategory>(
@"SELECT
Product.Id as Id,
Product.Name,
Category.Id as Id,
Category.Name
FROM
Product
INNER JOIN
Category
ON Product.CategoryId = Category.Id",
(product, category) =>
{
return new ProductCategory()
{
Id = product.id,
MusicName = product.Name,
CategoryId = category.Id,
CategoryName = category.CategoryName,
};
},
splitOn: "Id,Id"
);
さらに生のDapperをそのまま利用出来ます。sqlkataでクエリが実現出来るかわからないから選定しずらいって人も安心して使えると思います。ただ個人的にはsqlkataで実現できないクエリを人間が解読することが困難なので、生のDapperを使うのは極力辞めたほうが良いのではと思っています😇
db.Connection.Query<Product>("select * from Product");
fluentmigrator
RailsライクなC#用のマイグレーションライブラリです。fluentmigratorの素晴らしいところは、C#でマイグレーションを書くことが出来る、さらにドキュメントが読みやすいく、アップデートガイドも揃えているので安心感があります。以下はマイグレーションの例です。Migration
属性の数値が少ないコードから実行されます。さらにdotnet global toolも利用可能です。
using FluentMigrator;
namespace test
{
[Migration(20180430121800)]
public class AddLogTable : Migration
{
public override void Up()
{
Create.Table("Log")
.WithColumn("Id").AsInt64().PrimaryKey().Identity()
.WithColumn("Text").AsString();
}
public override void Down()
{
Delete.Table("Log");
}
}
}
まとめ
ORMにおける理想形と書いてますが、個人の主観であり、何を重要視するかによって変わると思います。保守性やライブラリの信頼性などを重視するのであればEntity Frameworkを使うのもありだと思います。RDBが陳腐化するのにあと何年かかるかわかりませんが、あと数年は続くであろうDBにおいてパフォーマンスの懸念があるものをベストとするかと言われたら私には出来ません。せっかくC#はパフォーマンスや書きやすさを兼ね備ているのにORMによってアプリケーションのパフォーマンスが低下するなんて悲しいですしね。いろんな意見があると思いますが、選定の一助となれば幸いです。