テクニカルアーティストのためのデータベース入門 (8) O/Rマッパー
実際のところ宗教論争に近いんだけど、個人的には、TAがインハウスで扱うようなDBならSQLだけで十分だというスタンス*1*2ではある。とはいえイマドキそんなことも言ってられないし、書くだけ書いてみる。
とりあえず、「O/R マッパー」と毎回書くのはだるいので、「ORM」と略すことにする。
必要とされる背景
ものすごくざっくりいうと、DBの中にあるテーブル定義と、プログラムの中にあるデータ定義と、それぞれの扱いを統一するのがとてもむずかしいし、MySQL か PostgreSQL かでクエリの文法も違ったりする。なんかそのへんをいいかんじにしてくれるライブラリが必要だよね、っていう話。
主な機能
ORM は、基本的には「クエリアダプタ」と「オブジェクトマッパー」の 2 つの機能を持っていることが多い。
クエリアダプタ
クエリビルダーともいう。その名の通り、SQL 文を自動生成してくれる機能。
たとえば EntityFramework の場合、設定ファイルを別途用意してこんなコードを書くと、
class File { public int Id { get; set; } public string Path { get; set; } } class Model { public int Id { get; set; } public string Name { get; set; } public File File { get; set; } } class ModelContext : DbContext { public DbSet<Model> Models { get; set; } public DbSet<File> Files { get; set; } } public static void Main(string[] args) { var context = new ModelContext(); // (1) var models = new Model[] { new Model() { Name = "model01" }, new Model() { Name = "model02" } }; models.ForEach(m => context.Add(m)); context.SaveChanges(); // (2) var model01 = context.Models.FirstOrDefault(m => m.Name == "model01"); // (3) model01.Name = "new_model01"; context.SaveChanges(); }
こんなかんじのクエリを自動生成して勝手に実行してくれる。*3
// (1) start transaction; insert into `models` (`name`) values ('model01'); select last_insert_id(); insert into `models` (`name`) values ('model02'); select last_insert_id(); commit; // (2) select `m`.`id` as `mid`, `m`.`name`, `f`.`id` as `fid`, `f`.`path` from `models` as `m` left join `files` as `f` on `f`.`id` = `m`.`file_id` where `m`.`name` = 'model01' limit 1; // (3) start transaction; update `models` set `name` = "model01" where `id` = 1 commit;
実際に生成されるクエリは、設定ファイルに書いてるのが MySQL か PostgreSQL かで違うし、この例では .net の Entity Framework だけど、Ruby の ActiveRecord ならまた微妙に違うクエリになる。
オブジェクトマッパー
DB から取得したデータを、クラスに定義されてる変数に割り当ててくれる。この機能だけを実装したライブラリを Micro ORM と呼んだりする。
たとえばこんなコードを書くと、
var model01 = context.Models.FirstOrDefault(m => m.Name == "model01");
だいたいこんなかんじに展開される。*4
Dictionary<int, object>[] data = ExecuteQuery(...); var model01 = new Model(); foreach (var prop in typeof(Model).GetProperties()) { if (data.ContainsKey(prop.Name)) { prop.SetValue(model01, data[0][prop.Name]); } }
メリットとデメリット
メリットは何と言っても、DB内のデータとプログラム内のデータを自然にやりとりできる*7こと。Model クラスが AssetBase クラスを継承してたり、Texture クラス内に File クラスのインスタンスがあったりしても、設定さえちゃんとしておけばあとは ORM がよしなにしてくれる。実際に DB とデータをやりとりするプログラムを書いてみると、この素晴らしさを実感する機会は数え切れないほどあると思う。綺麗ですっきりしたプログラムがとても書きやすい。
ただ気を付けないといけないのは、「自分でクエリ書かなくてもいいから楽でいいなー」って最初は思うんだけど、使ってるうちに、実際にどんなクエリが自動生成されてるのかを把握しておかないと不味いことに気付く。ORM のデメリットはまさにそれで、油断すると非効率なクエリがガンガン自動生成*8されていく。まともに動くアプリをつくろうと思ったら、扱う側に要求される知識と経験はこちらのほうがずっとシビアになりがち。
ORM を使うとクラス定義(≒ソースファイルの個数)が無駄に増えるとか、込み入った操作で部分的にクエリ直書きが必要になってコードが汚くなるとかもあるんだけど、まぁなんだ、いらないファイルは見なかったことにしても実害は大きくないし、クエリ直書きが必要なほど複雑なロジックが必要になってくることは(少なくとも TA がインハウスで扱う範疇では)それほど多くないと思う。
何よりもまずは、DBとプログラムとの間の整合性が保たれることと、それによってどういうパフォーマンスが犠牲になるかを把握すること。
*1:というか、クエリアダプタの挙動をきちんと把握して内部的にどんなSQLが生成されるかを理解した上で O/R マッパーを使うのはとても素晴らしいことだと思うけど、大抵の場合、TAがそこまでやるのは無駄だろうという判断。
*2:あと、大規模で複雑なDBでは O/R マッパーがあると実際すごく便利なんだけど、そんなDBをTAが主導的に扱うことはほぼないだろうという判断。
*3:あくまで「イメージ」
*4:実際にはもっとずっと複雑
*5:コードファースト
*6:モデルファースト
*7:データとプログラムとの間の整合性を保つことができる
*8:気が付いたらすごい数のテーブルが join されてたり、ループの中で毎回 select されてたりする。