graphics.hatenablog.com

技術系テクニカルアーティストのあれこれ

テクニカルアーティストのためのデータベース入門 (8) O/Rマッパー

実際のところ宗教論争に近いんだけど、個人的には、TAがインハウスで扱うようなDBならSQLだけで十分だというスタンス*1*2ではある。とはいえイマドキそんなことも言ってられないし、書くだけ書いてみる。

とりあえず、「O/R マッパー」と毎回書くのはだるいので、「ORM」と略すことにする。

graphics.hatenablog.com

必要とされる背景

ufcpp.net

ものすごくざっくりいうと、DBの中にあるテーブル定義と、プログラムの中にあるデータ定義と、それぞれの扱いを統一するのがとてもむずかしいし、MySQLPostgreSQL かでクエリの文法も違ったりする。なんかそのへんをいいかんじにしてくれるライブラリが必要だよね、っていう話。

主な機能

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;

実際に生成されるクエリは、設定ファイルに書いてるのが MySQLPostgreSQL かで違うし、この例では .net の Entity Framework だけど、RubyActiveRecord ならまた微妙に違うクエリになる。

オブジェクトマッパー

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]);
  }
}

マイグレーション

クエリアダプタとオブジェクトマッパーの機能を両方使えば、クラス定義のソースコードからテーブル定義クエリを自動生成*5したり、逆に、テーブル定義からクラス定義のソースコードを自動生成*6できたりする。この機能のことを「マイグレーション」と呼ぶ。初期化コードや初期化コマンドを用意しておいて、テスト用にあらかじめ決めておいたデータをDBにいれておくこともできる。

基本的に、ORM を使う場合はマイグレーション機能も一緒に使う前提で、このあたりを手動でやっちゃうと逆にややこしくなりがち。

メリットとデメリット

メリットは何と言っても、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 されてたりする。