This is not the current version. View the latest documentation
Realm Xamarin (.NET)はアプリケーションのモデル層を効率的に安全で迅速な方法で記述することができます。 下記の例をご覧ください:
// 通常のC#のクラスと同じように定義します
public class Dog : RealmObject
{
public string Name { get; set; }
public int Age { get; set; }
public Person Owner { get; set; }
}
public class Person : RealmObject
{
public string Name { get; set; }
public IList<Dog> Dogs { get; }
}
var realm = Realm.GetInstance();
// 検索にはLINQクエリが使用できます
var puppies = realm.All<Dog>().Where(d => d.Age < 2);
puppies.Count(); // => 0 because no dogs have been added yet
// トランザクションを用いてオブジェクトを保存・更新します
realm.Write(() =>
{
realm.Add(new Dog { Name = "Rex", Age = 1 });
});
// 検索結果は自動的にリアルタイムに更新されます
puppies.Count(); // => 1
// LINQクエリの構文はほとんど同じように使えます
var oldDogs = from d in realm.All<Dog>() where d.Age > 8 select d;
// 別のスレッドで検索や更新をすることもできます
new Thread(() =>
{
var realm2 = Realm.GetInstance();
var theDog = realm2.All<Dog>().Where(d => d.Age == 1).First();
realm2.Write(() => theDog.Age = 3);
}).Start();
はじめに
Realmを使用するにはNuGetを利用してインストールします。ソースコードはGitHubにあります。
必要条件
下記のプラットフォームをサポートしています。
- Xamarin.iOS for iOS 7以上、native UIとXamarin Formsの両方をサポート
- Xamarin.Android for API level 10以上、native UIとXamarin Formsの両方をサポート
- .NET Framework 4.6.1以上、Windows 8.1以上.
- Universal Windows Platformアプリケーション、.NET Standard 1.4以上
- 下記の環境で動く.NET Core 1.1:
- Ubuntu 16.04以降
- Debian 8以降
- RHEL 7.1以降
- macOS 10.11以降
- Windows 10以降
下記の開発環境をサポートしています。
- Visual Studio 2015 Update 2以上
- Visual Studio for Mac 7.0以上
Linux上の.NET Coreで動かすにはRealm Mobile PlatformのProfessionalまたはEnterprise Editionのアクセストークンが必要です。
.NET Core版のビルドではdotnet
コマンドラインツールは使用できません。Visual Studioかmsbuild
コマンドラインツールを使用してください。
PCLを利用している方へ重要なお知らせ - RealmとPCLは「NuGet Bait and Switch Trick」という記事で紹介されているテクニックによって動作します。そのためには、RealmをNuGetを用いて、すべてのRealmを使用しているPCLにインストールする必要があります。
プラットフォーム依存なプロジェクトを使用している合は、そのプラットフォームに対応したRealmをNuGetを用いてインストールしてください。
Android ABIサポート
いくつかの命令セットに制限があるので、armeabi
ABI設定を サポートしていません 。
Xamarinプロジェクトを新規作成する際のデフォルトテンプレートではデバッグビルドではすべてのABI設定が有効になっていますが、リリースビルドではarmeabi
だけが有効になっています。 リリースビルドではこの設定を変更する必要があります 。
ABI設定を正しく行っていない場合、特定のアーキテクチャのデバイスでSystem.TypeInitializationException
が発生することがあります。たとえば、Galaxy Tab S2などの64 bitデバイスにデプロイすると、armeabi
とarmeabi-v7a
有効で、arm64-v8a
が 無効の場合 はそのエラーが発生します。
他のABIをリンクする特別なな理由がなければ、armeabi
以外の すべて のABIを有効にすることが最善の方法です。
armeabi-v7a
arm64-v8a
x86
x86_64
Xamarin Studioでは、この設定はプロジェクトオプション>ビルド>Androidビルド>詳細タブを右クリックすることで変更できます。
Visual Studioでは、この設定はプロジェクトプロパティ>Androidオプション>詳細タブを右クリックすることで変更できます。
インストール
- ソリューションペインのプロジェクトの下にある”Packages”の歯車の形のボタンをクリックし、”Add Packages…“を選択します。
- 検索フィールドに”Realm”と入力します。
- Realmを選択し、追加します。
- Fodyが依存関係として追加されたことを確認します。
Realmパッケージには必要なものがすべて含まれています。RealmはFodyというWeavingのためのライブラリを依存関係として追加します。FodyはRealmObjectを永続化可能にするために必要です。
ここまででパッケージがインストールされます。もし、すでにプロジェクトでFodyを利用している場合は、もともとのFodyWeavers.xml
を更新する必要があります。これはRealmWeaverを含め、すべてのWeavingの設定を使えるようにするために必要な手続きです。
下記はすでにFodyをプロジェクトで使用したいた場合に、Realmの設定を有効にするためのFodyWeavers.xmlの例です。
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<RealmWeaver />
</Weavers>
- プロジェクトにて、ソリューションからTools - NuGet Package Manager - Manage Packagesと選択します。
- インストール可能なパッケージの一覧から、Realmパッケージを選択します。
- 右側にプロジェクトがチェックされていることと、インストールボタンが押せることを確認します。
- インストールボタンを押します。
- ダイアログが表示されたらOKボタンを押します。このダイアログはRealmとFodyをインストールするかどうかの確認が表示されています。
Realmパッケージには必要なものがすべて含まれています。RealmはFodyというWeavingのためのライブラリを依存関係として追加します。FodyはRealmObjectを永続化可能にするために必要です。
ここまででパッケージがインストールされます。もし、すでにプロジェクトでFodyを利用している場合は、もともとのFodyWeavers.xml
を更新する必要があります。これはRealmWeaverを含め、すべてのWeavingの設定を使えるようにするために必要な手続きです。
下記はすでにFodyをプロジェクトで使用したいた場合に、Realmの設定を有効にするためのFodyWeavers.xmlの例です。
<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
<RealmWeaver />
</Weavers>
UWPまたはproject.jsonベースのプロジェクトでは注意が必要です。NuGet 3.1の変更により、パッケージはコンテンツファイルを作成しなくなったので、FodyWeavers.xml
は自動的に作成されません。手作業でファイルを作成し、上記の内容をペーストする必要があります。詳細については、こちらのマニュアルを参照してください。
Realm Studio
Realm Studio is our premiere developer tool, built so you can easily manage the Realm Database and Realm Platform. With Realm Studio, you can open and edit local and synced Realms, and administer any Realm Object Server instance. It supports Mac, Windows and Linux.
API Reference
Realmで使用できるすべてのクラスとメソッドに関しては、APIリファレンスをご覧ください。
サンプルコード
GitHubリポジトリのexamplesフォルダにたくさんのサンプルコードがあります。ぜひ、ご参考にしてください。
ヘルプ
- 使い方に困ったときは、StackOverflowで#realmタグを付けて質問してください。私たちは毎日StackOverflowをチェックしています。
- さらに複雑な問題に対する質問は、こちらの Slackチャットにて聞いてください。(質問は日本語で構いません)
- バグ報告や機能リクエストについては GitHubのIssuesからご報告ください。
モデル
Realmのデータモデルは、普通のC#のクラスやプロパティとして定義できます。モデルオブジェクトを定義するには、単にRealmObjectのサブクラスを作成するだけです。
Realmのモデルオブジェクトは、他のC#のオブジェクトとほとんど同様に機能します。一般的なクラスと同様にメソッドやイベントを追加し、同じように使用することができます。主な制限はオブジェクトは作成したスレッド以外では使用できないことと、永続化するプロパティはgetterとsetterを生成する必要があることです。
さらに、モデルクラスはPublicかつ引数なしのコンストラクタを持っていなければなりません。カスタムのコンストラクタを1つも定義しなければ、コンパイラが自動的に引数なしのコンストラクタを追加します。 しかし、もし1つでも自分でコンストラクタを定義した場合は、合わせて引数なしのコンストラクタも定義しなければなりません。
関連とネストしたデータ構造は、単純に関連の対象となるモデルクラスのプロパティを持たせるか、対象のモデルクラスを保持するIList
をプロパティとして持たせます。
// Dog model
public class Dog : RealmObject
{
public string Name { get; set; }
public int Age { get; set; }
public Person Owner { get; set; }
}
public class Person : RealmObject
{
public string Name { get; set; }
public IList<Dog> Dogs { get; }
}
対応しているデータ型
Realmは符号なし整数型以外のプリミティブ型(bool
、char
、byte
、short
、int
、long
、float
、double
)とstring
、そしてDateTimeOffset
型をサポートしています。Null許容型も同様にサポートしています。詳しくはNull許容型のプロパティセクションで説明しています。
Date型
日付を表す型としてはDateTime
型ではなく標準のDateTimeOffset
型を採用しています。
このことはMicrosoftによって曖昧さがあるDateTime
よりも推奨されていることに基づいています。
実際の値は100ナノ秒の精度で保存されます。
タイムゾーンを指定してDateTimeOffset
を保存できますが、RealmにはUTCの値が保存されます。他のプラットフォームとの互換性のために曖昧さのない表現を採用しています。タイムゾーン指定子がなくなるので、UtcTicks
の代わりにTicks
を使って値を比較すると正しくないことがあります。
カウンタ
RealmではRealmInteger<T>という特別な整数型を提供しています。RealmInteger<T>
には同期されたRealmを使用している場合において、より明確な意図とより良いコンフリクトの解決方法を提供するためのAPIが備わっています。 型パラメータのT
はbyte
、short
、int
またはlong
のいずれかになります。
慣習的にカウンターは値を読み出し、値を増加させて再度セットし直すという方法で実装されます(例: myObject.Counter += 1
)。この方法は非同期的な状況では正しく動きません。たとえば、2つのクライアントがオフラインであるとします。両方のクライアントが同時にある値を読み出し(仮に10
ということにします)、1つ加算し、再び値をセットします。値は11
となります。最終的に、クライアントがオンライン状態に復帰したあと、その値がマージされると、カウンターは期待した12
ではなく11
を示すことになります。
RealmInteger
は通常の整数型によって作られているので、プロパティの型をT
からRealmInteger<T>
に変更してもマイグレーションは必要ありません。
public class MyObject : RealmObject
{
[PrimaryKey]
public int Id { get; set; }
public RealmInteger<int> Counter { get; set; }
}
カウンタの値を加算するにはIncrementメソッドを呼びます。
var obj = realm.Find<MyObject>(1);
obj.Counter; // 0
obj.Counter.Increment(); // 1
obj.Counter.Decrement(); // 0
obj.Counter.Increment(2); // 2
obj.Counter.Increment(-5); // -3
カウンタをリセットするには単に新しい値を代入します。この場合は通常のマージルールが適用されることに注意してください。 正しいセマンティクスを保証するには次のようにします。
var obj = realm.Find<MyObject>(1);
obj.Counter = 0; // Resetting to 0
RealmInteger<T>
は暗黙的にT
に変換されるので、使用に際してキャストしたり何か追加で必要になることはありません。
var obj = new MyObject
{
Counter = 3
};
realm.Write(() => realm.Add(obj));
DoSomething(obj.Counter);
public void DoSomething(int value)
{
// ...
}
リレーションシップ(関連)
RealmObjectはプロパティにRealmObjectやIListを使用して、他のオブジェクトとの関連を定義することができます。
IList
型のプロパティについては、 IList
プロパティを定義すると最初に使用されたときに実際のインスタンスが作成されます。自分でインスタンスを作成することはできません。そのためget;
だけを定義しなければなりません。
1対1のリレーションシップ
モデル間で1対1や多対1の関連を持たせるには、別のRealmObjectのサブクラスのプロパティを定義します。
public class Dog : RealmObject
{
// ... other property declarations
public Person Owner { get; set; };
}
public class Person : RealmObject
{
public string Name { get; set; }
}
関連として定義したプロパティの使い方は、他のオブジェクトと同様に代入するだけです。
realm.Write(() =>
{
var jim = realm.Add(new Person { Name = "Jim" });
var rex = realm.Add(new Dog { Owner = jim });
});
関連を解除するには単にnull
を代入します。
rex.Owner = null;
RealmObjectをプロパティとして持つとき、ネストされたプロパティには通常のオブジェクトのプロパティと同じ文法でアクセスできます。上記の例では、rex.Owner.Address.Country
とすると、Realmはネストしたオブジェクトを必要に応じて自動的にフェッチします。
1対多のリレーションシップ
1対多の関連を定義するにはIList
をプロパティとして定義します。このプロパティにアクセスすると、空のコレクションか、一種類のRealmObject型のインスタンスを要素に持つコレクションが返ります。
例えば、Person
クラスがDog
オブジェクトを複数持てるように、1対多の関連として“Dogs”を追加するとすると、IList<Dog>
をプロパティとして定義するだけです。IList
オブジェクトを自分で生成することはできません。 – Realmが自動的にオブジェクトを生成します。関連のオブジェクトについては要素のオブジェクトを追加または削除する操作のみ行うことができます。
public class Dog : RealmObject
{
public string Name { get; set; }
}
public class Person : RealmObject
{
// ... other property declarations
public IList<Dog> Dogs { get; }
}
jim.Dogs.Add(rex);
jim.Dogs.Count(); // => 1 -- nobody but rex
逆方向の関連
Realmの関連は一方通行です。Dog
への1対多の関連であるPerson.Dogs
のプロパティと、Person
への1対1の関連であるDog.Owner
の2つのプロパティは、それぞれ互いに独立して動作します。Person.Dogs
プロパティにDog
オブジェクトを追加しても、Dog.Owner
プロパティにPerson
のオブジェクトは自動的に追加されたりはしません。手作業でこの2つの関連の整合性を保とうとすることは、間違いを起こしやすく、複雑で冗長さを招くので、Realmでは下記に示すような、逆方向の関連(バックリンクとも呼ばれます)を表現するためのプロパティを使用することができます。
Backlink(逆方向の関連)プロパティを使用することで、関連元のオブジェクトを特定のプロパティを使って取得することができます。例えば、Dog
クラスにOwners
という自分自身を関連として保持しているPerson
オブジェクトをすべて取得するというプロパティを持たせることができます。その方法はOwners
プロパティをIQueryable<Person>
型として定義し、[Backlink]属性を用いてOwners
はPerson
クラスの関連であると指定するだけです。
public class Dog : RealmObject
{
[Backlink(nameof(Person.Dogs))]
public IQueryable<Person> Owners { get; }
}
Backlink(逆方向の関連)プロパティはIList<RealmObject>
型(1対多の関連)とRealmObject型(1対1の関連)のプロパティのどちらにも使えます。
public class Ship : RealmObject
{
public Captain Captain { get; set; }
}
public class Captain : RealmObject
{
[Backlink(nameof(Ship.Captain))]
public IQueryable<Ship> Ships { get; }
}
Null許容型のプロパティ
string
、byte[]
や関連に用いるRealmObjectのような参照型はnull値をとることができます。
int?
のようなNull許容型やNull許容型のDateTimeOffset?
も完全にサポートしています。
プロパティの永続性をコントロールする
RealmObjectを継承したクラスはFody weaverによってコンパイル時に処理されます。そのとき、すべての自動実装プロパティは永続化の対象であると推定され、Realmの内部ストレージとマッピングするためのsetterとgetterが自動的に生成されます。
プロパティの永続化をコントロールするためのメタデータを付加するC#属性をいくつか提供しています。
プロパティが永続化されないようにするには、[Ignored]属性を付加します。よくある例としては、画像を保存するときに、実際のデータではなくパスだけを保存するような場合です。
public string HeadshotPath { get; set; }
// 画像データは永続化されず、メモリ上にのみ保持されます
[Ignored]
public Image Headshot { get; set; }
カスタムSetter
カスタムのsetterやgetter実装を持つプロパティは自動的に保存されないプロパティとして扱われます。例えば次のようなバリデーションをsetterに追加することができます。
private string Email_ { get; set; }
// Validating version of persistent Email_ property
public string Email
{
get { return Email_; }
set
{
if (!value.Contains("@")) throw new Exception("Invalid email address");
Email_ = value;
}
}
インデックス付きプロパティ
現在のバージョンでは、文字列と整数型、bool型およびDateTimeOffset
型のプロパティはインデックスに対応しています。
プロパティをインデックスに登録することは==
やContains
を使ったクエリの速度を大幅に向上します。その代わりにオブジェクトを作成する速度は少し遅くなります。
プロパティをインデックスに登録するには、[Indexed]属性をプロパティの定義に追加します。 (例)
public class Person : RealmObject
{
[Indexed]
public string Name { get; set; }
public IList<Dog> Dogs { get; }
}
プロパティ名を異なる名前で保存する(Mapped Properties)
Realmファイルはプラットフォームに依存しないので、異なるプラットフォーム上で使用されることがあります。プロパティ名などの命名規則により、別の言語では異なるプロパティ名が使われることがあります。標準のC#の命名規則とは異なる名前を使って保存するには[MapTo]属性を使用します。
// C# model
public class Person : RealmObject
{
[MapTo("name")]
public string Name { get; set; }
[MapTo("dogs")]
public IList<Dog> Dogs { get; }
}
// Equivalent js model
class Person {}
Person.schema = {
name: 'Person',
properties: {
name: {type: 'string'},
dogs: {type: 'list', objectType: 'Dog'}
}
};
オブジェクトの自動更新(ライブアップデート)
Realmに管理(保存)されたRealmObjectのインスタンスは常に最新の内部データの状態に自動的に更新されます。つまり、オブジェクトをいちいち再読み込みする必要はありません。プロパティを変更すると、その変更はただちに同じオブジェクトを参照している他のインスタンスに反映されます。
var myDog = new Dog { Name = "Fido", Age = 1 };
realm.Write(() =>
{
realm.Add(myDog);
});
var myPuppy = realm.All<Dog>().First(d => d.Age == 1);
realm.Write(() =>
{
myPuppy.Age = 2;
}
myDog.Age; // => 2
このようなRealmObjectの性質は、Realmが高速にかつ効率的に動作するだけでなく、アプリケーションのコードをよりシンプルに、リアクティブにします。例えば、あるRealmオブジェクトのデータを表示しているUIコンポーネントがあるとした場合、そのUIコンポーネントを再描画する前に、いちいち再読み込みしたり、検索し直す必要はありません。
Realmの通知に登録することで、いつRealmのデータが更新されたのかが分かります。アプリケーションのUIを更新すべきタイミングを知ることができます。
プライマリキー・プロパティ
[PrimaryKey]属性はそのRealmObjectモデルクラスのプロパティに 1つにだけ 設定することができます。プライマリキーを定義すると、オブジェクトの検索と更新を効率よく行なうことができ、それぞれの値がユニークであるということも保証できます。
char
と整数型、および、文字列型のプロパティのみ、プライマリキーとして指定できます。小さな整数値を指定したり、どの型を使っても、パフォーマンスや保存されるサイズには関係ありません。
複数のプロパティに[PrimaryKey]属性を付加すると、コンパイルされますが実行時に最初のRealmを開く際に Schema validation failed というエラーが発生します。
PrimaryKey
を指定したオブジェクトをRealmに保存した後は、そのオブジェクトIDを変更することはできなくなります。
同じプライマリキーの値を持つ別のオブジェクトを保存しようとすると、RealmDuplicatePrimaryKeyValueException例外が発生します。
プライマリキーが定義されていれば、Realm.Findを使ってオブジェクトをすばやく取得することができます。これは、LINQを使用するよりもより効率的なインデックスを使用したクエリが使われます。文字列、文字または整数キーに対応するためにそれぞれのオーバーロードが定義されています。
public class Person : RealmObject
{
[PrimaryKey]
public string SSN { get; set; }
public string Name { get; set; }
public IList<Dog> Dogs { get; }
}
var objByPK = realm.Find<Person>("457-55-5462");
モデルクラスの継承
Realm Xamarinではモデルクラスをさらに継承することはできません。RealmObjectの直接のサブクラス以外に継承されたクラスがあるとWeavingに失敗します。
コレクションクラスについて
Realmオブジェクトの集合を表すには標準の型を使用します。
実行時にはどちらもIRealmCollection\<T\>
インターフェース、IReadOnlyList<T>
、INotifyCollectionChanged
に準拠し、遅延ロードに対応します。つまり、コレクションのサイズを取得したとしても、実際に要素のオブジェクトがメモリにロードされるのはその要素にアクセスしるときまで遅延されます。
書き込み
Realmオブジェクトに対するすべての変更(追加、変更、削除)は、トランザクションの内部で行う必要があります。
スレッド間でデータを共有したり、アプリキーションの再起動時に以前のデータを利用するには、Realmにデータを保存しなければなりません。これらの操作は、トランザクションの中で行う必要があります。
トランザクションには、無視できないオーバーヘッドが発生しますので、トランザクションの数はできるだけ最小限に抑えることが望ましいです。例えば、ループの中で複数のオブジェクトを追加する場合などは、ループの中で要素ごとにトランザクションを作るのではなく、ループの外に1つだけトランザクションを作る方がパフォーマンスが良くなります。
realm.Write(() =>
{
var people = Realm.All<Person>();
foreach (var person in people)
{
person.Age += 1;
}
});
トランザクションはディスクI/Oを伴う操作などと同様に失敗する可能性があります。
ディスクの容量不足などで失敗した際のエラーから復帰するためには例外処理に備える必要があります。簡単にするためにこのドキュメントやサンプルコードではエラー処理をしていませんが、実際のアプリケーションでは、正しくエラーを処理するべきです。
トランザクションを開始するには2つの簡単な方法があります。[Realm.BeginWrite()](/docs/dotnet/1.6.0/api/reference/Realms.Realm.html#Realms_Realm_BeginWrite)と[Realm.Write()](/docs/dotnet/1.6.0/api/reference/Realms.Realm.html#Realms_Realm_Write_System_Action_)です。
最初の方法、[Realm.BeginWrite()](/docs/dotnet/1.6.0/api/reference/Realms.Realm.html#Realms_Realm_BeginWrite)は[Transaction](/docs/dotnet/1.6.0/api/reference/Realms.Transaction.html)オブジェクトを返します。`Transaction`オブジェクトは`Dispose`パターンを実装しているので、`using`とともに使うことができます。
```csharp
using(var transaction = realm.BeginWrite())
{
person.FirstName = "John";
person.Age = 56;
transaction.Commit();
}
明示的にCommitメソッドをトランザクションオブジェクトに対して呼び出す必要があります。そうしなければ、トランザクションは自動的にロールバックされます。もう一つの方法は、Realm.Write()を用いて変更を加えるコードを囲む方法です。
realm.Write(() =>
{
person.FirstName = "John";
person.Age = 56;
});
この方法では、例外が起こらない限りはトランザクションは暗黙的にコミットされます。
マルチスレッドのセクションに書かれているRealm.WriteAsync()に関する議論もご覧ください。
オブジェクトの生成
RealmObjectのサブクラスとして定義したモデルクラスは、インスタンス化してRealmに保存することができます。例として下記のようなシンプルなモデルを考えます。
public class Dog : RealmObject
{
public string Name { get; set; }
public int Age { get; set; }
}
インスタンスをRealmの保存するにはRealm.Add()メソッドを使用します。保存されたインスタンスは自動更新されるようになります。
realm.Write(() =>
{
var myDog = new Dog();
myDog.Name = "Rex";
myDog.Age = 10;
realm.Add(myDog);
});
オブジェクトをRealmに追加した後は、オブジェクトに対するすべての変更は保存されます。(オブジェクトの変更は、トランザクションの中で行わなければなりません)。別のスレッドで、同じRealmに保存されているオブジェクトに変更があった場合は、トランザクションが完了した時点ですべての変更が適用されます。
同時に発生した書き込み処理は、互いの書き込み処理をブロックします。 これは類似の他のデータベースでも同様で、よく使われるベストプラクティスとして、書き込み処理を別のスレッドに分けることを推奨します。
RealmはMVCCアーキテクチャーを採用しているので、書き込み処理の最中でも読み込み処理をブロックすることはありません。同時に複数のスレッドから書き込みをするのでなければ、大きな単位でトランザクションを使いましょう。細かいトランザクションを使うよりこの特性を活かすことができます。
オブジェクトの更新
オブジェクトを更新するには、トランザクションの中でプロパティに値を代入します。
// トランザクションを開始して、オブジェクトを更新します
using (var trans = realm.BeginWrite())
{
author.Name = "Thomas Pynchon";
trans.Commit();
}
[PrimaryKey]を定義したオブジェクトについては、realm.Addメソッドの追加の引数としてupdate: true
を渡すことで、既存のオブジェクトを上書きできます。
public class Person : RealmObject
{
[PrimaryKey]
public int Id { get; set; }
// ... other property declarations
}
realm.Write(() =>
{
realm.Add(new Person
{
Id = 1,
Name = "Kristian"
});
});
var kristianWithC = new Person
{
Id = 1,
Name = "Christian"
};
realm.Write(() => realm.Add(kristianWithC, update: true));
update: true
を指定したにもかかわらず、PrimaryKey
を持たないオブジェクトを渡した場合は、上書きするオブジェクトを見つけられないので、update: false
を指定した場合と同じ挙動になります。 更新対象のオブジェクトが別のRealmObjectを関連として持っている場合、PrimaryKey
がある場合は追加または更新されます。PrimaryKey
が無い場合は単に追加されるだけになります。
var kristian = new Person { Id = 1, Name = "Kristian" };
var rex = new Dog { Id = 1, Name = "Rex" };
kristian.Dogs.Add(rex);
realm.Write(() => realm.Add(kristian));
var christian = new Person { Id = 1, Name = "Christian" };
christian.Dogs.Add(new Dog { Id = 1, Name = "Bethoven" });
realm.Write(() => realm.Add(christian, update: true));
var newName = kristian.Name; // Christian
var newDogName = kristian.Dogs.First().Name; // Bethoven
オブジェクトの削除
オブジェクトを削除するには、トランザクションの中で削除したいオブジェクトをRealm.Removeに渡します。
var cheeseBook = realm.All<Book>().First(b => b.Name == "Cheese");
// トランザクションを開始してオブジェクトを削除します
using (var trans = realm.BeginWrite())
{
realm.Remove(cheeseBook);
trans.Commit();
}
Realmに保存されてるオブジェクトをすべて削除することもできます。ディスクスペースを効率的に再利用するために、Realmファイルのサイズはそのまま維持されることに注意してください。
クエリ
クエリは標準のLINQ構文を実装しています。
まずrealm.All<T>メソッドに型を指定して、その型の全てのオブジェクトを取得します。これがもっとも基本的なクエリになります。それに対して、Where
やその他のLINQ演算子を繋げて、結果をフィルタすることができます。
Realmで使えるLINQ構文についてはLINQサポートページをご覧ください。
拡張構文とメソッドチェーン
var oldDogs = realm.All<Dog>().Where(d => d.Age > 8);
クエリ式
var oldDogs = from d in realm.All<Dog>() where d.Age > 8 select d;
どちらの構文を利用した場合でも返されるオブジェクトはIQueryableインターフェースに準拠しています。そのため、下記のようにオブジェクトをイテレーションすることができます。
foreach (var d in oldDogs)
{
Debug.WriteLine(d.Name);
}
より複雑なクエリのサンプル
もしLINQクエリの使い方にあまり慣れていない場合のために、下記にいくつかの基本的なクエリ構文を示します。 拡張構文 を用いて基本的なクエリを書く方法がわかります。
下記はPersonオブジェクトからJohnまたはPeterというファーストネームを持つオブジェクトをすべて検索する例です。
var johnsAndPeters = realm.All<Person>().Where(p =>
p.FirstName == "John" ||
p.FirstName == "Peter");
var peopleList = johnsAndPeters.ToList();
最初の文に書かれているjohnsAndPeters
という変数のオブジェクトはIQueryable
を実装しています。”John”または”Peter”というファーストネームを持つオブジェクトが格納されています。
これが標準的なLINQクエリの実装です。クエリによって条件に合うオブジェクトを取得することができます。クエリは遅延実行されるので、結果のオブジェクトをループしたり、件数を取得しようとしなければ実際の処理は何も実行されることはありません。
最後に呼び出しているToList
メソッドで、Realmコアエンジンからのマッピングを切り離しています。
オブジェクトはコピーされません。条件に合うオブジェクトの参照を取得します。
すべてのオブジェクトをリストとして取得する代わりに、クエリの結果オブジェクトをC#標準のforeach
分を用いてループすることもできます。
foreach (var person in johnsAndPeters) // iterate query
{
Debug.WriteLine(person.Name);
}
論理演算子
C#標準の論理演算子をLINQクエリで利用できます。
カッコを使用して、ネストした条件や優先順位を示すことができます。
var complexPeople = realm.All<Person>().Where(p =>
p.LastName == "Doe" &&
(p.FirstName == "Fred" || p.Score > 35));
型を指定してオブジェクトを取得する
指定した型のオブジェクトをすべて取得するには、realm.All<T>()メソッドを使用します。指定したすべてのモデルクラスを保持したIQueryable
コレクションオブジェクトが返ります。上記の例ではPerson
クラスのオブジェクトすべてが返ります。
さらに取得したオブジェクトを絞り込むために重ねてLINQクエリを適用することができます。遅延実行されるので、コレクション内のオブジェクトをイテレーションしたりしない限りは何のオーバーヘッドもありません。
var ap = realm.All<Person>(); // this is all items of a type
var scorers = ap.Where(p => p.Score > 35); // restrict with first search clause
var scorerDoe = scorers.Where(p => p.LastName == "Doe"); // effectively an AND clause
並べ替え
標準のLINQクエリのようにOrderBy
、OrderByDescending
、ThenBy
、ThenByDescending
を用いて、複数レベルの並び順をQueryable
に対して指定することができます。
並べ替えは内部クエリエンジンにて非常に効率良く実行されます。並べ替えを実行するためにすべてのオブジェクトをロードすることはありません。また、単にRealm.Allで取得したすべてのオブジェクトを並べ替える場合にはWhere
を使う必要はありません。
注意: 取得したオブジェクトをリストに変換するためにToList
を 呼び出した後 に、OrderBy
などを用いて並べ替えを行うと 、 _標準のLINQ によりメモリ上で並べ替えが実行されます。ToList
を使う場合は、構文の最後で実行するように気をつけてください。
var highestScore = realm.All<Person>().OrderByDescending(p => p.Score).First();
var sortedPeople = realm.All<Person>()
.OrderBy(p => p.LastName)
.ThenBy(p => p.FirstName);
クエリの連鎖
取得したオブジェクトはけっしてコピーされず、必要に応じで計算は遅延されるので、複雑なクエリも、非常に効率良くチェーンして分かりやすく書くことができます。
var teenagers = realm.All<Person>().Where(p => p.Age >= 13 && p.Age <= 20);
var firstJohn = teenagers.Where(p => p.FirstName == "John").First();
取得データの数を制限
Realm以外のほとんどのデータベースには検索結果を「ページネーション(ページング)」する仕組みが備わっています(例えばSQLiteの’LIMIT’句によるものなどです)。この仕組みはディスクの過剰な読み込み、あるいは一度に大量のデータをメモリに読み込むことを避けるために使われます。
Realmのクエリは遅延実行されるので、このような「ページネーション」の仕組みはまったく必要ありません。なぜなら、Realmはクエリの実行結果の要素に対して、実際にアクセスしたときだけオブジェクトを読み込むからです。
UIや実装の都合により、クエリの実行結果の一部分だけが必要だったとします。そのときは、単にIQueryable
オブジェクトを用いて、必要な要素にだけアクセスすれば良いのです。
LINQが備えているTake
を使いたいと思うかもしれませんが、その機能はまだサポートされていません。使えるようになった際には、一回の操作で要求したすべてのオブジェクトをインスタンス化するものになるでしょう。
Realmについて
Realm はRealm Mobile Databaseのコンテナを表すインスタンスです。Realmとは ローカルRealm と 同期されたRealm があります。同期されたRealmは透過的にRealm Object Serverと別のデバイス間でデータを同期します。アプリケーションからは同期されたRealmをローカルファイルのように扱いますが、そのデータは別のデバイスから更新される可能性があります。どちらのRealmでも同じように操作できますが、同期されたRealmを開くには認証済みのユーザーオブジェクト、およびアクセス権が必要なことが異なります。
Realmについてさらに詳しくはRealm Data Modelのセクションをご覧ください。
Realmを開く
Realmを開くには、Realm
オブジェクトをインスタンス化するだけです。これまで見てきたように、次のようにします。
// Get a Realm instance
var realm = Realm.GetInstance();
引数なしでGetInstance
を呼んだ場合は、デフォルトRealmのインスタンスが作成されます。RealmConfiguration
オブジェクトを指定して設定を変更することもできます。
Realm Configuration
RealmConfigurationを使ってファイルのパスを指定する方法はいくつかあります。(Configurationオブジェクトを一度作成したら後からパスの値を変えることはできません。)
- 絶対パスを指定して、ファイルパスを完全に変更する。
- 相対パスでディレクトリを指定して、Realmファイルを標準のパスの下層に作成する。
- ファイル名だけを指定して、保存場所は変えずにファイル名だけを変更する。
var config = new RealmConfiguration("OtherPath.realm");
var otherRealm = Realm.GetInstance(config);
RealmConfiguration
オブジェクトではでは、スキーマバージョンを指定できます。 詳細については、マイグレーションのセクションをご覧ください。 開発中の利便性のために、既存のファイルと現在のスキーマが一致しないとき(マイグレーションが必要になるとき)に自動的に既存のファイルを削除し、新しいスキーマで作り直す機能があります。その機能を有効にするにはShouldDeleteIfMigrationNeededプロパティをtrue
に設定します。すると、Realm.GetInstance()はスキーマが既存のファイルと一致しない場合は既存のファイルを自動的に削除します。リリースする前にモデルの構造を簡単に試行錯誤できます。リリースするアプリではこのプロパティを 有効にしないことを推奨します 。 意図せずデータが消えてしまうという事態を避けるために#if DEBUG
セクション内に設定することも良い方法です。
同じConfigurationオブジェクトをRealmをオープンする際に渡すことで、すべてのRealmを同じ設定で使うことができます。この方法は、複数のスレッドで同じRealmを扱うもっとも一般的な方法です。
Configurationオブジェクトを渡して回る代わりに、デフォルトConfigurationをオーバーライドすることで、すべて同じ設定を適用することもできます。
RealmインスタンスはConfiguration単位、スレッド単位のシングルトンであることに注意してください。つまり、Realm.GetInstance()は、同じConfiguration、かつ同じスレッド上で呼び出されるなら同じインスタンスを返します。
デフォルトRealm
Realm.GetInstance()を引数なしで呼び出した場合は”デフォルトRealm”インスタンスを返します。デフォルトRealmはEnvironment.SpecialFolder.Personal
が指すパスに作られたdefault.realm
という名前のファイルになります。
非同期にRealmを開く
Realmファイルを開くことはマイグレーションやコンパクションを伴う場合は時間がかかる場合があります。または同期されたRealmでは初回同期のダウンロードに時間がかかります。そのような場合にRealm.GetInstanceAsyncメソッドを利用すると、Task<Realm>
を戻り値として返し、非同期にそのような時間のかかる処理を実行します。Realm.GetInstanceAsync
メソッドは読み取りのアクセス権しか持たないRealmを開く場合にも使う必要があります。
例:
var config = new RealmConfiguration
{
SchemaVersion = 1,
MigrationBlock = (migration, oldSchemaVersion) =>
{
// potentially lengthy data migration
}
};
try
{
var realm = await Realm.GetInstanceAsync(config);
// Realm successfully opened, with migration applied on background thread
}
catch (Exception ex)
{
// Handle exception that occurred while opening the Realm
}
同期されたRealmではすべてのサーバー上のリモートコンテンツをバックグラウンドスレッドでダウンロードします。
var serverURL = new Uri("realm://my.realm-server.com:9080/~/default");
var config = new SyncConfiguration(User.Current, serverURL));
try
{
var realm = Realm.GetInstanceAsync(config);
// Realm successfully opened, with all remote data available
}
catch (Exception ex)
{
// Handle error that occurred while opening or downloading the contents of the Realm
}
Realmインスタンスをクローズする
Realmはネイティブメモリとファイルディスクリプタの開放を行うためにIDisposable
を実装しています。そのため、インスタンスがスコープの外に出たときに、自動的にクローズされます。
Realmファイルの探し方
アプリケーション内のRealmファイルの場所がわからないときは、このStackOverflowの回答を参考にしてください。
補助的に作成されるRealmの関連ファイルについて
Realmでは.realm
拡張子を持つメインのRealmファイルとは別に、いくつかの内部的に使用する関連ファイルを自動的に作成します。
.realm.lock
- Realmファイル開く際の競合を防ぐためのロックとして使われます。.realm.note
- スレッド・プロセス間の通知に使用する名前付きパイプのファイルです。
これらのファイルは、.realm
拡張子のRealmのデータファイルには何の影響も与えません。またこれらのファイルを(実行中を除いて)削除したり移動したりすることも問題ありません。
Realmに関する問題を報告する際には、これらの関連ファイルを.realm
拡張子のRealmのデータファイルと一緒に添付してください。関連ファイルには問題を調査するときに役に立つ情報が含まれています。
Realmファイルをアプリに同梱する
Realmファイルをあらかじめアプリに組み込んだ状態で配布したい場合は、こちらのStackOverflowの回答を参考にしてください。Xamarinプロジェクトにリソースとしてファイルをコピーして使用する方法が説明されています。
モデル定義のサブセット
各Realmファイルで保存されるモデルの定義を使い分けたり、制限したいことがあります。
下記のように、RealmConfigurationオブジェクトのObjectClassesプロパティを用いて、それぞれのRealmに保存されるモデルクラスを制限することができます。
class LoneClass : RealmObject
{
public string Name { get; set;}
}
var config = new RealmConfiguration("RealmWithOneClass.realm");
config.ObjectClasses = new Type[] {typeof(LoneClass)};
// Realmに保存されるクラスを指定した2つだけにする
config.ObjectClasses = new Type[] {typeof(Dog), typeof(Cat)};
ファイルサイズを最適化(コンパクション)する
アプリケーションの使用中に、Realmによって使用されるメモリが断片化され、必要以上に多くの領域が占有される可能性があります。RealmConfiguration.ShouldCompactOnLaunchを指定することで、Realmを開く際に内部ストレージを再編成し、可能な場合にファイルサイズを減らすようにすることができます。
var config = new RealmConfiguration
{
ShouldCompactOnLaunch = (totalBytes, usedBytes) =>
{
// totalBytes refers to the size of the file on disk in bytes (data + free space)
// usedBytes refers to the number of bytes used by data in the file
// Compact if the file is over 100MB in size and less than 50% 'used'
var oneHundredMB = 100 * 1024 * 1024;
return totalBytes > oneHundredMB && (double)usedbytes / totalBytes < 0.5;
}
};
try
{
var realm = Realm.GetInstance(config);
}
catch (Exception e)
{
// handle error compacting or opening Realm
}
RealmファイルのコンパクションはShouldCompactOnLaunch
デリゲートがtrue
を返し、かつRealmがまだ使用されてない場合に実行されます。
Realmインスタンスを必要としない別の方法として、Realm.Compact(config)があります。
var config = new RealmConfiguration("my.realm");
Realm.Compact(config);
同期されたRealmに対するコンパクションはサポートしていません。コンパクションを実行するとトランザクション履歴が失われるためです。同期には履歴の情報が必要なためです。
コンパクションに関する注意事項
- コンパクションの対象ファイルを開いているRealmインスタンスがある場合はコンパクションは実行できません。
- ディスクには同じRealmファイルをコピーできるだけの空き容量が必要です。
- コンパクションに失敗した場合、Realmファイルはそのままで変更されません。
- このメソッドは成功すると
true
を返し、他に同じファイルを開いているRealmインスタンスがある場合はfalse
を返します。 - 現時点ではコンパクションを実行した結果のファイルサイズを見積もるためのAPIは提供していません。推奨する方法としては、起動時にファイルのサイズをチェックし、一定の値を超えた場合にコンパクションを実行します。
Realmファイルを削除するには
キャッシュを消去する、データをすべてリセットするなど、いくつかのケースにおいて、Realmファイルを完全にディスクから削除することが適していることがあります。
普通のファイルと異なり、Realmのファイルはメモリにマッピングされており、RealmファイルはRealmインスタンスが生存している間、有効でなければなりません。
アプリケーションコードで、Realmの細かなファイルについて意識しなくて済むように、Realm.DeleteRealm(RealmConfiguration)という便利なメソッドを提供しています。
var config = new RealmConfiguration("FileWeThrowAway.realm");
Realm.DeleteRealm(config);
var freshRealm = Realm.GetInstance(config);
マルチスレッド
独立したスレッドでRealmを使っている限りは、Realmのオブジェクトはすべて一般的なオブジェクトと同じように扱え、並行処理やマルチスレッドについて気にする必要はありません。 Realmにアクセスするためにロックや排他処理を考える必要はありません(たとえRealmが他のスレッドから同時に更新されるとしても)。そして、データの変更が起こるのはトランザクションブロックで囲まれた範囲だけと考えられます。
Realmは並行処理を簡単に扱えるようにするために、それぞれのスレッドで常に一貫性を保ったデータを返します。数多くのスレッドからRealmを同時に操作したとしても、それぞれのスレッドごとにスナップショットのデータが返され、不整合な状態が見えることがありません。
1つだけ注意しなければならないことは、複数のスレッドをまたいで同じ Realmインスタンス を共有することはできないということです。もし、複数のスレッドで同じオブジェクトにアクセスする必要がある場合は、それぞれのスレッドでRealmインスタンスを取得する必要があります。(そうでなければデータが不整合に見える可能性があります。)
他のスレッドから更新されたデータを反映するには
メインスレッド(またはランループ/Looperスレッドを持つサブスレッド)では、ランループが回るごとに自動的に他のスレッドから更新されたデータが反映されます。それ以外のタイミングでは、その時点のスナップショットのデータが返され、他のスレッドでデータが更新されているかどうかを気にすることなく、常に一貫性のあるデータが見えることになります。
それぞれのスレッドで最初にRealmファイルをオープンしたとき、Realmの状態は最後のコミットが成功した状態にあります。そしてそれは次の更新が反映されるまでそのままです。 Realmはランループが回るたびに自動的に最新のデータに更新されます。 もしスレッドがランループを持っていない(一般的なバックグラウンドスレッド)場合は、最新のデータを反映するためにRealm.Refresh()メソッドを自分で呼ぶ必要があります。
また、トランザクションがコミットされたときも最新のデータが反映されます(Transaction.Commit())。
定期的な最新データの反映を行わないでいると古いデータがいつまでも保持され最新のデータまでのすべての差分を保持し続けることになり、ファイルサイズの肥大につながります。この現象について詳しくは現バージョンにおける制限事項をご覧ください。
スレッド間でオブジェクトを受け渡す
永続化されたRealm、RealmObject、Realm.Allメソッドから取得されたIQueryable
オブジェクト、またはRealmObjectのIList
プロパティのオブジェクトは、生成されたスレッド内でなければ利用することができません。 別のスレッドで利用されると例外が発生します。これはRealmがトランザクションを分離するための必要な仕様です。
Realmでは安全にスレッド間で上記のオブジェクトを受け渡す方法を提供しています。
- ThreadSafeReference.Createメソッドを用いてThreadSafeReferenceオブジェクトを作成します。このメソッドは上記のそれぞれの型についてオーバーロードがあります。
- ThreadSafeReferenceオブジェクトを別のスレッド、またはディスパッチキューに渡します。
- リファレンスをオブジェクトが保存されているRealmのRealm.ResolveReferenceメソッドを用いて解決します。返されるオブジェクトは通常のRealmオブジェクトです。
(例)
var person = new Person { Name = "Jane" };
realm.Write(() => realm.Add(person));
var personReference = ThreadSafeReference.Create(person);
Task.Run(() =>
{
var otherRealm = Realm.GetInstance(realm.Config);
var otherPerson = otherRealm.ResolveReference(personReference);
if (otherPerson == null)
{
return; // person was deleted
}
realm.Write(() =>
{
otherPerson.Name = "Jane Doe";
});
});
ThreadSafeReferenceオブジェクトが解決できるのは一度きりです。リファレンスによって固定されたバージョンは、インスタンスが解放されるまで固定されたままです。そのためThreadSafeReferenceはできるだけ短い期間で使い終わるべきで。長い間保持することは推奨しません。
ThreadSafeReferenceはRealmObject
、IList<RealmObject>
、IQueryable<RealmObject
に対して使用できます。Realmに保存されているオブジェクトに対してのみ使用すべきです。Realmに保存する前のオブジェクトはスレッド間で受け渡すことができます。
複数スレッド間でRealmを使う
同じRealmファイルを複数のスレッドから使う場合は、各スレッドでRealmインスタンスをそれぞれ生成する必要があります。 同じ設定によって作られたRealmインスタンスは、ディスク上で同じものを指します。
複数のスレッド間で、Realmインスタンスを共有することはサポートされていません。 同じRealmファイルにアクセスするRealmインスタンスは、すべて同じ設定(RealmConfiguration)でなければなりません。
Realmは、大量のデータを追加するときには、一つのトランザクション中に複数の一括更新をすることにより、非常に効率よく動作します。
Realmオブジェクトはスレッドセーフではないため、複数のスレッド間で共有することができません。それぞれのスレッド/ディスパッチキューでRealmオブジェクトを生成する必要があります。
非同期の書き込み
Realm.WriteAsync()メソッドはWrite
メソッドの特殊形です。このメソッドを利用すると、書き込みを簡単にUIスレッドからバックグラウンドスレッドに逃がすことができます。
注意: WriteAsyncは一時的に別のRealm Realmインスタンスを作ります。このインスタンスはラムダ式のパラメータとして渡されます。下記の例にあるように、ラムダの中ではパラメータとして渡されたインスタンスを使うように気をつけてください。元のスレッドのRealmを使わないように注意してください。ラムダは外側の変数をキャプチャするので、コンパイルエラーは起こらずに、実行時エラーが発生することになります。
await realm.WriteAsync(tempRealm =>
{
var pongo = tempRealm.All<Dog>().Single(d => d.Name == "Pongo");
var missis = tempRealm.All<Dog>().Single(d => d.Name == "Missis");
for (var i = 0; i < 15; i++)
{
tempRealm.Add(new Dog
{
Breed = "Dalmatian",
Mum = missis,
Dad = pongo
});
}
});
バックグラウンドスレッドにおける処理が完了すると、WriteAsyncメソッドは元のUIスレッドのデータを最新の状態に更新するためにRealm.Refresh()を呼びます。そのことにより、バックグラウンドスレッドで行われた変更は即座に通知されます。
すでにワーカースレッドで動作しているコードからWriteAsyncを呼んだ場合は、単にWriteの呼び出しに変えられ、余計なスレッドが作られるなどのオーバーヘッドは発生しません。そのためどのスレッドからでも安全にこのメソッドを使用できます。バックグラウンドスレッドからWriteAsyncを呼ぶ際に重要なことは、そのとき使われているRealmインスタンスがそのまま使われ、新しいインスタンスを作成するわけではないということです。
一般的にはRealmの書き込みは非常に高速なので、UIの操作による少量のデータの変更についてはWriteメソッドを使用することを推奨します(ユーザーのテキスト入力やチェックボックスを変更するなど)。WriteAsyncが有効なシチュエーションとしては、数百から数千もの変更を一度に行う(例えば、ネットワークから取得した大量のデータを保存するなど)場合で、UIを止めてしまう恐れのあるときです。
通知
Realmに対する通知
リスナーに登録しておくと、Realmインスタンスは、他のスレッドやプロセスでトランザクションが完了するたびに、他のRealmインスタンスに対して通知を送ります。
realm.RealmChanged += (s, e) =>
{
// Update UI
}
現在はRealmChangedによる通知は変更内容の情報は提供されないことに注意してください。変更内容を知る必要があるならオブジェクトに対する通知またはコレクションに対する通知を使用します。
オブジェクトに対する通知
RealmObjectクラスはINotifyPropertyChangedインターフェースに準拠しているので、プロパティの変更を監視できます。プロパティに変更があるたびにPropertyChangedEventメソッドが呼ばれます。
public class Account : RealmObject
{
public long Balance { get; set; }
}
下記のように、PropertyChangedEventイベントをクラスに対して監視すると、Balance
プロパティに変更があるたびにイベントハンドラが呼び出されます。
var account = new Account();
realm.Write(() => realm.Add(account));
account.PropertyChanged += (sender, e) =>
{
Debug.WriteLine($"New value set for {e.PropertyName}");
}
realm.Write(() => account.Balance = 10); // => "New value set for Balance"
監視を設定するタイミングは、Realmにオブジェクトを保存する前でも後でもどちらでも構いません。変更のイベントはどちらの場合でも同じように発生します。
これだけでも便利な機能ですが、さらにXamarin.Formsを使ったデータバインディングも可能です。詳細は、XamarinドキュメントのデータバインディングからMVVMへを参照してください。
コレクションに対する通知
Realm.All<T>()メソッド、またはIQueryable
LINQクエリから取得したIQueryable
オブジェクトはは自動更新されます。常に最新の状態に自動的にアップデートされるということです。アクセスしたときには常に最新の変更が反映された値が取得されます。
同様に、RealmObjectのIList
プロパティは、自動更新される1対多の関連です。つまり、アクセスするたびに、関連オブジェクトの最新のコレクションが取得されます。
これらのコレクションの実際のオブジェクトはIRealmCollection<T>に準拠しています。つまり変更を監視するための2つの仕組みが使えることを意味しています。Realmでは便利な拡張メソッドとしてAsRealmCollectionを提供しています。このメソッドを用いてIList<T>
またはIQueryable<T>
をIRealmCollection<T>
にキャストできます。
.NET標準のINotifyCollectionChanged
を使用するとMVVMデータバインディングの仕組みとうまく連携できます。IRealmCollection<T>をビューに直接渡すことができます。
UITableView
や ListView
を直接(例えばXamarin Native UIプロジェクトで)操作するのに便利な、より詳細な変更情報が必要なら、SubscribeForNotifications拡張メソッドを使用します。このメソッドには、変更セットを使用して呼び出されるデリゲートから追加、削除、または変更されたことを取得できます。
var token = realm.All<Person>().SubscribeForNotifications ((sender, changes, error) =>
{
// Access changes.InsertedIndices, changes.DeletedIndices, and changes.ModifiedIndices
});
// Later, when you no longer wish to receive notifications
token.Dispose();
マイグレーション
データベースを使ってる場合、時間が経つにつれ、データモデルは変更されていくものです。 Realmでのデータモデルは、標準的なC#インターフェースで定義されていますので、インターフェースに変更を加えるだけで、簡単にデータモデルを変えられます。 例えば次のようなPerson
モデルがあるとします。
public class Person : RealmObject
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
ここでFirstName
プロパティとLastName
プロパティを統合して1つのFullName
に変更したくなったとします。その場合モデルのインターフェースを次のように変更します。
public class Person : RealmObject
{
public string FullName { get; set; }
public int Age { get; set; }
}
このとき、元のデータモデルを使ってすでにデータを保存していた場合は、既存のRealmファイルとコード上のクラス定義に不一致が発生します。ファイルを開こうとした際に不一致が検出されると例外が発生します。その場合はマイグレーションを実行しない限りは続行できません。
マイグレーションを実行する
スキーマの不一致を解決するもっとも簡単な方法はスキーマバージョンを上げることです。スキーマバージョンを指定しなかった場合は自動的にゼロがセットされています。モデル定義を変更した場合は、スキーマバージョンを前よりも大きな値にセットします。スキーマバージョンの指定はConfigurationオブジェクトのプロパティに設定します。これはモデルの変更が意図的かどうかを判断するために必要な作業になります。
var config = new RealmConfiguration() { SchemaVersion = 1 };
var realm = Realm.GetInstance(config);
この状態で既存のファイルを開くと、Person
モデルのFirstName
プロパティとLastName
プロパティに記録されたデータはすべて削除され、新しくFullName
プロパティが追加されます。FullName
プロパティには空の文字列が設定されます。Age
プロパティは以前のデータがそのまま残っています。おそらく本当に実現したいことは古いデータを移行することでしょう。そのためにはRealmのマイグレーションハンドラにデータを移行する処理を書かなければなりません。マイグレーションハンドラはMigrationCallback関数を用います。この関数はMigrationオブジェクトを引数として受け取ります。Migrationオブジェクトは2つのRealm型のプロパティ、OldRealmとNewRealmを持ちます。これらを使って古いデータモデルから新しいデータモデルにデータをコピーする処理を記述します。
クラスやプロパティの名前を変更した場合も、Realmは自動的に何が変更されたかは検知できないので、通常はマイグレーション処理を記述する必要があります。
このコールバック関数は古いスキーマバージョンのファイルを開く際に1回だけ呼び出されます。複数のバージョンに渡るマイグレーションがある場合は、すべてのバージョンに対してマイグレーションを行う必要があります。
この例では新しいPerson
クラスはFirstName
プロパティとLastName
プロパティが無くなっているので、そのデータに通常の方法ではアクセスできません。そこで動的APIを利用します。
var config = new RealmConfiguration
{
SchemaVersion = 1,
MigrationCallback = (migration, oldSchemaVersion) =>
{
var newPeople = migration.NewRealm.All<Person>();
// Use the dynamic api for oldPeople so we can access
// .FirstName and .LastName even though they no longer
// exist in the class definition.
var oldPeople = migration.OldRealm.All("Person");
for (var i = 0; i < newPeople.Count(); i++)
{
var oldPerson = oldPeople.ElementAt(i);
var newPerson = newPeople.ElementAt(i);
newPerson.FullName = oldPerson.FirstName + " " + oldPerson.LastName;
}
}
};
var realm = Realm.GetInstance(config);
さらにアプリが更新されて、モデルに対して別の変更が行われスキーマバージョンを増加させたとします。アプリがすべて最新の状態に更新されているとは限らないので、oldSchemaVersion
パラメータをチェックして、必要なマイグレーションを順番に実行しなければなりません。
ここではPerson
クラスにさらに変更を加え、Birthday
プロパティを追加したとします。
public class Person : RealmObject
{
public string FullName { get; set; }
public int Age { get; set; }
public DateTimeOFfset Birthday { get; set; }
}
上記は変更したモデルです。マイグレーションの処理は下記のようになります。
var config = new RealmConfiguration
{
SchemaVersion = 2,
MigrationCallback = (migration, oldSchemaVersion) =>
{
var newPeople = migration.NewRealm.All<Person>();
var oldPeople = migration.OldRealm.All("Person");
for (var i = 0; i < newPeople.Count(); i++)
{
var oldPerson = oldPeople.ElementAt(i);
var newPerson = newPeople.ElementAt(i);
// Migrate Person from version 0 to 1: replace FirstName and LastName with FullName
if (oldSchemaVersion < 1)
{
newPerson.FullName = oldPerson.FirstName + " " + oldPerson.LastName;
}
// Migrate Person from version 1 to 2: replace Age with Birthday
if (oldSchemaVersion < 2)
{
newPerson.Birthday = DateTimeOffset.Now.AddYears(-(int)oldPerson.Age);
}
}
}
};
var realm = Realm.GetInstance(config);
暗号化
Please take note of the Export Compliance section of our LICENSE, as it places restrictions against the usage of Realm if you are located in countries with an export restriction or embargo from the United States.
Realmでは64バイトの暗号化キーを用いてAES-256+SHA-2暗号化方式でデータベースファイルを暗号化する機能を提供しています。
var config = new RealmConfiguration("Mine.realm");
config.EncryptionKey = new byte[64] // キーは必ずこの大きさでなければなりません
{
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18,
0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28,
0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38,
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78
};
var realm = Realm.GetInstance(config); // "Mine.realm"というファイル名で暗号化されたRealmファイルを作成します
この機能を使用すると、ディスクに保存されるデータが透過的にAES-256で暗号/複合され、SHA-2 HMACによって検証されます。
暗号化したRealmファイルのインスタンスを作成するには同じ暗号化キーが必要になります。
アプリケーションごとに異なるキーを使ってください。 けっして 上記のサンプルコードをコピー&ペースト しないでください 。ユーザーごとに異なるキーを使用したい場合は、Xamarin.Auth APIを参考にしてください。
暗号化したRealmファイルを開く際に、間違ったキーを指定したりキーを渡さなかった場合は、GetInstanceを呼ぶタイミングでRealmFileAccessErrorExceptionが発生します。
暗号化したRealmを使う場合、わずかに(10%未満)パフォーマンスが下がります。
同期
Realm Mobile PlatformはRealm Mobile Databaseを拡張し、ネットワークを通じてデバイス間の自動的なデータ同期を実現します。同期されたRealmに対応するために、いくつかの新しい型とクラスが導入されました。下記以降でRealm Mobile Databaseに新しく同期のために追加されたクラスを説明します。
Realm Mobile Platformを有効にする
Realm Mobile Platformを有効にするために特別な手順は必要ありません。NuGetのRealmパッケージにデフォルトで含まれています。有料の機能についてはSyncConfiguration.SetFeatureTokenメソッドにアクセストークンを設定することで機能をアンロックする必要があります。アクセストークンはトライアルのサインアップまたはProfessional Edition、Enterprise Editionを購入した際のEメールに記載されています。
同期はWindowsとUWP環境では利用できません。iOSまたはAndroid、macOS、Linux環境でのみ利用できます。
Using Synchronized Realms on Linux
Linux環境で同期されたRealmを使用するにはProfessional Edition以上が必要です。SyncConfiguration.SetFeatureToken
を呼ばずにRealmを開こうとすると、RealmFeatureUnavailableExceptionが発生します。
Userクラス
Realmユーザー(User)はRealm Object Serverの中心となるクラスです。User
クラスはユーザー名+パスワードによる認証に加え、SNSを使う方法などいくつかのサードパーティの認証方式をサポートしています。
ユーザーの作成、およびログインは次の2つの値が必要です。
- 接続先のRealm ServerのUri
- ユーザーを認証するための認証情報(Credentials)。認証情報は認証方式により変わります。(例: ユーザー名+パスワード、アクセストークンなど)
この2つのオブジェクトはUserオブジェクトを作成するために使われます。
認証
ユーザーログインには認証が必要です。どのような認証プロバイダがサポートされているかについては、Realm Object Serverの認証についてのドキュメント をご覧ください。
ユーザー認証に使用する アカウント情報 はCredentialsオブジェクトによって表されます。このオブジェクトは次に示すさまざまな方法で作成できます。
- ユーザー名/パスワードの組み合わせ。
- 組み込みのサードパーティ認証サービスによって認証されたアクセストークン。
- カスタム認証プロバイダによって認証されたトークン(カスタム認証をご覧ください)。
ユーザー名とパスワードによる認証はRealm Object Server内に完全に閉じています。ユーザーに関するコントロールをすべて自分自身で行うことができます。その他の認証サービスを利用する場合は、アプリケーション内で外部のサービスに接続し、認証トークンを取得する必要があります。
下記に数種類のアカウント情報の作成方法を示します。
ユーザー名/パスワード
var credentials = Credentials.UsernamePassword(username, password, createUser: false);
UsernamePassword()メソッドの3番目の引数であるcreateUser
はユーザーを新らしく作成するかどうかを示すフラグです。true
を指定できるのは作成する最初の1度だけです。ユーザーが作成済みですでに存在する場合はfalse
を指定しなければなりません。
またはすべてのユーザーをあらかじめWebの管理者ダッシュボードから作成しておき、createUser
メソッドには常にfalse
を渡すという運用も可能です。
ユーザーのパスワードを変更するにはUser.ChangePasswordメソッドを使用します。
var currentUser = User.Current;
await currentUser.ChangePassword("new-secure-password");
// The user's password has been changed, they'll no longer be able to use their old password.
管理者ユーザーはユーザーIDが分かっていれば別のユーザーのパスワードを変更できます。
var adminUser = await User.LoginAsync(...);
await adminUser.ChangePasswordAsync("some-user-id", "new-secure-password");
// The password of the user with Id some-user-id has been changed to new-secure-password.
ユーザー名+パスワードの認証を使用する場合は、ユーザーの情報を中間攻撃から守るために、HTTPSの利用を強く推奨します。
var token = "..."; // a string representation of a token obtained by Google Login API
var credentials = Credentials.Google(token);
var token = "..."; // a string representation of a token obtained by Facebook Login API
var credentials = Credentials.Facebook(token);
##### Azure Active Directory
```csharp
var token = "..."; // a string representation of a token obtained by logging in with Azure Active Directory
var credentials = Credentials.AzureAD(token);
カスタム認証
var token = "..."; // a string representation of a token obtained from your custom provider
var credentials = Credentials.Custom("myAuth", token, null);
3番目の引数にディクショナリの形式でカスタム認証プロバイダが必要とする追加の認証情報を渡すことができます。詳しくはAPIリファレンスをご覧ください。
ログイン
ユーザーを作成するために必要な情報が揃ったなら、Realm Object Serverにログインできます。
var authURL = new Uri("http://my.realm-auth-server.com:9080");
var user = await User.LoginAsync(credentials, authURL);
ログアウト
同期されたRealmからログアウトするには次のようにします。
user.LogOut();
ログアウトすると同期は停止します。ログアウトしたユーザーは同期されたRealmファイルを開くことはできなくなります。
ユーザーの保存方法のカスタマイズ
デフォルトでは一度ログインしたユーザーは自動的にRealmによって保存されます。アプリケーションを起動するたびにログインする必要はありません。ユーザーを保存するかどうかはUser.ConfigurePersistenceを用いて変更できます。保存に関する挙動は3つのモードがあります。
Disabled
: ユーザーオブジェクトは保存されません。自分でユーザーの認証情報を保存するか、アプリケーションを起動するたびにいつでもログインする必要があります。NotEncrypted
: ユーザーオブジェクトを暗号化せずに保存します。ユーザーの認証情報を守るために、このオプションを使用することは推奨しません。Encrypted
: ユーザーの認証情報を暗号化して保存します。このオプションを選択する場合は正しくencryptionKey
を指定する必要があります。
iOS環境ではデフォルトでEncrypted
が指定されており、指定されたencryptionKey
を用いてKeychainび暗号化してユーザーを保存します。AndroidではデフォルトではNotEncrypted
が指定されています。AndroidKeyStoreのAPIはAPIレベルが18以上でなければ使用できないためです。可能ならばAndroid環境ではEncrypted
モードに変更しencryptionKey
をアプリケーションごとに指定することを推奨します。
ユーザーオブジェクトを使用する
通常の組み込みデータベースとしてのRealmでは、Realmの設定を変更するためにRealmConfigurationを使用します。Realm Mobile PlatformではSyncConfigurationを代わりに利用します。
設定オブジェクトはユーザーと同期サーバーのURLに関連づけられています。同期サーバーのURLはチルダ(“~”)を含むことがあります。チルダは各ユーザーのユニークIDに透過的に変換されます。この仕組みは、ユーザーごとに別のRealm URLを指定する場合に非常に便利です。
同期されたRealmファイル保存される場所はフレームワークによって管理されています。保存場所は必要に応じて変更できます。
var serverURL = new Uri("realm://my.realm-server.com:9080/~/default");
var configuration = new SyncConfiguration(user, serverURL);
ユーザーを保存する機能を有効にしている場合は、User.Currentを用いてログイン済みのユーザーを取得できます。ログイン済みユーザーが存在しない、またはすべてのユーザーがログアウトしていたならnull
が返されます。複数のユーザーがログイン中の場合はRealmExceptionが発生します。
var user = User.Current;
複数のユーザーがログイン中であると考えられる場合は、User.AllLoggedInを用いてログイン中のすべてのユーザーのコレクションを取得できます。ログイン中のユーザーが存在しなければ空のコレクションが返ります。
var users = User.AllLoggedIn;
foreach (var user in users)
{
// do something with the user
}
同期されたRealmを開く
SyncConfigurationを作成できたら、同期されたRealmインスタンスは通常のRealmと同じように作成できます。
var realm = Realm.GetInstance(configuration);
同期セッション
同期されたRealmとRealm Object Serverとの接続はSessionオブジェクトとして表されます。 セッションオブジェクトはrealm.GetSession()メソッドを用いて取得します。
セッションの状態を知るにはStateプロパティを使います。このプロパティからセッションがアクティブ、サーバーと未接続、またはエラー状態のいずれかであることがわかります。
同期の進捗を通知
セッションオブジェクトからsession.GetProgressObservable(direction, mode)を用いてIObservableオブジェクトを取得し、監視することでアプリとObject Serverの同期(アップロード、ダウンロード両方)の進捗状態を監視できます。
同期の進捗を通知するコールバックは同期システムによって継続的に呼び出されます。複数のコールバックを同時に登録でき、ブロックはアップロードとダウンロードの両方について設定することができます。コールバックにはSyncProgressオブジェクトが渡され、全体のバイト数と、そのうちの転送済みのバイト数が含まれています(全体のバイト数は、転送済みのバイト数と未転送のバイト数として渡されます)。
コールバックが登録されると、IDisposable型のトークンオブジェクトを返します。通知を有効にする間はこのオブジェクトを保持しておかなければなりません。通知を停止するにはトークンオブジェクトに対してDisposeメソッドを呼びます。
通知には2つのモードがあります。
ReportIndefinitely
- 通知を停止しない限りは、データの転送が行われるたびに何度でも継続して通知ブロックが呼び出されます。この種類の通知ブロックは例えばネットワークのインジケータを表示するために使用できます。例えば通信が発生するとインジケータを表示し、アップロードとダウンロードで色を変えて区別する、などです。
var token = session.GetProgressObservable(ProgressDirection.Download, ProgressMode.ReportIndefinitely)
.Subscribe(progress =>
{
if (progress.TransferredBytes < progress.TransferableBytes)
{
// Show progress indicator
}
else
{
// Hide the progress indicator
}
});
ForCurrentlyOutstandingWork
- 登録されたときに、転送されるべきバイト数を取得し、その値を基準に進捗が通知されます。この通知は転送バイト数がその初期値に達するか超えると、自動的に通知を解除します。この種類の通知は、例えば初回のRealmファイルの同期が完了するまでの進捗状況の表示や、大きめのデータのアップロードの進捗表示に利用できます。
var token = session.GetProgressObservable(ProgressDirection.Download, ProgressMode.ForCurrentlyOutstandingWork)
.Subscribe(progress =>
{
var progressPercentage = progress.TransferredBytes / progress.TransferableBytes;
progressBar.SetValue(progressPercentage);
if (progressPercentage == 1)
{
progressBar.Hide();
}
});
ここで示しているコード例は、通知の登録を簡単にするためにObservableExtensions.Subscribe拡張メソッドを使用しています。Realmでは通知のコードをシンプルにするためにReactive Extensionsクラスライブラリを使用することを推奨しています。例えば、下記はさらに複雑な通知のシナリオを表しています。アップロードとダウンロードの両方を監視し、通知の回数を制限し、最終的にメインスレッドにディスパッチします。
var uploadProgress = session.GetProgressObservable(ProgressDirection.Upload, ProgressMode.ReportIndefinitely);
var downloadProgress = session.GetProgressObservable(ProgressDirection.Download, ProgressMode.ReportIndefinitely);
var token = uploadProgress.CombineLatest(downloadProgress, (upload, download) =>
{
return new
{
TotalTransferred = upload.TransferredBytes + download.TransferredBytes,
TotalTransferable = upload.TransferableBytes + download.TransferableBytes
};
})
.Throttle(TimeSpan.FromSeconds(0.1))
.ObserveOn(SynchronizationContext.Current)
.Subscribe(progress =>
{
if (progress.TotalTransferred < progress.TotalTransferable)
{
// Show spinner
}
else
{
// Hide spinner
}
});
アクセスコントロール
Realm Mobile Platformでは、どのユーザーが同期されたRealmにアクセスできるのかを柔軟にコントロールできる仕組みがあります。 例えば、1つのRealmを複数のユーザーが共同で編集するようなアプリを作ることもできます。 また、あるユーザーのみが編集できるファイルを公開し、他のユーザーは読み取り専用で閲覧だけを可能にするといったことも可能です。
Realmのアクセス権限には3種類のレベルが存在します。
Read
は読み取り権限が与えられていることを示します。Write
は書き込み権限が与えられていることを示します。Manage
はそのRealmに対するアクセス権限を変更できることを示します。
アクセス権限を明示的に与えなかった場合は、デフォルトの動作ではRealmファイルの作成者だけがそのファイルにアクセスできます。 唯一の例外はAdminユーザーです。Adminユーザーは常にすべてのRealmファイルに対してすべてのアクセス権限を持ちます。
アクセス権限の概念についてより詳しく知るには、Realm Object Serverドキュメントのアクセスコントロールセクションをご覧ください。
アクセス権限を取得する
各ユーザーに付与されたアクセス権限をすべて取得するにはUser.GetGrantedPermissionsAsyncメソッドを使用します。
var permissions = await user.GetGrantedPermissionsAsync(Recipient.CurrentUser, millisecondTimeout: 2000);
// Permissions is a regular query
var writePermissions = permissions.Where(p => p.MayWrite);
// Queries are live and emit notifications
writePermissions.SubscribeForNotifications((sender, changes, error) =>
{
// handle permission changes
});
特定のユーザーによって与えられたアクセス権限を取得するにはRecipient.OtherUserパラメータを渡します。 サーバーからの応答をタイムアウトする時間はmillisecondTimeout
で指定します。タイムアウトした場合は、一番最後に取得した状態が返されます。そのため、クライアントがオフラインであっても、引き続きデータが表示できる場合があります。
アクセス権限を変更する
Realmファイルのアクセス権限を変更するには、直接アクセス権限を変更する方法と、リクエストベースの2つの方法があります。
アクセス権限の付与
アクセス権限の変更(付与および取り消し)はUser.ApplyPermissionsAsyncメソッドを使用して、Realmファイルに対するアクセス権限を直接変更できます。
var condition = PermissionCondition.UserId("some-user-id");
var realmUrl = "realm://my-server.com/~/myRealm";
await user.ApplyPermissionsAsync(condition, realmUrl, AccessLevel.Read);
PermissionConditionを生成する方法は以下の2つのファクトリメソッドのいずれかを使用します。
UserId
- ユーザーIDを用いてアクセス権を付与するユーザーを指定する場合に使用します。(ユーザーIDはRealmが内部的に生成したIDです)Email
- ユーザー名/パスワードの認証プロバイダを使用している場合にEメールアドレス(ユーザー名)を用いてアクセス権を付与するユーザーを指定できます。
最後の引数によってユーザーに付与されるアクセス権のレベルを指定します。より高いレベルの権限は低いレベルの権限を含みます。たとえば、Write
はRead
を含んでいます。Admin
はRead
とWrite
権限を含みます。AccessLevel.None
を指定するとユーザーからRealmに対するアクセス権限を取り消します。
Offer/Response
OfferPermissionsAsyncメソッドから返されるトークンを用いて、Realmファイルを共有する仕組みです。
var realmUrl = "realm://my-server.com/~/myRealm";
var expiration = DateTimeOffset.UtcNow.AddDays(7);
var token = await userA.OfferPermissionsAsync(realmUrl, AccessLevel.Write, expiresAt: expiration);
必要に応じてexpiresAt
引数を使って、リクエストの期限を設定できます。期限の切れたトークンではRealmのアクセス権を変更することはできません。すでにトークンを使用して獲得した権限については、期限が過ぎたとしても失われることはありません。アクセス権を取り消す場合はApplyPermissionsAsyncを使用します。
受け取ったトークンはメッセージアプリで送ったり、QRコードとして表示したりして、別のユーザーに共有します。別のユーザーは、受け取ったトークンを使用してパーミッションのリクエストを了承します。
var token = "...";
var realmUrl = await userB.AcceptPermissionOfferAsync(token);
// Use the realmUrl to open the Realm
var config = new SyncConfiguration(userB, new Uri(realmUrl));
var realm = await Realm.GetInstanceAsync(config);
既存のOffer/Changeの状態を取得する
Offerを無効にしたり、アプリを再起動した際にChangeオブジェクトを変更通知リスナーで再び監視したい場合など、既存の管理オブジェクトを取得したいことがあります。
上記に示したアクセス権を変更するための各メソッドで作られるオブジェクトを取得するAPIは以下の3種類です。
OfferPermissionsAsync
によって作られたオブジェクトを取得するにはGetPermissionOffersメソッドを使用します。AcceptPermissionOfferAsync
によって作られたオブジェクトを取得するにはGetPermissionOfferResponsesメソッドを使用します。ApplyPermissionsAsync
によって作られたオブジェクトを取得するにはGetPermissionChangesメソッドを使用します。
上記のメソッドはいずれもManagementObjectStatus型のparams
引数を取り、オブジェクトをフィルタリングできます。
NotProcessed
- まだ処理されていないオブジェクトを取得します。(クライアントがオフラインであった場合などに有効です。)Success
- 処理が成功した(アクセス権の変更が適用された)オブジェクトを取得します。Error
- 問題があり、処理中にエラーが発生したオブジェクトを取得します。
上記のメソッドはいずれもIQueryable
型のオブジェクトを返します。そのため、さらにフィルタリングしたり変更通知(詳しくはコレクションに対する通知をご覧ください)を使用できます。
PermissionOffer
を無効にして、新たにトークンが使用されることを止めたい場合はInvalidateOfferAsyncメソッドを使用します。
// Get all offers that grant write permissions and have no expiration date
var offers = user.GetPermissionOffers(ManagementObjectStatus.Success)
.Where(s => s.MayWrite && s.ExpiresAt == null);
foreach (var offer in offers)
{
await user.InvalidateOfferAsync(offer);
}
エラー処理
同期関連のAPIのうち、失敗する可能性のある操作については例外が発生するものがあります。例外はtry-catchブロックで捕捉できます。
合わせて、Session.Errorイベントを監視することを 強く推奨 します。グローバルな同期サブシステム、または特定のセッション(セッションは同期サーバーと関連したRealmを表します)により発生するエラーはこのハンドラによって通知されます。エラーば発生すると、エラーハンドラが呼び出されます。ハンドラにはErrorEventArgsパラメータが渡され、例外オブジェクトと、エラーが発生したSessionオブジェクトが含まれています。
Session.Error += (session, errorArgs) =>
{
// handle error
};
エラーについて
Realm Mobile PlatformのエラーはSessionExceptionsとして表されます。標準の例外が持っているプロパティに加えて、ErrorCodeが使用できます。このプロパティにより、エラーの種類を強い型付けによって区別することができます。
Session.Error += (session, errorArgs) =>
{
var sessionException = (SessionException)errorArgs.Exception;
switch (sessionException.ErrorCode)
{
case ErrorCode.AccessTokenExpired:
case ErrorCode.BadUserAuthentication:
// Ask user for credentials
break;
case ErrorCode.PermissionDenied:
// Tell the user they don't have permissions to work with that Realm
break;
case ErrorCode.Unknown:
// Likely the app version is too old, prompt for update
break;
// ...
}
};
クライアントリセット
Realm Object Serverがクラッシュしてバックアップから復元しなければならなくなったときは、アプリに対して クライアントリセット を要求することがあります。これはローカルのRealmがサーバーのRealmよりも新しくなってしまったときに起こります(例えば、Realm Object Serverのバックアップ後にアプリケーションから変更があった場合は、復元した際にその変更は失われるのでアプリケーションがサーバーより新しい状態になります)。
クライアントリセットは次のような処理を行います。ローカルのRealmファイルのバックアップを作成します。その後オリジナルのRealmファイルは削除されます。次にRealm Object Serverに接続したタイミングで、新しくRealmファイルをサーバーからダウンロードします。Realm Object Serverのバックアップ後に行われた変更についてはバックアップには残っていますが復元されません。
クライアントリセットが要求されたかどうかは、Session.Errorオブジェクトのエラーハンドラにて通知されます。クライアントリセットのエラーはsessionException.ErrorCode.IsClientResetErrorをチェックするか、ClientResetExceptionにキャストできるかどうかで判断します。このオブジェクトには追加のBackupFilePathプロパティを持っています。このプロパティはクライアントリセットの処理中に作成されるバックアップコピーの保存先を示します。また、クライアントリセットの処理が開始したときに呼ばれるInitiateClientReset関数もあります。
この関数を自分で呼び出してクライアントリセットの処理を開始するには、関数を呼び出す前にすべてのRealmインスタンスは 解放されていなければなりません 。そうすることでクライアントリセットのプロセスが完了した際にはすぐにRealmをオープンし、同期を再開できます。
もしInitiateClientReset関数を自分で呼ばなかった場合は、クライアントリセットの処理は次にアプリが起動したタイミングで自動的に行われます。最初にRealmにアクセスしたときに行われます。オリジナルのバックアップからデータを移行する必要がある場合は、自分自身で行う必要があります。
クライアントリセットが必要なRealmについてはデータの読み込み、および書き込みは通常と同じようにすることができますが、 その変更はクライアントリセットが完了し、Realmを再ダウンロードしない限りはは同期されません 。この動作について知っておくことは非常に重要です。アプリケーションでは少なくともクライアントリセットのエラーを監視する必要があります。必要ならクライアントリセット後に行われた変更を反映するために処理をする必要があります。
下記の例はクライアントリセットをどのように扱うかの一例を示しています。
void CloseRealmSafely()
{
// Safely dispose the realm instances, possibly notify the user
}
void SaveBackupRealmPath(string path)
{
// Persist the location of the backup realm
}
void SetupErrorHandling()
{
Session.Error += (session, errorArgs)
{
var sessionException = (SessionException)errorArgs.Exception;
if (sessionException.ErrorCode.IsClientResetError())
{
var clientResetException = (ClientResetException)errorArgs.Exception;
CloseRealmSafely();
SaveBackupRealmPath(clientResetException.BackupFilePath);
clientResetException.InitiateClientReset();
}
// Handle other errors
}
}
Realm Object Serverがクライアントリセットをどう扱うかについての詳細はRealm Object Serverのドキュメントをご覧ください。
同期されたRealmのマイグレーション
同期されたRealmについてはマイグレーションは自動的に適用されます。ただし、いくつかの制限と注意事項があります。クラスやプロパティを追加するといった、追加による変更のみ適用されます。追加ではない変更がある場合は例外が発生します。
マイグレーションは必要に応じて自動的に実行されます。スキーマバージョンを設定したり、マイグレーション処理を記述する必要はありません。
コンフリクトの解決
コンフリクトの解決については、Realm Object Serverのドキュメントをご覧ください。
既知の制限事項
Realm Xamarinは現在ベータ版であり、1.0のリリースに向けて、頻繁に昨日の追加や不具合の修正が行われています。ベータ版の間は以下の制限があります。
さらに網羅的に現在の制限を知りたい場合は、GitHub issuesをご覧ください。
これらの未実装の機能は正式版のリリースまでには実装されます。
- 非同期クエリ
- 関連のカスケード削除(ワークアラウンドとしてこちらのStackOverflowの回答が参考になります)
- さらなるLINQクエリのサポート - 詳しくはLINQサポートをご覧ください。
- 関連に対する検索
一般的な制限事項
Realmは、柔軟性とパフォーマンスのバランスをうまく保つため、保存するデータに対していくつかの制限事項があります。
- クラス名は57文字が上限です。UTF-8の文字をサポートしています。超えている場合は例外が発生します。
- プロパティ名は63文字が上限です。UTF-8の文字をサポートしています。超えている場合は例外が発生します。
- iOSにおける制限: 各Realmファイルのサイズは、アプリケーションごとにに割り当てられるメモリサイズを超えてはいけません。割り当てられるメモリサイズは、デバイスごとに異なり、実行時のメモリの断片化にも依存します。(詳しくは、rdar://17119975 をご覧ください)それ以上のデータを保存される場合は、Realmファイルを複数に分割してください。
スレッド
Realmファイルは複数のスレッドから同時にアクセスできますが、RealmインスタンスやRealmオブジェクト、クエリ、検索結果のオブジェクトをスレッド間で受け渡すことはできません。Realmのスレッドについて詳しくはマルチスレッドセクションをご覧ください。.
ファイルサイズと中間データについて
SQLiteなどにデータを保存した時より、ディスクの使用容量が少なくなることを期待されることかと思います。 Realmファイルの容量が考えているよりも大きい場合はRealmObjectは古い履歴データを残している可能性があります。
データの一貫性を保つために、Realmは最新のデータにアクセスしたときのみ履歴をアップデートします。 このことは、別のスレッドが多くのデータを長い時間をかけて書き込んでいる最中にデータを読み出そうとした場合、履歴はアップデートされずに古いデータを読み出すことになります。結果として、履歴の中間データが増加していくことになります。
この余分な領域は、最終的には再利用されるか消去されます。
空き領域をすぐに消去したい場合は、コンパクションのセクションを参照してください。
Windowsデスクトップ環境とUWPにおける制限事項
Realm Xamarinがサポートするすべてのプラットフォーム間で機能の等価性をを確保することは簡単ではありません。現在はWindows環境に特有のいくつかの制限があります。近い将来この制限は修正される予定です。
- 同期はまだ実装されていません。WindowsアプリケーションでRealmパッケージをNuGetに指定すると、ビルド時にエラーが発生します。その代わりに、Realm.Databaseパッケージをインストールして、オフライン対応バイナリだけをインストールする必要があります。
- 暗号化は未サポートです。RealmConfigurationのEncryptionKeyにNull出ない値を’指定してRealm.GetInstanceを呼び出すと、実行時例外が発生します。
- 多くの異なるプロセスが同じRealmファイルを開いていると、一部の変更通知が受け取れないことがあります。
- Realmファイルのコンパクションはサポートしていません。
FAQ
Realmはオープンソースソフトウェアですか?
はい。C++で書かれた内部ストレージエンジン、および各言語のSDKは完全にオープンソースソフトウェアとしてApache 2.0ライセンスの元に公開されています。 拡張コンポーネントであるRealm Platformのソースコードは非公開ですが、このコンポーネントはRealmを組み込みデータベースとして使う場合には必須ではありません。
CI環境でRealmをビルドするには
RealmのビルドタスクはMSBuildによって設定される$(SolutionDir)
プロパティに依存しています。CI環境(VSTS、Jenkins、Appveyorなど)では$(SolutionDir)
をセットせずに、msbuild
コマンドラインツールを個々のプロジェクトに対して使用することがあります。この問題を解決するには/p:SolutionDir=/path/to/solution/folder
をmsbuild
に渡します。たいていのCI環境ではソリューションディレクトリはカレントフォルダ.
ですが、そうでないものもあります。詳しくはCIサーバーのドキュメントをご覧ください。
アプリケーションを起動したときにMixpanelへの通信が発生しているのを確認しましたが、なぜでしょうか?
Realmは匿名の統計データを収集しています。統計データの送信は、Realmが[RealmObject](/docs/dotnet/1.6.0/api/reference/Realms.RealmObject.html)クラスをWeavingする際に行われます。送信するデータに個人を特定する情報は一切含まれません。データにはRealmのバージョン、(iOS、OS Xなどの)プラットフォーム、プログラミング言語、などの情報が含まれ、それはRealmの製品の向上にのみ利用されます。統計データの送信は、リリースビルド、またはデバイス上で動作している時には行われません。あくまでも、ビルド時にFodyがWeavingを実行する時だけ送信されます。実際にどのようなデータを収集しているのかは、ソースコードから確認していただけます。
RealmはPCL(ポータブル・クラス・ライブラリ)として動作しますか?
実行ファイルを作るにはiOS、AndroidまたはWin32のそれぞれのバージョンのRealmを利用する必要があります。しかし、Realm APIに対してコンパイルできるPCLを提供しています。これは「NuGet Bait and Switch Trick」という記事で紹介されているテクニックです。PCLプロジェクトにNuGetを用いてRealmを追加し、それを実行ファイルに追加すれば動作します。
任意の.NETプラットフォームでRealmを使用できるわけではありません 。RealmネイティブのC++ coreに依存しているので、コアを各プラットフォームに移植する必要があるからです。
トラブルシューティング
クラッシュレポート
私たちは開発者の方にクラッシュレポーターを使用していただくことを推奨しています。Realmの操作のうちの多くは(ディスクI/Oを伴う操作などと同様に)実行時に失敗する可能性があります。そのため、クラッシュレポートを収集することは、何が原因で問題が発生したのかを特定し、エラー処理の改善や不具合の修正に有効です。
多くの商用のクラッシュレポーターはログを収集するオプションを提供しています。この機能を有効にすることを強く推奨します。Realm例外が発生した時や、致命的な状態に陥った際にはメタデータの情報をログに出力しており(そこにユーザーデータは一切含まれていません)、それらのログは我々が問題を調査する際に役に立ちます。
Realmの問題や不具合を報告するには
Realmについて問題を発見した際は、GitHubで問題を報告するか、help@realm.ioにEメール、またはSlackで報告してください。その際には、こちらで問題を再現できるように、できるだけ多くの情報をあわせて教えてください。
下記に示す情報は問題を解決するために非常に役に立ちます。
- あなたが実際にやりたいこと・目的。
- 期待している結果。
- 実際に発生した結果。
- 問題の再現方法。
- 問題を再現、または理解できるサンプルコード (そのままビルドして実行できるXcodeプロジェクトだと理想的です) 。
- Realm、Xcode、OS Xのバージョン
- (利用している場合は)CarthageやCocoaPodsなどのパッケージマネージャのバージョン
- 問題の発生するプラットフォーム(iOSやtvOSなど)、OSのバージョン、アーキテクチャ(例: 64-bit iOS 8.1)。
- クラッシュログやスタックトレース。上述のクラッシュレポートの項目も参考にしてください。
クラスにプロパティが1つも無い(No properties in class)という実行時例外が起こる
“No properties in class, has linker stripped it?”というメッセージのSystem.InvalidOperationException
が発生することがあります。
バージョン0.77.2以降では、もう少し詳しい情報が表示されます。
判明している原因は2つあります。
- Fodyに何か問題が発生していてWeavingされたRealmObjectsが1つも無い
- RealmObjectsのプロパティがStripされてしまっていてRealmが空になっている
最初のケースに関しては例外はRealmSchemaからスローされます。詳しくはWeavingに失敗したときのセクションをご覧ください。
2番目のケースに関しては例外はObjectSchemaからスローされます。 Linker Beahviour を Link All に設定していてクラス宣言に[Preserve(AllMembers = true)]
属性が無い場合に問題が起こることがあると報告されています。リンカーはコード内で明示的に参照されるメンバーのみを保持します。つまり、永続化されるがどこでも参照されないプロパティがある場合、そのスキーマが削除されてデータベースの不一致が発生する可能性があります。 デフォルトの挙動では、Realmはすべてのアセンブリ内のRealmObjectサブクラスの すべて に対するするスキーマを構築します。これは 遅延 されるので、最初にGetInstance()を呼び出すまでは起こりません。これは実行するごとに1回だけ実行され、結果をメモリにキャッシュします。
特定のモデルクラスだけをRealmに保存したい場合は、ObjectClassesでサブセットを指定する方法でGetInstance(myConf)を使ってRealmを作成します。このようにすることで、すべてのクラスに対してスキーマ構築はされなくなるので、例外を回避できます。
それ以外の場合は、未使用のクラスがある場合はPreserve
属性を追加してください。
Fody: キャッチされなかった例外が発生する
このビルどエラーは既存のプロジェクトに新しくRealmObjectのサブクラスを追加するだけで容易に発生します。
BuildまたはRunを選択するだけでは、Fody Weaverを使用してビルドされません。
Rebuildを選択すると、正しくビルドできます。
Weavingに失敗したとき
ビルドログにクラスがWeavingされなかったという旨の警告メッセージが表示されることがあります。これはWeavingを行うライブラリであるFodyが正しくインストールされていないことを示しています。
まず最初に、FodyWeavers.xml
ファイルにRealmWeaver
の記述があるかどうか確認してください。
Fodyのインストール時にはソリューションのディレクトリ(通常はプロジェクトディレクトリと同じ階層にあります)のTools
ディレクトリにRealmWeaver.Fody.dll
がコピーされます。このファイルはどのプロジェクトともリンクされてはいませんが、FodyがDLLをロードするためにはこの場所に存在する必要があります。
Visual Studio 2015とNuGetパッケージマネージャの3.2より古いバージョンの組み合わせで使っていると、Fodyのインストールが失敗することがあります。確認するためには、テキストエディタを用いて、.csproj
ファイルを開き、Fody.targets
をインポートしている行を見ます。その行は下記のようになっています。
<Import Project="..\packages\Fody.1.29.3\build\portable-net+sl+win+wpa+wp\Fody.targets"
Condition="Exists('..\packages\Fody.1.29.3\build\portable-net+sl+win+wpa+wp\Fody.targets')" />
最新のバージョンのNuGetパッケージマネージャにアップグレードすると、この問題は解決します。
これでうまくいかなければ、おそらくFodyとMicrosoft.Bcl.Build.targets
に問題があります。.csprojファイルから次の行を削除すると、直ることがあります。
<Import Project="..\..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets" Condition="Exists('..\..\packages\Microsoft.Bcl.Build.1.0.21\build\Microsoft.Bcl.Build.targets')" />
このことについてより詳しくはこちらのStackOverflowの回答をご覧ください。
非同期の書き込み(WriteAsync)に関するトラブルシューティング
WriteAsync
メソッドでは内部的にSynchronizationContext.CurrentがNullでないことをチェックしています。これはUIスレッドから呼ばれているかどうかを確かめるためです。 よく使われているワーカースレッドにCurrent
をセットするというデザインパターンでは、このチェックは意図した通りに動かないかもしれません。Postメソッドを用いて、UIスレッドにディスパッチできます。このパターンは2007年のMSDNブログの記事に載っています。
SynchronisationContext.Current
を設定しても問題にはなりませんが、その場合WriteAsync
はスレッドプールに再度ディスパッチされることになり、別のスレッドが作成されることがあります。Current
をスレッドで設定している場合は、WriteAsync
ではなく単にWrite
を呼ぶことも検討してください。
Realm Coreのダウンロードに失敗する場合
Realmがビルドされるときには、自動的にRealm Coreライブラリをスタティックライブラリとしてダウンロードし、Realm-Cocoaプロジェクトに組み込みます。
下記のようなメッセージが表示された場合は、ビルド時のCoreライブラリのダウンロードに失敗しています。
Downloading core failed. Please try again once you have an Internet connection.
このエラーが起こるのは、下記のいずれかの理由によります。
- 米国輸出規制法のリストに含まれる国のIPアドレスが割り当てられている。 米国の法律に従うために、Realmは上記のリストに含まれる国で使用することはできません。 さらに詳しいことはライセンス条項をお読みください。
- 中国国内、あるいは国レベルのファイアウォールによってCloudFlareまたはAmazon AWS S3へのアクセスを制限している国からアクセスしている。 さらに詳しいことはこちらのIssueをご覧ください。
- Amazon AWS S3が障害により一時的に利用できなくなっている。AWS Service Health Dashboardを見てサービスの稼働状況を確認し、 障害が解決された後で再度やり直してください。