雑記 - otherwise

最近はDQ10しかやっていないダメ技術者がちまちまと綴る雑記帳

Local Database for Windows Phone

Windows Phone Advent Calendar 14 日目です。
Advent Calendar は後になるほどきついとはよく言ったもので、正直目新しいネタがありません。
そこで、ちょっとズルいですが今日分のセッションアフターフォロー記事から切り出して Windows Phone の Local Database についてまとめてみようと思います。
# MSDN のドキュメントを読みつつ書いたので、あまり検証出来ていない箇所があります。
# 間違え等ありましたら指摘をお願いします。。。

概要

Windows Phone の Local Database は SQLCE をベースとした簡易 DB です。
基本的には SQLCE 4.0 と同等の機能を有しますが、一部制限があります。

  • DB ファイルを IsolatedStorage に保管するため、他のアプリケーションからデータにアクセスする事は出来ません。
  • データへのアクセス方法は LINQ to SQL に限定されます。( Transact-SQL は使用出来ません)
  • DDLDML は使用出来ません。(データベースやテーブルの定義は全てコードファーストで行います)

また、 LINQ to SQL についても幾つかの制限があります。

  • ExecuteCommand は使用出来ません。
  • DataReader 等の ADO.NET オブジェクトは使用出来ません。(全てのデータは DataContext で定義したオブジェクトコレクションで提供されます)
  • SQLCE 4.0 でサポートされるデータ型のみが使用可能です。(サポートされるデータ型は MSDN の一覧を参照してください)
  • Table.IListSource.GetList メソッドは使用出来ません。
  • BinaryFormatter は使用出来ません。
  • Take() は LINQ クエリ内での定数値を必要とします。( SQL の TOP ステートメントの値を使用する事は出来ません)
  • Skip() と Take() は順序付きリストに対して使用する必要があります。

テーブル定義

テーブルを定義するには、テーブル単位にデータクラスを作成します。
テーブルのスキーマ情報を属性 (Attribute) で指定します。

using System;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.ComponentModel;
using System.Collections.ObjectModel;

[Table("TableName")]
public class SomeTable : INotifyPropertyChanged, INotifyPropertyChanging
{
  private int _keyColumn;
  [Column(Name = "KeyColumnName", IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity", CanBeNull = false, Storage = "_keyColumn")]
  public int Key
  {
    get { return _keyColumn; }
    set
    {
      if (_keyColumn == value)
      {
        return;
      }
      NotifyPropertyChanging("Key");
      _keyColumn = value;
      NotifyPropertyChanged("Key");
    }
  }

  private string _valueColumn;
  [Column(Name = "ValueColumnName", CanDbNull = false, Storage = "_valueColumn")]
  public string Value
  {
    get { return _valueColumn; }
    set
    {
      if (_valueColumn == value)
      {
        return;
      }
      NotifyPropertyChanging("Value");
      _valueColumn = value;
      NotifyPropertyChanged("Value");
    }
  }

  private EntitySet<ChildTable> _children = new EntitySet<ChildTable>();
  [Association(OtherKey = "Key", Storage = "_children")]
  public EntitySet<ChildTable> Children
  {
    get { return _children; }
    set { _children.Assign(value); }
  }

  [Column(IsVersion = true)]
  private Binary _version;

  #region INotifyPropertyChanged Members

  public event PropertyChangedEventHandler PropertyChanged;

  private void NotifyPropertyChanged(string propertyName)
  {
    if (PropertyChanged != null)
    {
      PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
  }

  #endregion

  #region INotifyPropertyChanging Members

  public event PropertyChangingEventHandler PropertyChanging;

  private void NotifyPropertyChanging(string propertyName)
  {
    if (PropertyChanging != null)
    {
      PropertyChanging(this, new PropertyChangingEventArgs(propertyName));
    }
  }

  #endregion
}
Table 属性

テーブルを示すクラスに付与します。
Name プロパティにテーブル名を指定出来ます。

Column 属性

テーブルの列を示すフィールドもしくはプロパティに付与します。
この属性には以下の様なプロパティがあります。

Name
列名を指定します。
IsPrimaryKey
テーブルの主キーであるかどうかを指定します。
CanBeNull
null 値を含められるかどうかを指定します。
IsDbGenerated
設定値をデータベースで自動設定するかどうかを指定します。
Expression
計算列を定義します。
IsVersion
タイムスタンプ列であるかどうかを指定します。
UpdateCheck
オプティミスティック同時実行競合の検出方法を指定します。
Storage
実際の値を保持するプライベートフィールドを指定します。
Association 属性

外部キーと主キーのリレーションシップなど、データベース内の関連付けを表すプロパティに付与します。
この属性には以下の様なプロパティがあります。

Name
列名を指定します。
OtherKey
リレーション対象のキーをカンマ区切りの文字列で指定します。
ThisKey
リレーションの親となるキーをカンマ区切りの文字列で指定します。
IsUnique
外部キーの一意制約の有無を指定します。
IsForeignKey
外部キーであるかどうかを指定します。
Storage
実際の値を保持するプライベートフィールドを指定します。

Association 属性の指定例は後述の EntitySet クラスと EntityRef 構造体で紹介します。

Index 属性

クラスに指定する事で、該当のテーブルにインデックスを追加します。
※この属性は Microsoft.Phone.Data.Linq.Mapping 名前空間にあるので注意が必要です。
この属性には以下の様なプロパティがあります。

Name
インデックスの名前を指定します。
IsUnique
インデックスキーの一意制約の有無を指定します。
Columns
インデックスのキーとなる列名をカンマ区切りの文字列で指定します。
EntitySet クラス

一対多のリレーションを作る際は、 EntitySet クラスを用いて表現します。

private EntitySet<ChildTable> _children = new EntitySet<ChildTable>();
[Association(OtherKey = "Key", Storage = "_children")]
public EntitySet<ChildTable> Children
{
  get { return _children; }
  set { _children.Assign(value); }
}
  • EntitySet の型パラメータには子(「多」)側のテーブル型を指定します。
  • EntitySet 型のプロパティには Association 属性を付けて関連付けの情報を指定します。
  • 属性の OtherKey プロパティには外部キー列の名称を指定します。

EntitySet はリスト系のインターフェース (IList, ICollection, IList, ICollection, IEnumerable, IEnumerable) を継承しているので、このプロパティで取得したデータはテーブルコレクションと同様に操作可能です。
また、データ(リスト)をセットする際は Assign メソッドを使用します。

EntityRef 構造体

多対一のリレーションを作る際は、 EntityRef 構造体を用います。

private EntityRef<SomeTable> _parent = new EntityRef<SomeTable>();
[Association(ThisKey = "Key", Storage = "_parent")]
public EntityRef<SomeTable> Parent
{
  get { return _parent.Entity; }
  set { _parent.Entity = value; }
}
  • EntityRef の型パラメータには親(「一」)側のテーブル型を指定します。
  • EntityRef 型のプロパティには Association 属性を付けて関連付けの情報を指定します。
  • 属性の ThisKey プロパティには親キー列の名称を指定します。

データのアクセスは Entity プロパティを通して行います。

データベース定義

テーブル定義同様、データベースもコードで行います。
データベース単位で DataContext クラスの継承クラスを作成します。

using System;
using System.Data.Linq;

public class SomeDatabase : DataContext
{
  public SomeDatabase(string connectionString) : base(connectionString) { }

  public Table<SomeTable> SomeTables { get { return GetTable<SomeTable>(); } }
  public Table<ChildTable> ChildTables { get { return GetTable<ChildTable>(); } }
}
  • 接続文字列を受け取るコンストラクタを作成します。
  • 該当データベース内のテーブルを Table クラスのプロパティとして定義します。
Database 属性

DataContext の派生クラスに Database 属性を付与して明示的に Database である事を指定する事が出来ます。
この属性はデータベースの名称を指定するための Name プロパティを持ちます。
なお、属性自体は省略可能ですが、属性を指定する場合は Name プロパティを必ず指定する必要があります。

接続文字列

DataContext の初期化時に指定する接続文字列には以下の設定を記述する事が出来ます。

var db = new SomeDatabase(
                   "Data Source='isostore:/SomeDatabase.sdf'; "
                 + "Password="'securepassword'; "
                 + "Max Buffer Size='1024'; "
                 + "Max Database Size='128'; "
                 + "File Mode='Read Write'; "
                 + "Culture Identifier='ja-JP'; "
                 + "Case Sensitive=true; "
                 );
データソースパス

DB ファイルのパスを指定します。
# 必ず指定する必要があります。
なお、 DB ファイルは IsolatedStorage か アプリケーション xap ファイル内のどちらかにある必要があります。

IsolatedStorage
IsolatedStorage にある場合はプレフィックスに "isostore:" を付けます。
アプリケーション xap ファイル内
アプリケーション xap ファイル内にある場合はプレフィックスに "appdata:" を付けます。

※アプリケーション xap ファイル内の DB ファイルは常に読取専用で更新は出来ません。

Data Source='DB ファイルのパス'
パスワード

データベースを暗号化する際に指定が必要です。
# 省略可能です。

Password='パスワード'

※既に作成済のデータベースを後から暗号化する事は出来ません。

バッファ最大サイズ

メモリバッファの最大サイズを指定します。(単位 : KByte )
# 省略可能です。省略時は 384(KB) です。
5120(KB) 以下の値を設定する必要があります。

Max Buffer Size='バッファサイズ'
データベース最大サイズ

データベースのサイズ上限を指定します。(単位 : MByte )
# 省略可能です。省略時は 32(MB) です。
512(MB) 以下の値を設定する必要があります。
※当たり前の話ですが、このサイズを超えない場合でもローカルストレージの容量を超えて保存する事は出来ません。

Max Database Size='データベースサイズ'
オープンモード

データベースを開く際のモードを指定します。
# 省略可能です。省略時は "Read Write" となります。
以下の値を設定可能です。

Read Write
複数のプロセスがデータベースを開いて変更する事が出来ます。
Read Only
データベースのコピーを読取専用で開きます。
Exclusive
データベースを排他的に開きます。(他のプロセスからの読み書きを禁止します)
Shared Read
共有設定でデータベースを開きます。(他のプロセスはデータを読む事は出来ますが編集する事は出来ません)
File Mode='モード'
カルチャ識別子

データベースで使用するカルチャを指定します。
# 省略可能です。省略時はアプリケーションのカルチャが使用されます(?)。

Culture Identifier='カルチャコード'

※データベース作成後にカルチャを変更する事は出来ません。

大文字小文字の区別

データベースの照合順序で大文字と小文字を区別するかどうかを指定します。
# 省略可能です。省略時は false になります。

Case Sensitive=[true or false]

※データベース作成後にこの値を変更する事は出来ません。

データベースの作成

DataContext クラスの CreateDatabase メソッドを呼ぶ事で、対象のデータベース及びデータベースに属するテーブルが作成されます。
CreateDatabase メソッドを呼ぶ際は、先に DatabaseExists メソッドを使用して該当データベースの有無を確認し、既に存在する場合は CreateDatabase メソッドの呼出しを回避する様にしてください。

using (var db = new SomeDatabase(connectionString))
{
  if (!db.DatabaseExists())
  {
    db.CreateDatabase();
  }
}

データアクセス

データへのアクセスは .NET Framework 版の LINQ to SQL とほぼ一緒です。

取得

データベースのクラス( DataContext の派生クラス)に作成したテーブルのプロパティから取得します。

// 全件取得
var allData = db.SomeTables;
// 1 件目に紐づく子テーブルのリストを取得
var firstChildren = db.SomeTables.First().Children;
// もちろんクエリ式も使用可能
var query = from item in db.SomeTables
            where item.Key <= 100
            select item;
更新

更新も LINQ to SQL と一緒です。

// 追加
var some1 = new SomeTable { Value = "親1" };
var child1 = new ChildTable { ChildValue = "子供1", Parent = some1 };
var child2 = new ChildTable { ChildValue = "子供2", Parent = some1 };
db.ChildTables.InsertAllOnSubmit(new[] { child1, child2 });
db.SomeTables.InsertAllOnSubmit(some1);
db.SubmitChanges(); // これを忘れずに!
// 更新
var updateTarget = db.SomeTable.First();
updateTarget.Value = "値を書き換え!";
db.SubmitChanges(); // これを忘れずに!
// 削除
var deleteTarget = db.SomeTable.Last();
db.DeleteOnSubmit(deleteTarget);
db.SubmitChanges(); // これを忘れずに!

データベーススキーマの変更

データベースのスキーマを変更する際は、 DataContext の CreateDatabaseSchemaUpdater メソッドを使用してスキーマアップデートのヘルパークラスインスタンスを取得した上で、このヘルパークラスを使用してアップデートを行います。

using (var db = new SomeDatabase(connectionString))
{
  var updater = db.CreateDatabaseSchemaUpdater();
  // 現在のスキーマバージョンを取得
  var version = updater.DatabaseSchemaVersion;
  if (version != 2)
  {
    // 最新でない場合はスキーマ変更処理を実施
    // 例えばテーブルに列を追加
    updater.AddColumn<ChildTable>("ExtraComment");
    updater.DatabaseSchemaVersion = 2; // スキーマバージョンを最新に更新
    updater.Execute(); // これを呼ばないと反映されない
  }
}
アプリケーションのバージョンアップと DB の関係

Marketplace 経由でアプリケーションをアップデートしても、 IsolatedStorage に保存されている DB ファイルのスキーマ変更は自動的には行われません。
# DB ファイルに限らず、 IsolatedStorage に保存されているすべてのファイルは、アプリケーションのアップデート中に変更される事はありません。