はぢめてのWindows Phone 7でのデータベース(Linq to Sql)

Windows Phone 7に新しいSDKが来ました!9月頃リリースという話のMangoアップデート(Windows Phone 7.1)対応SDK。まだベータですが色々触れます。そしてついにデータベースが搭載されました。というわけで軽く触ってみました。

Code First

フツーだとSQLを書いてデータベースの定義を用意しなければならないところですが、WP7でのデータベースプログラミングにおいて、SQLは不要です。と、いうよりも、そもそも使えません。データベース本体(SQLCE)や、データベースにSQLを発行するクラス(ADO.NET)はMicrosoft.Phone.Data.Internalに格納されており、Internalという名のとおり、外から触ることは出来ません。ではどうするか、というと、WP7ではデータベースはLinq to Sqlを介して操作します。

じゃあテーブル定義どうするの、リレーションどうするの、というと、それはクラスから生成します。クラスを書いて、ある程度DB的に属性を付与して、CreateDatabaseとすれば、それらのテーブルを持ったデータベースが生成されます。まずコードを。あ、そうそう、System.Data.Linqの参照が別途必要です。

public class Meibo : DataContext
{
    public Meibo()
        : base("isostore:/Meibo.sdf") // connection string
    { }

    public Table<Person> Persons { get { return GetTable<Person>(); } }
}

[Table]
public class Person
{
    [Column(IsPrimaryKey = true, IsDbGenerated = true)]
    public int Id { get; set; }

    [Column(CanBeNull = false)]
    public string Name { get; set; }

    [Column]
    public int Age { get; set; }
}

名前と年齢のあるPersonというテーブルを持つ、Meiboというデータベースを定義しました。データベースはDataContextを継承し、各Tableを持つプロパティを。そしてテーブルはTable属性とColumn属性を。IsPrimaryKeyは主キー、IsDbGeneratedは自動連番、CanBeNullはnull非許可です。Columnを付けて回るのが面倒くさそうですが、まぁ単純明快ではありますねん。

接続文字列は基底クラスに渡す形で。保存場所はIsolatedStorage内です。どうせ場所固定で弄らないでしょ?と思うので、上の例では直接定義しちゃっていますが、弄りたい場合はその辺調整で。ちなみにisostore:/です。isostore://にするとダメです(最初うっかり引っかかった)。

実際に使う場合は

// 初回実行時はデータベースを作る、これはapp.xaml.csに書いておくといい
using (var db = new Meibo())
{
    if (!db.DatabaseExists())
    {
        db.CreateDatabase();
    }
}

// Insertの例
var meibo = new Meibo();

var person1 = new Person { Name = "ほげほげ", Age = 20 };
var person2 = new Person { Name = "ふがふが", Age = 15 };
var person3 = new Person { Name = "たこたこ", Age = 23 };

meibo.Persons.InsertOnSubmit(person1); // Insertする
meibo.Persons.InsertAllOnSubmit(new[] { person2, person3 }); // 複数の場合

meibo.SubmitChanges(); // SubmitChangesまではDBへの挿入はされていない

// Selectの例(Ageが20以上のものを抽出)
var query = meibo.Persons.Where(p => p.Age >= 20);

foreach (var item in query)
{
    MessageBox.Show(item.Id + ":" + item.Name + ":" + item.Age);
}

というわけで、DBの存在を全く意識せず自然に書けます。実に素晴らすぃー。

リレーション

リレーションも勿論張れます。例はマクドナルドのバーガーの価格表で。地域で価格が違うので、Burger(バーガー名)、Price(値段)、Place(地域)の3つをBurger-Price-Placeで関連付けてきませう。

public class McDonald : DataContext
{
    public McDonald()
        : base("isostore:/McD.sdf")
    { }

    public Table<Burger> Burgers { get { return GetTable<Burger>(); } }
    public Table<Price> Prices { get { return GetTable<Price>(); } }
    public Table<Place> Places { get { return GetTable<Place>(); } }
}

[Table]
public class Burger
{
    [Column(IsPrimaryKey = true, IsDbGenerated = true)]
    public int Id { get; set; }
    [Column]
    public string Name { get; set; }

    [Association(Storage = "_Prices", OtherKey = "BurgerId")]
    public EntitySet<Price> Prices
    {
        get { return this._Prices; }
        set { this._Prices.Assign(value); }
    }
    private EntitySet<Price> _Prices = new EntitySet<Price>();
}

[Table]
public class Price
{
    [Column(IsPrimaryKey = true, IsDbGenerated = true)]
    public int Id { get; set; }
    [Column]
    public int BurgerId { get; set; }
    [Column]
    public int PlaceId { get; set; }
    [Column]
    public int Value { get; set; }

    [Association(IsForeignKey = true, Storage = "_Burger", ThisKey = "BurgerId")]
    public Burger Burger
    {
        get { return _Burger.Entity; }
        set { _Burger.Entity = value; }
    }
    private EntityRef<Burger> _Burger = new EntityRef<Burger>();

    [Association(IsForeignKey = true, Storage = "_Place", ThisKey = "PlaceId")]
    public Place Place
    {
        get { return _Place.Entity; }
        set { _Place.Entity = value; }
    }
    private EntityRef<Place> _Place = new EntityRef<Place>();
}

[Table]
public class Place
{
    [Column(IsPrimaryKey = true, IsDbGenerated = true)]
    public int Id { get; set; }

    [Column]
    public string Name { get; set; }

    [Association(Storage = "_Prices", OtherKey = "PlaceId")]
    public EntitySet<Price> Prices
    {
        get { return this._Prices; }
        set { this._Prices.Assign(value); }
    }
    private EntitySet<Price> _Prices = new EntitySet<Price>();
}

ぎゃー。面倒くさ。本来のLinq to Sqlではデータベースが先にあって、そこから機械生成でこれを作るんですが、コードを先で作るのはちょっと骨が折れます。Entity Framework Code Firstは、コードを先に作るのが大前提だけあって書きやすいように色々調整してある感じですが、WP7/Linq to Sqlは、本当にただただ手で書きますというわけで全くイケてない。

さて、リレーションはAssociation属性でつけます。また、多を辿る場合はEntitySet、一を辿る場合はEntityRefのプロパティを用意します。これがまた面倒くさ……。たいしたことはない機械的作業ですが、自動プロパティで済ませられないとウザったいことこの上なく。コードスニペットでも用意しますかねえー。

しかし苦労するだけの価値は、あります!

まずデータを用意しなきゃということでInsertを。

// Insertの例
var mcd = new McDonald();

var hamburger = new Burger() { Name = "ハンバーガー" };
var blt = new Burger() { Name = "ベーコンレタストマト" };

var kanto = new Place() { Name = "関東" };
var qshu = new Place() { Name = "九州" };
var hokkaido = new Place() { Name = "北海道" };

var prices = new[]
{
    new Price { Burger = hamburger, Place = kanto, Value = 100 },
    new Price { Burger = hamburger, Place = qshu, Value = 150 },
    new Price { Burger = hamburger, Place = hokkaido, Value = 160 },
    new Price { Burger = blt, Place = kanto, Value = 250 },
    new Price { Burger = blt, Place = qshu, Value = 230 },
    new Price { Burger = blt, Place = hokkaido, Value = 220 }
};

mcd.Places.InsertAllOnSubmit(new[] { kanto, qshu, hokkaido });
mcd.Burgers.InsertAllOnSubmit(new[] { hamburger, blt });
mcd.Prices.InsertAllOnSubmit(prices);

mcd.SubmitChanges();

リレーションを軽やかに片付けて、挿入してくれます。実に自然でイイ!それに、こういうののinsert文手書きはカッタルイですからねえ。更にSelectは

var mcd = new McDonald();

// 関東のバーガーのNameとPriceを抽出
var query = mcd.Burgers.Select(b => new
{
    b.Name,
    Price = b.Prices.First(p => p.Place.Name == "関東").Value
});

// ハンバーガー:100, ベーコンレタス:250
foreach (var item in query)
{
    MessageBox.Show(item.Name + ":" + item.Price);
}

// なお、IQueryable<T>をToStringすると手軽に発行されるSQLが確認出来る
// もう一つの手はDataContext.Logから取ること
MessageBox.Show(query.ToString());

コード上にjoinはないけど、発行されるSQLはjoinしています。手動でjoinすることも可能ですが、基本的にはオブジェクト間をドットで辿って操作します。その方が自然に書けるし、何より、楽ですもの。

DataContextのDispose

usingで括ってあげるのが礼儀正しいわけですが、WP7では実際どう考えるべきだろう。サンプル見てると、CreateDatabaseやSchemaUpdateではusingで囲んでますが、そうでない普通の操作ではコードビハインド内でDataContext使い回してるんですね。基本的にIsolatedStorageに隔離されているわけだし、画面外に出るときだけ切った繋げたすればいいのかなあ、といったふうに思いましたがどうなのでしょ。

.NET版との差分

ほとんど.NET版のLinq to Sqlと同じなのですが、若干追加があります。一つはデータベースのスキーマのアップデート。Microsoft.Phone.Data.Linq Namespace名前空間の参照で、DataContextにCreateDatabaseSchemaUpdaterが追加されます。これにより、アップデートなどによるテーブル構造の変化にも対応出来ます。もう一つは IndexAttribute Class

これらは、通常Linq to Sqlが用いられていたデータベースからのクラス自動生成じゃなく、クラスからのデータベース生成になったことにより、テーブル作りに足りていなかった面の補足と見れるかな。また、その逆で.NET版でサポートされているけれど、WP7版にはないものも幾つかあります。詳しいリストはMSDNのLINQ to SQL Support for Windows Phoneを見ればいいんじゃないかな、ということで。

学習リソース

若干の差異はあるとはいえ、Linq to SqlはLinq to Sqlなので、MSDN - LINQ to SQLを見るのが良いでしょう。また、慣れない間はWP7版ではなく.NET版で、ConsoleApplicationで挙動をあらかた確認しておいたほうが、スムースに行くかとは思います。属性貼ったりは、結構面倒だし罠もあるところですからね……。

まとめ

諸君らの愛したLinq to Sqlは死んだ!何故だ!そうしてEntity Frameworkに置き換えられる運命を辿ったLinq to Sqlですが、ここにきて華麗に復活するとは誰も予想だにしなかったところで、こういう展開は面白い。そして生SQLが使えないのは英断。縛りではあるのですが、Phoneでのアプリケーションの9.9割は、生SQLを必要とすることはないのではないか、とも。

生SQL触れるだろうと思ってWP7版も作るぜ!な勢いで用意していたDbExecutorのWP7版は永劫さようならになってしまいましたががが。DbExecutorはDbExecutorで、もう少し機能追加しますがー。

ところでMangoで、他に追加されたクラスを少し。System.Reflection.Emitが追加されました。これはILを直弄りして動的コード生成するためのクラスですが、WP7でIL生成とかヤラネーヨ。というわけでもなくはなく実は有益。シリアライザの高速化のために動的コード生成は常套手段となっているので、自分は直に使わなくても、普通にメリットは大きく。例えばJSON.NETのJSONシリアライズ/デシリアライズは、WP7版だけリフレクションを直に使ったもので見たところ遅そうでしたが、恐らく次からは.NET版と同じく動的コード生成になり、高速化されるでしょう。ORマッパーなどもそうです。そう、Linq to SqlでもMetaAccessorクラスなどの辺りを覗いてみれば、ILをEmitしているコードが見えます。

そういえばLambdaExpressionもCompile出来るようになりました。が、AssignやLoopなどは搭載されていません、ぐぬー。コード生成したい人はExpressionTreeでお手軽、ではなく、まだまだILGeneratorでEmit頼りしかなさそうです。更に言えばExpressionVisitorも入っていませんね。SL4に近くなったけれどSL4とは言えない、WP7はWP7としか言いようのないAPIになってまいりました。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2024

Twitter:@neuecc GitHub:neuecc

Archive