최신 영문 문서를 보시려면 이 곳을 참고하세요.

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

// 스레드 안전한 트랜잭션에서 객체를 갱신하고 영속화하기
transaction
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에서 소스 코드를 볼 수 있습니다.

나이틀리 빌드도 MyGet에서 받을 수 있습니다. 자세한 것은 아래를 보세요.

요구사항

아래 환경을 지원합니다.

  • Visual Studio 2015 업데이트 2 이상, Xamarin Studio 버전 6.1.1 이상.
  • 네이티브 UI나 Xamarin Forms을 iOS 7 이상 버전에서 사용하기 위해서 Xamarin.iOS 버전 10.0.1.10.
  • 네이티브 UI나 Xamarin Forms을 API 레벨 10이상에서 사용하기 위해서 Xamarin.Android 버전 7.0.1.3.
  • Windows 데스크탑 (win32-x86와 win64-x64).
  • Xamarin.Mac과 UWP는 아직 지원하지 않지만 곧 지원할 예정입니다.

Xamarin 개발의 속도를 올리기 위해 Realm을 출시하는 시점의 스테이블 업데이트 채널의 지원 버전을 기초로 합니다. 만약에 몇단계 마이너 버전이 뒤쳐진 Xamarin 버전을 사용한다면 Realm이 Xamarin의 기능에 밀접하게 기반하지 않기 때문에 문제는 되지 않겠지만 베타나 알파 채널의 Xamarin을 쓰는 경우에는 문제가 될 수 있습니다.

PCL 사용자를 위한 중요한 안내 - NuGet 유인 상술을 쓸 수 있습니다. Realm에서 사용하는 플랫폼 특화 프로젝트의 모든 PCL마다 Realm Nuget 패키지를 설치해야합니다. 예: 여러분의 앱이 iOS와 안드로이드를 지원하고 공유된 프로젝트를 사용한다면 각 플랫폼 특화 프로젝트마다 NuGet을 설치해야 합니다.

안드로이드 ABI 지원

몇몇 인스트럭션 집합의 제한 때문에 armeabi ABI 설정을 지원하지 않습니다.

새 Xamarin 프로젝트를 만드는 기본 템플릿이 현재 디버그 빌드에서 모든 ABI 설정이 체크되어 있지만 릴리즈 빌드에서는 armeabi만 체크되어 있습니다. 반드시 여러분의 릴리즈 빌드를 하기 전에 설정을 수정하셔야 합니다.

만약 다른 ABI 체크가 없다면 다른 장비에서 수행할 때 System.TypeInitializationException가 발생할 수 있습니다. 예를 들어 갤럭시 탭 S2와 같은 64비트 장비에서 armeabiarmeabi-v7a가 체크되어 있고 arm64-v8a체크되지 않다면 에러를 발생시킨다.

다른 ABI들의 링크를 막을 좋은 이유가 없다면 armeabi을 제외한 모든 ABI들을 체크하는 것이 최선일 겁니다. 그래서 이렇게 설정할 수 있습니다.

  • armeabi-v7a
  • arm64-v8a
  • x86
  • x86_64

Xamarin 스튜디오에서 이 설정들은 마우스 오른쪽 클릭으로 Options - Build - Android Build - Advanced Tab입니다.

비주얼 스튜디오에서 이 설정들은 마우스 오른쪽 클륵으로 Properties - Android Options - Advanced Tab입니다.

설치

나이틀리 피드를 사용하려면 NuGet 소스를 대신 https://www.myget.org/F/realm-nightly/로 설정합니다. VS2015혹은 Xamarin 스튜디오 6.1 이상에서 NuGet v3를 쓰려면 https://www.myget.org/F/realm-nightly/api/v3/index.json를 사용합니다.

  1. Solution 페인의 여러분의 프로젝트에 “Packeses” 노드에서 기어 버튼을 클릭하고 “Add Packages…“를 클릭합니다.
  2. 검색 창에 “Realm”을 입력합니다.
  3. Realm을 선택하여 추가합니다.
  4. 의존성에서 Fody가 추가된 것을 볼 수 있습니다.

Realm 패키지는 Realm을 사용하기 위해 필요한 모든 것을 가지고 있습니다. 이는 Fody 위버에 의존합니다. 이는 여러분의 RealmObject 서브클래스를 영속적인 객체로 만드는 책임을 집니다.

이 시점에서 패키지를 설치해야 합니다. 프로젝트가 이미 Fody를 사용하고 있다면 기존의 FodyWeavers.xml가 갱신된 것을 볼 수 있습니다. 중요한 점은 FodyWeavers.xml 파일이 RealmWeaver를 포함한 당신이 필요한 모든 위버를 가지고 있다는 것입니다.

이미 추가한 다른 위버가 없었고 Realm을 추가했다면 FodyWeavers.xml 파일의 모양은 다음 예제와 같습니다.

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
    <RealmWeaver />
</Weavers>
  1. 프로젝트에서 Tools - NuGet Package Manager - Manage Packages for Soultion을 선택합니다.
  2. 가능한 패키지에서 Realm 패키지를 선택합니다.
  3. 오른쪽에 여러분의 프로젝트가 선택되고 Install 버튼이 활성화된 것을 확인합시다.
  4. Install을 누릅니다.
  5. Realm과 Fody가 설치되었다는 다이얼로그가 뜨면 OK를 누릅니다.

Realm 패키지는 Realm을 사용하기 위해 필요한 모든 것을 가지고 있습니다. 이는 Fody 위버에 의존합니다. 이는 여러분의 RealmObject 서브클래스를 영속적인 객체로 만드는 책임을 집니다.

이 시점에서 패키지를 설치해야 합니다. 프로젝트가 이미 Fody를 사용하고 있다면 기존의 FodyWeavers.xml가 갱신된 것을 볼 수 있습니다. 중요한 점은 FodyWeavers.xml 파일이 RealmWeaver를 포함한 당신이 필요한 모든 위버를 가지고 있다는 것입니다.

이미 추가한 다른 위버가 없었고 Realm을 추가했다면 FodyWeavers.xml 파일의 모양은 다음 예제와 같습니다.

<?xml version="1.0" encoding="utf-8" ?>
<Weavers>
    <RealmWeaver />
</Weavers>

Realm 브라우저

우리는 .realm 파일을 읽고 편집하기 위해 독립적인 맥 앱 Realm 브라우저를 제공합니다.

Realm Xamarin이 지원하는 파일 포맷은 현재 브라우저보다 낮습니다. 이런 이유로 새 버전의 브라우저로 realm 파일을 열게 되면 Realm Xamarin에서 동작하지 않게 됩니다. 위의 링크는 호환성을 위한 버전입니다.

Realm Browser

메뉴 항목 Tools > Generate demo database.을 이용하면 샘플 데이터로 테스트 데이터베이스를 생성할 수 있습니다.

앱의 Realm 파일에 대한 도움을 찾기를 원한다면 StackOverflow 답변에서 자세한 설명을 확인하세요.

Realm 브라우저는 맥 앱 스토어에서 사용할 수 있습니다.

API 문서

전체 클래스와 메서드는 전체 API 문서에서 살펴보세요.

예제

저장소의 예제 폴더에서 여러 예제를 찾을 수 있습니다.

도움을 구하려면

모델

Realm 데이터 모델은 프로퍼티를 포함한 전통적인 C# 클래스를 통해 정의합니다. 단순히 RealmObject를 상속받아 Realm 데이터 모델 객체를 생성합니다.

Realm 모델 객체는 대부분의 경우 다른 C# 객체들처럼 동작합니다. 자신만의 메서드 이벤트를 추가할 수 있고 다른 객체들처럼 사용할 수 있습니다. 주요 제약은 생성된 스레드에서만 객체를 사용해야 하고 영속적인 속성은 게터와 세터를 생성해야 한다는 점입니다.

또한 클래스가 퍼블릭이며 파라미터가 없는 생성자를 가져야한다는 점을 주의하세요. 만약 어떤 생성자도 추가하지 않았다면 컴파일러는 생성자를 추가합니다. 만약 최소 하나의 생성자를 추가했다면 공개된 파라미터 없는 생성자가 있는지 확인하세요.

관계와 내부 데이터 구조는 단순히 대상 타입이나 객체들의 리스트를 위한 타입 IList을 포함합니다.

// 개 모델
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은 unsigned를 제외한 타입(bool, char, byte, short, int, long, float, double)과 string, DateTimeOffset을 지원합니다. 널도 마찬가지로 지원합니다. 더 이상의 내용은 선택적 속성을 참고하세요.

날짜 타입

우리는 DateTime 대신 표준적인 DateTimeOffset형을 날짜를 표현하는 주 자료형으로 사용하고 있습니다.

이는 DateTime의 모호함 떄문에 마이크로소프트가 한 권고를 따른 것입니다.

이는 100 나노세컨드 단위의 틱의 정밀도로 저장됩니다.

타임존을 붙여 DateTimeOffset을 지정할 수 있지만 Realm은 이를 UTC 값으로 저장합니다. 이는 다른 언어에서 읽을 때 같은 의미를 가지는 인스턴트를 가질 수 있는 모호하지 않은 표현입니다. UtcTicks 대신에 Ticks을 사용하여 값을 비교할 때 혼란의 원인이 됩니다.

관계

RealmObjectRealmObject와 IList 속성을 이용해서 서로 연결할 수 있습니다.

List는 .NET의 표준 IList 제너릭 인터페이스를 구현합니다. 클래스에 IList 프로퍼티를 정의하면 get이 사용될 때 RealmList가 생성됩니다. get; 자동 메서드만 지정할 수 있고 이런 리스트에 대해 set할수는 없습니다.

대상이 하나인 관계

N대 1이나 1대 1 관계를 위해 단순히 속성을 RealmObject의 서브클래스에 선언하세요.

public class Dog : RealmObject
{
    // ... 다른 속성 선언
    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으로 부터 각 객체를 자동으로 가져오게 됩니다.

대상이 여럿인 관계

대상이 여럿인 관계는 IList 속성을 통해 만들 수 있습니다. 이런 속성을 이용할 때 컬렉션은 비어있을 수도 있고 관계된 RealmObject의 타입을 받을 수도 있습니다.

간단히 IList<Dog> 속성을 선언하는 것으로 “Dogs” 속성들을 여러 개와 연결될 수 있는 Person 모델에 추가할 수 있습니다. IList를 초기화할 필요는 없으며, Realm.CreateObject가 이런 작업을 대신 처리합니다. 주어진 PersonDog 사이의 관계 정립을 위해 목록으로부터 Dog 객체들을 추가하거나 삭제하면 됩니다.

public class Dog : RealmObject
{
    public string Name { get; set; }
}

public class Person : RealmObject
{
    // ... 다른 속성 선언
    public IList<Dog> Dogs { get; }
}
jim.Dogs.Add(rex);
jim.Dogs.Count();  // => 1 -- rex외에 없음

역관계

관계 연결은 한 방향이므로 대상이 여럿인 속성, Person.DogsDog 인스턴스에 연결되고, 대상이 하나인 속성, Dog.OwnerPerson에 연결됩니다. 이 연결들은 서로 독립적입니다. Dog 속성을 Person 인스턴스에 추가해도 개의 Owner 속성에 자동으로 해당 Person`이 연결되지 않습니다. 수동으로 관계 쌍을 동기화하는 것은 오류가 발생하기 쉽고 정보가 중복될 수 있으므로, Realm은 이런 역관계를 나타내는 백링크 속성을 제공합니다.

백링크 속성을 사용하면 특정 속성에서 특정 객체에 연결된 모든 객체를 얻을 수 있습니다. 예를 들어 Dog 객체는 Owners라는 속성을 가질 수 있으며, 이 속성은 모든 자신의 Dog 속성에 해당 Dog 객체를 가진 Person 객체를 모두 포함합니다. 이는 Owners 속성을 IQueryable<Person> 타입으로 만들고, Person 모델 객체에 Owners의 관계를 나타내기 위한 [Backlink]를 적용해서 만들 수 있습니다.

public class Dog : RealmObject
{
    [Backlink(nameof(Person.Dogs))]
    public IQueryable<Person> Owners { get; }
}

백링크 속성은 대상이 여럿인 관계인 IList<RealmObject> 속성이나 대상이 하나인 관계인 RealmObject을 가리킬 수 있습니다.

public class Ship : RealmObject
{
    public Captain Captain { get; set; }
}

public class Captain : RealmObject
{
    [Backlink(nameof(Ship.Captain))]
    public IQueryable<Ship> Ships { get; }
}

선택적 속성

string, byte[]와 같은 참조 타입과 RealmObject의 값은 null이 될 수 있습니다. 각 속성 형의 선택적인 범위 전체를 가지기 위해 선택적인 DateTimeOffset? 형도 지원합니다.

int?와 같이 널 값이 허용되는 타입도 전부 지원합니다.

속성 영속성 제어하기

RealmObject을 상속받은 클래스들은 Fody weaver에 의해 컴파일 타임에 처리됩니다. 자동 세터와 게터가 추정될 수 있는 모든 속성은 영구적으로 저장되며 내부의 Realm 저장소와 연결되는 생성된 세터와 게터를 가지게 됩니다.

몇몇 C# 특성을 영속 제어를 위한 메타데이터로 제공합니다.

속성의 영속화를 막으려면 단순히 [Ignored] 특성을 추가하면 됩니다. 영속화를 막아야 하는 일반적인 예로는 외부 미디어를 사용하기 때문에 바이너리 이미지 대신에 파일의 경로를 저장해야하는 상황등을 영속화를 막아야 하는 상황등이 있습니다.

public string HeadshotPath { get; set; }

// 실행 중 메모리 이미지
[Ignored]
public Image Headshot { get; set; }

커스텀 세터

그들 자체의 세터와 게터 구현을 가진 속성들은 자동으로 무시됩니다. 검증을 위해서는 아래를 사용할 수 있습니다.

private string Email_ { get; set; }

// 영속 Email_ 속성의 검증
public string Email
{
    get { return Email_; }
    set
    {
        if (!value.Contains("@")) throw new Exception("Invalid email address");
        Email_ = value;
    }
}

인덱스가 추가된 속성

현재 스트링, 정수, 불린, DateTimeOffset은 인덱스를 추가할 수 있습니다.

속성을 인덱스하는 것은 ==Contains 연산을 사용하는 것과 같은 속성 동등 비교에 비해 많은 성능을 향상시킵니다. 다만 삽입 성능은 조금 느려집니다.

속성을 인덱스화하려면 [Indexed] 특성을 속성 선언에 추가합니다.

public class Person : RealmObject
{
    [Indexed]
    public string Name { get; set; }
    public IList<Dog> Dogs { get; }
}

자동 갱신되는 객체

RealmObject 인스턴스가 관리되는 상태로 바뀌자마자 이는 라이브가 되고 내부의 데이터로 자동 갱신됩니다. 이 말은 객체를 리프레쉬할 필요가 없다는 뜻입니다. 객체의 속성을 수정하면 즉각 다른 인스턴스가 참조하는 같은 객체에 반영됩니다.

public class Dog : RealmObject
{
    public string Name { get; set; }
    public int Age { get; set; }
}

var realm = Realm.GetInstance();
// 설정 끝

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을 빠르고 효과적으로 할 뿐 아니라 여러분의 코드를 단순하고 더 반응적이게 합니다. 예를 들어 UI 코드가 특정 Realm 객체에 기반한다면 UI를 다시 그리기 전에 객체를 리프레쉬하거나 다시 가져올 필요가 없습니다.

Realm 알림을 구독하여 객체의 Realm 데이터가 갱신되는 것을 알 수 있고 이를 이용하여 앱의 UI를 갱신할 수 있습니다.

기본 키 특성

[PrimaryKey] 특성은 RealmObject 클래스의 객체 id를 설정하는 하나의 속성에 정의할 수 있습니다. PrimaryKey를 정의하면 객체들의 참조하고 갱신하는 것이 효과적으로 되고 각 값이 유일한 값을 가져야 합니다.

오직 char, 정수형, 문자열만이 기본 키가 될 수 있습니다. char나 작은 정수형 형을 사용할 때 사용되는 특별한 저장소는 없고 성능 상의 이점도 없으며, 단지 그 형으로 된 속성을 이미 가지고 있을 때만 이점이 있습니다.

여러 속성에 [PrimaryKey]를 붙이면 컴파일은 되지만 실행 시에 검증되며 Realm을 열자마자 _Schema validation failed_를 보고하는 예외를 던집니다.

한번 PrimaryKey의 객체가 Realm에 추가되면 PrimaryKey는 변경할 수 없습니다.

같은 키를 가진 다른 객체를 만들면 RealmDuplicatePrimaryKeyValueException을 발생시킵니다.

Realm.ObjectForPrimaryKey을 사용하면 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");

무시된 속성

RealmObject 클래스의 속성에 자동 생성되지 않은 게터와 세터를 정의하면 이 속성은 저장되지 않습니다. 명시적으로 속성을 무시하고 싶으면 속성이 무시될 수 있도록 [Ignored] 특성을 적용합니다.

모델 상속

Realm Xamarin은 어떤 식으로든 추가로 상속되는 것을 허용하지 않습니다. RealmObject 클래스로부터 간접적으로 상속받은 객체가 발견되면 위버가 실패합니다.

컬렉션

Realm은 객체의 그룹을 표현하기 위해 표준 타입을 사용합니다.

  1. IQueryable<T>쿼리로부터 가져온 객체들을 표현하는 클래스입니다.
  2. IList<T>, 모델에서 대상이 여럿인 관계를 표현하는 클래스입니다.

런타임시 둘 모두 IReadOnlyList<T>INotifyCollectionChanged를 따르는 IRealmCollection<T> 인터페이스를 따르며 지연 열거를 지원합니다. 컬렉션의 크기만 알려지고 객체는 컬렉션을 순회할 때만 메모리에 적재됩니다.

쓰기

객체에 대한 모든 변경(추가, 수정, 삭제)은 쓰기 트랜잭션 내에서 이루어져야 합니다.

스레드 간에 객체를 공유하거나 앱이 실행된 후 재사용하기 위해 그들을 쓰기 트랜잭션내에 연산을 처리하여 Realm으로 영속화를 해야합니다.

쓰기 트랜잭션은 무시못할 오버헤드를 초래하기 때문에 코드에서 쓰기 트랜잭션의 수를 줄이도록 설계해야 합니다. 예를 들어 루프에서 여러 개의 아이템을 삽입하는 경우 매번 트랜잭션을 만드는 것보다 아래처럼 하나의 트랜잭션에서 하는 것을 권장합니다.

realm.Write(() =>
{
    var people = Realm.All<Person>();
    foreach (var person in people)
    {
        person.Age += 1;
    }
});

쓰기 트랜잭션은 잠재적으로 다른 디스크 IO 연산처럼 디스크 용량 부족등의 잠재적인 실패 가능성이 있기 때문에 쓰기로 부터 예외를 대비하고 실패를 다루거나 복원하여야 합니다. 다른 복구가능한 에러는 없습니다. 간결함을 위해 우리의 코드 샘플은 이런 에러들을 다루지 않습니다만 제품 어플리케이션에는 포함되어야 합니다.

쓰기 트랜잭션은 Realm.BeginWrite()Realm.Write(), 두 가지 방법으로 쉽게 작성할 수 있습니다. 첫번째 Realm.BeginWrite()Dispose 패턴을 구현한 트랜잭션을 반환합니다. using과 함께 사용할 수 있습니다.

using(var transaction = realm.BeginWrite())
{
    person.FirstName = "John";
    person.Age = 56;
    transaction.Commit();
}

트랜잭션을 명시적으로 Commit해야 함을 유의하세요. 그렇지 않으면 자동으로 복구됩니다. 다른 방법은 변경 코드를 Realm.Write()로 감싸는 것입니다.

realm.Write(() =>
{
    person.FirstName = "John";
    person.Age = 56;
});

예외가 발생되지 않으면 기본으로 암시적인 트랜잭션이 커밋됩니다.

객체 만들기

모델을 정의할 때 RealmObject의 서브클래스를 실체화하고 새로운 인스턴스를 Realm에 추가할 수 있습니다. 간단한 모델을 예로 들겠습니다.

public class Dog : RealmObject
{
    public string Name { get; set; }
    public int Age { get; set; }
}

이 클래스의 인스턴스를 Realm.Add() 메서드를 이용하여 영속적인 라이브 객체로 변환할 수 있습니다.

realm.Write(() =>
{
    var myDog = new Dog();
    myDog.Name = "Rex";
    myDog.Age = 10;
    realm.Add(myDog);
});

객체를 만든 이후 실시한 모든 변경은 영속적이게 됩니다. (이 변경들은 쓰기 트랜잭션에서 이루어져야 합니다.) 쓰기 트랜잭션이 완료되면 같은 Realm을 사용해서 다른 스레드에서 변경을 하는 것이 가능합니다.

쓰기가 다른 쓰기를 블록하고 만약 진행중인 여러 쓰기가 있다면 다른 스레드를 블록할 것이라는 점을 유의하세요. 이것은 다른 영속적인 도구에서도 비슷합니다. 우리는 이런 상황에서 일반으로 통용되는 최선의 방법인 별도의 스레드에서 쓰기를 해 부담을 감하는 것을 추천합니다.

Realm이 MVCC 아키텍쳐를 사용하기 때문에 읽기는 트랜잭션이 열려 있어도 방해받지 않습니다. 한번에 여러 스레드에서 쓰기를 같이 하지 않는 한 여러분은 잘게 나눈 많은 쓰기 트랜잭션보다 큰 쓰기 트랜잭션을 선호할 것입니다.

객체 갱신하기

쓰기 트랜잭션에서 속성들을 설정하는 방법으로 객체를 갱신할 수 있습니다.

// 트랜잭션에서 객체를 갱신하기
using (var trans = realm.BeginWrite())
{
    author.Name = "Thomas Pynchon";
    trans.Commit();
}

[기본 키]를 지정한 객체의 경우 realm.Addupdate: 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를 지정하고 기본 키가 없는 클래스를 넘기면, 메서드는 업데이트할 객체를 찾을 수 없으므로 update: false를 받은 것처럼 동작합니다. 객체가 다른 RealmObject들과 관계를 맺고 있다면 다른 객체들에 기본 키가 지정된 경우 추가 혹은 업데이트되고, 다른 객체들에 기본 키가 없는 경우에는 단순히 추가됩니다.

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](https://realm.io/docs/xamarin/latest/api/reference/Realms.Realm.html#Realms_Realm_Remove_Realms_RealmObject_) 메서드에 삭제할 객체를 전달합니다.

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 연산을 컬렉션 필터링을 위해 사용할 수 있습니다.

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;

어떤 문구를 선택하든 반환된 RealmResults 컬렉션은 IQueryable 인터페이스입니다. 그래서 이것으로 반복하거나 다른 처리를 할 수 있습니다.

foreach (var d in oldDogs)
{
    Debug.WriteLine(d.Name);
}

더 많은 쿼리 예제

LINQ 쿼리에 익숙하지 않은 경우를 위해 기본적인 쿼리 문구 예제를 알려드립니다. 확장 문구 와 함께 기본적인 쿼리를 보겠습니다.

John 혹은 Peter란 이름을 가진 모든 사용자의 리스트를 출력하려면 다음과 같이 합니다.

var johnsAndPeters = realm.All<Person>().Where(p =>
    p.FirstName == "John" ||
    p.FirstName == "Peter");
var peopleList = johnsAndPeters.ToList();

첫번째 문장은 “John”과 “Peter”가 이름인 사용자를 찾는 IQueryable를 구현한 클래스의 johnsAndPeters 인스턴스를 돌려줍니다. 이는 표준적인 LINQ 구현 방법으로, 쿼리를 표현하는 객체를 얻을 수 있습니다. 쿼리는 반복이나 결과의 수를 세는 추가적인 호출이 있을 때까지 어떤 일도 하지 않습니다.

이 예제에서 ToList 호출은 Realm 코어에 직접 연결된 쿼리를 수행시킵니다

객체는 복사되지 않습니다. 조건에 맞는 객체들의 레퍼런스 리스트를 받아 쿼리에 맞는 원본 객체를 직접 다룹니다.

결과를 모두 리스트로 받아오는 대신 쿼리 결과를 표준 C#의 foreach 문으로 타고 갈 수 있습니다.

foreach (var person in johnsAndPeters) // 쿼리 순회
{
    Debug.WriteLine(person.Name);
}

논리 연산자

LINQ 표현의 표준 C# 논리 연산자로 쿼리를 작성할 수 있습니다.

if 문에 기대하는 전제 조건들을 괄호에 묶인 중첩된 표현식으로도 사용할 수 있습니다.

var complexPeople = realm.All<Person>().Where(p =>
    p.LastName == "Doe" &&
    (p.FirstName == "Fred" || p.Score > 35));

타입으로 객체를 가져오기

주어진 타입에 대해 모든 객체를 Realm에서 가져오려면 realm.All<T>()를 사용합니다. 이는 쿼리된 모델 클래스의 전체 인스턴스를 가지는 IQueryable 컬렉션을 반환하며, 위의 예제에서는 Person입니다. 집합에 대해 추가적인 제약을 적용하기 위해 LINQ 쿼리를 사용할 수 있습니다. 컬렉션에 대해 순환을 하기 전까지는 어떠한 오버헤드도 없습니다.

var ap = realm.All<Person>();  // 타입에 관한 전체 아이템
var scorers = ap.Where(p => p.Score > 35);  // 첫번째 검색 절로 제한 두기
var scorerDoe = scorers.Where(p => p.LastName == "Doe");  // 효과적인 AND 

정렬

표준 LINQ 절 OrderBy, OrderByDescending, ThenBy, ThenByDescending은 여러 단계의 정렬에 사용될 수 있습니다. 이 정렬은 Realm.All에서 반환되거나 후속 LINQ절에서 제공되는 Queryable의 객체 순서에 영향을 줍니다.

결과에 대한 전체 객체를 읽지는 않으며 효과적인 검색 정렬을 지원하는 내부의 쿼리 엔진을 통해 정렬이 이루어집니다.

주의: 만약 ToList 절을 객체 리스트를 추출하기 위해 사용하고 객체를 위한 LINQ를 이용해서 메모리 상에서 정렬하기 위해 OrderBy를 사용할 수 있습니다. 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();

결과 제한

대부분의 다른 데이타베이스 기술들은 쿼리 결과들을 (SQLite의 LIMIT 키워드와 같이) 페이지를 바꾸는 기능을 제공합니다. 이는 종종 디스크로 부터 너무 많이 읽는 것을 막거나 한번에 메모리로 많은 결과를 가져오는 것을 막기 위해 필요합니다.

Realm의 쿼리는 지연되고 쿼리의 결과로 부터 객체들을 명시적으로 접근될 때만 가져오기 때문에 페이지를 바꾸는 행위같은 것은 전적으로 불필요합니다.

만약 UI 관련이나 다른 구현 이유로 쿼리로 부터 특정한 객체의 부분집합을 원한다면 IQueryable 객체를 가져오고 필요한 객체만 읽으면 됩니다.

만약 LINQ Take를 사용하길 원하더라도 아직 쓸 수 없습니다. 이것이 추가되면 한 명령에 요청한 객체 전부를 열거하고 인스턴스화할 수 있을 것입니다.

Realms

Realm은 실제 데이터베이스를 표현합니다. 여러 종류의 객체를 가지고 있고 디스크 상 하나의 파일에 대응합니다.

기본 Realm

realm 변수를 항상 optionalPath를 지정하지 않고 Realm.GetInstance(string optionalPath)로 초기화한 것을 보았을 것입니다. 정적 메서드는 스레드에 맞는 Realm 인스턴스를 반환합니다. 이 인스턴스는 Environment.SpecialFolder.Personal에 저장되는 default.realm 파일에 연결됩니다.

Realm 설정하기

Realm.getInstance()을 호출하면 Realm을 손쉽게 시작할 수 있습니다. Realm이 생성되는 여러 측면을 제어할 수 있는 RealmConfiguration 객체를 생성해 보다 세밀한 제어를 할 수 있습니다.

RealmConfiguration은 다양한 데이터 베이스 경로를 허용합니다. (한번 설정이 생성되면 경로를 바꿀 수 없는 점을 유의하세요.)

아래와 같이 할 수 있습니다.

  1. 새로운 절대 경로를 전달하여 전체 경로를 변경하기.
  2. 표준 경로의 서브 디렉토리에 Realm 파일들을 두고 상대 경로를 전달하기.
  3. 새로운 파일 이름을 전달하여 Realm 파일 이름을 변경하기.

설정에서 스키마 버전을 지정할 수 있습니다. 여기에 대한 자세한 것은 마이그레이션 섹션을 참조하세요. 개발 중에 ShouldDeleteIfMigrationNeeded 속성을 true로 설정할 수 있습니다. 이 설정은 열려있는 파일이 여러분의 스키마와 맞지 않다면 Realm.GetInstance()가 기존의 데이터베이스를 삭제하게 됩니다. 이렇게 설정하면 릴리즈 전에 모델을 다루는게 편해집니다. 하지만 이 설정으로 릴리즈하지 마세요. 사고를 막기 위해서는 #if DEBUG 섹션안에 설정할 수 있습니다.

같은 구성의 Realm을 열기 위해 어떤 설정의 인스턴스를 여기 저기에 쓸 수 있습니다. 다른 스레드에서 같은 Realm을 열기 위해 사용하는 일반적인 사용례입니다.

물론 어떤 객체를 전달하지 않고 기본 값을 바꾸기 위해 기본 설정을 재정의할 수 있습니다.

설정 마다 스레드마다 싱글턴으로 Realm 인스턴스가 유지된다는 것이 중요합니다. 같은 스레드에서 같은 설정이라면 Realm.GetInstance()가 매번 같은 인스턴스를 반환합니다.

Realm 인스턴스 닫기

Realm은 네이티브 메모리 해제와 파일 설명자(descriptors)를 다루기 위해 IDisposable을 구현합니다. 이렇게 하면 인스턴스는 변수가 스코프에서 사라질 때 자동으로 닫힐 수 있습니다.

Realm 파일 찾기

앱의 Realm 파일을 찾는 것의 도움이 필요하면 자세한 설명을 StackOverflow의 답변에서 확인하세요.

외부 Realm 파일

표준 .realm 파일을 따라 Realm은 내부적인 자체 연산을 위해 추가적인 파일을 생성하고 관리합니다.

  • .realm.lock - 리소스 락을 의한 락 파일.
  • .realm.note - 알림을 위한 명명된 파이프(named pipe).

이 파일들은 .realm 데이터베이스 파일에 어떤 영향도 미치지 않습니다. 그들의 부모 데이터베이스 파일이 삭제되거나 대체되어도 어떤 에러를 유발하지 않습니다.

Realm 이슈를 보고할 때는 디버깅을 위해 유용한 정보를 포함한 외부 파일들을 .realm 파일과 함께 포함해주세요.

Realm 파일 포함하기

앱에 Realm 파일을 포함하길 원한다면 스택오버플로우의 대단한 답변을 보세요. 어떻게 Xamarin 프로젝트에 리소스로 포함하고 그것을 사용하기 위해 복사하는지 보여줍니다.

클래스 부분집합

특정 Realm에 저장될 클래스를 한정짓고 싶은 경우, RealmConfigurationObjectClasses 속성을 설정해서 할 수 있습니다.

class LoneClass : RealmObject
{
    public string Name { get; set;}
}

var config = new RealmConfiguration("RealmWithOneClass.realm");
config.ObjectClasses = new[] { typeof(LoneClass) };

// 또는 Realm에 두개의 클래스를 지정하기
config.ObjectClasses = new[] { typeof(Dog), typeof(Cat) };

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.Compact(config)를 호출할 수 있습니다.

var config = new RealmConfiguration();
var initialSize = new FileInfo(config).Length;
if (initialSize > 100 * 1024 * 1024) // 100 MB
{
    Realm.Compact(config);
    var newSize = new FileInfo(config).Length; // Should be less than initialSize
}

압축 시 주의사항

  • 파일을 사용하고 있는 열린 상태의 realm 인스턴스가 없어야 합니다.
  • 파일 시스템에 Realm 파일 복사를 위해 해당 크기 이상의 최소한의 공간이 있어야 합니다.
  • 작업이 실패하면 Realm 파일은 변경되지 않습니다.
  • 메서드는 작업이 성공하는 경우 true를, 동일 파일에 접근하는 Realm 인스턴스가 있는 경우 false를 반환합니다.
  • 현재로서는 Realm을 압축해서 얻을 수 있는 크기에 대한 예측을 제공하지 않습니다. 애플리케이션 시작 사이에 데이터베이스 크기를 모니터링하고 입계값을 초과하는 경우 압축하는 접근 방법을 권장합니다.

스레드

개별적인 스레드에서 모든 것을 동시성과 멀티스레드에 대한 걱정없이 일반 객체처럼 다룰 수 있습니다. (다른 스레드에서 동시에 수정되더라도) 데이터에 접근하기 위해 락을 쓰거나 리소스를 조율할 필요가 없습니다. 단 수정을 위한 연산만 트랜잭션으로 감싸야 합니다.

Realm은 개별 스레드가 항상 Realm의 일관된 뷰를 보는 걸 보장함으로서 동시적인 사용이 쉽도록 합니다. 개별 스레드가 자신만의 스냅샷을 가지기 때문에 같은 Realm을 병렬적으로 여러 스레드에서 수행할 수 있습니다. 그들은 서로에게 일관성이 없는 상태를 보게끔 영향을 주지 않습니다.

알아야 할 유일한 부분은 Realm 객체의 같은 인스턴스들은 여러 스레드에서 공유될 수 없다는 점입니다. 만약 여러 스레드에서 같은 객체에 대한 접근이 필요하다면 그들은 각자의 인스턴스들이 필요합니다. (그렇지 못하면 하나의 스레드에서 변경점은 다른 스레드에 불완전하게 보게되거나 불일관된 데이터를 보게 됩니다.)

다른 스레드에서 변경 보기

메인 UI 스레드(혹은 runloop이나 looper의 어떤 스레드)에서 객체는 매 runloop의 단계마다 다른 스레드의 변경은 자동으로 객체에 갱신됩니다. 어떤 시점이든 스냅샷을 다루게 되고 개별 메서드는 언제나 일관성있는 뷰를 보게 되고 다른 스레드에서 무엇이 일어나고 있는지 염려할 필요가 없습니다.

스레드에서 Realm을 초기화했다면 이것의 상태는 최근에 성공한 쓰기 커밋에 기반을 하게 되고 갱신되기 전까지는 그 버전을 유지하게 됩니다. Realm은 매 runloop의 매 단계의 시작마다 자동으로 갱신됩니다. (일반적으로 백그라운드 스레드인 경우에) 만약 스레드에 runloop이 없다면 가장 최근 상태의 트랜잭션으로 전진하기 위해 Realm.Refresh()를 수동으로 호출 해야합니다.

Realm은 쓰기 트랜잭션이 Transaction.Commit()으로 커밋되었을 때도 갱신합니다.

일반적인 이유로 Realm 갱신이 실패하면 그 버전에 사용되었던 디스크 공간이 재사용되는 것을 막기 위해서 어떤 트랜잭션 버전이 “고정”될 수 있고 파일 사이즈가 커질 수 있습니다. 자세한 내용은 우리의 현재 제한사항을 참고하세요.

스레드간 인스턴스 전달하기

Realm, RealmObject의 영속적인 인스턴스와, Realm.All에서 반환된 IQueryable, RealmObjectIList 속성은 생성된 스레드에서만 사용할 수 있고 그렇지 않으면 예외를 발생합니다. 이는 Realm이 트랜잭션 버전을 고립시키는 한 가지 방식입니다. 그렇지 않으면 잠재적으로 확장된 관계 그래프를 가진 다른 트랜잭션 버전의 스레드들 사이에 객체를 전달할 때 무엇을 해야할지 결정할 수 없습니다.

대신에 스레드 사이에 데이터를 전달할 안전한 방법들이 있습니다. 예를 들어 기본 키를 가진 객체는 그 객체의 PrimaryKey 값으로 표현될 수 있습니다. Realm은 자신의 RealmConfiguration으로 표현할 수 있습니다. 대상 스레드는 Realm이나 RealmObject를 그들의 스레드 안전 표현을 이용하여 다시 가져올 수 있습니다. 다시 가져오기는 대상 스레드에 그 버전의 인스턴스를 가져오게 됩니다. 이때 대상 스레드는 원 스레드와 다를 수 있습니다.

스레드 사이에 Realm을 사용하기

같은 Realm 파일을 다른 스레드에서 접근하려면 앱의 다른 스래드마다 다른 인스턴스를 얻기 위해 새로운 Realm을 초기화해야합니다. 같은 설정을 지정하는 한 모든 Realm 인스턴스는 디스크의 같은 파일에 연결됩니다.

여러 스레드에서 Realm 인스턴스를 공유하는 것은 지원되지 않습니다. 같은 realm 파일을 접근하는 Realm 인스턴스는 같은 RealmConfiguration을 사용해야 합니다.

Realm은 많은 양의 데이터를 하나의 트랜잭션에 여러 쓰기를 같이 일괄적으로 기록하는 것이 효과적입니다. Realm 객체는 스레드 안전하지 않고 스레드 사이에 공유될 수 없습니다. 읽기나 쓰기가 필요할 때 각 스레드나 dispatch_queue 마다 Realm 인스턴스를 얻으세요.

알림

안드로이드에서 변경 리스너는 오로지 Looper 스레드에서 작동합니다. 비 Lopper 스레드에서는 대신에 Realm.Refresh()를 수동으로 호출해야 합니다.

Realm 알림

Realm에 데이터를 추가하는 백그라운드 스레드가 있고 UI나 다른 스레드에서 리스너를 추가하여 Realm의 변경을 통보받을 수 있습니다. 변경 리스너는 (다른 스레드나 프로세스에서도 물론) Realm이 변경될 때마다 수행됩니다.

realm.RealmChanged += (s, e) =>
{
    // UI를 갱신한다
}

현재 RealmChanged의 구현은 변경 사항에 대한 자세한 정보를 제공하지 않으므로, 이런 기능이 필요한 경우 객체 알림이나 컬렉션 알림을 참고하세요.

Object 알림

RealmObject는 [INotifyPropertyChanged] (https://developer.xamarin.com/api/type/System.ComponentModel.INotifyPropertyChanged/)를 구현하므로, [PropertyChangedEvent] (https://developer.xamarin.com/api/event/System.ComponentModel.INotifyPropertyChanged.PropertyChanged/) 이벤트를 구독해서 변경 사항을 수신할 수 있습니다.

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); // => "Balance에 새 값을 설정합니다"

Realm에 객체를 추가하기 전에 구독했는지 후에 구독했는지는 중요하지 않습니다. 이벤트는 어느 경우라도 잘 발생합니다.

이렇게 하면 알림에도 적합하며 Xamarin.Forms에도 데이터바인드를 허용할 수 있습니다. 더 많은 정보는 Xamarin 문서의 데이터 바인딩에서 MVVM까지를 참고하세요.

컬렉션 알림

Realm.All<T>()에서 반환되거나 후속 LINQ 절에서 반환된 IQueryable 객체는 라이브 쿼리이므로 항상 최신 상태로 유지됩니다. 컬렉션을 순회할 때마다 최신 변경 사항이 반영된 결과를 받을 수 있습니다. 유사한 방식으로 RealmObjectIList 속성 역시 라이브로 대상이 여러 개인 관계를 나타냅니다. 즉, 매번 이를 순회할 때마다 가장 최신 컬렉션이나 관련 객체를 받을 수 있습니다.

이런 컬렉션을 모두 지원하는 런타임 객체는 IRealmCollection<T>도 구현하므로, 변경 알림 구독을 위한 두 가지 방법을 제공합니다. 간편한 확장 메서드인 AsRealmCollection을 사용해서 IList<T>IQueryable<T>IRealmCollection<T>로 캐스팅할 수 있습니다.

만약 MVVM과 데이터 바인딩에 적합한 표준 .NET INotifyCollectionChanged 인터페이스를 사용하려면 뷰에 직접 IRealmCollection<T>을 넘기세요.

혹은 더 많은 변경 정보를 사용하려면, (예를 들어 Xamarin 네이티브 UI 프로젝트에서처럼) UITableViewListView를 직접 조작하는 것이 유용할 수 있습니다. SubscribeForNotifications 확장 메서드를 사용할 수 있습니다. 이 델리게이트를 메서드에 전달해서 변경 집합에 대해 호출되어, 무엇이 추가되고 지워지고 변경되었는지 알 수 있습니다.

realm.All<Person>().SubscribeForNotifications ((sender, changes, error) =>
{
    // Access changes.InsertedIndices, changes.DeletedIndices and changes.ModifiedIndices
});

마이그레이션

어떤 데이터베이스를 쓰더라도 데이터 모델은 시간에 따라 바뀌곤 합니다. Realm의 데이터 모델이 표준 C# 클래스로 정의되었기 때문에 모델의 변경은 다른 클래스를 수정하는 것 만큼 쉽습니다. 예를 들어 아래의 Person 모델을 가지고 있다고 가정해보자.

public class Person : RealmObject
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
}

성과 이름 대신에 FullName 속성을 요구하게 데이터 모델을 갱신하고 싶을 수 있습니다. 그럴 경우 단순히 객체 인터페이스를 아래와 같이 변경합니다.

public class Person : RealmObject
{
    public string FullName { get; set; }
    public int Age { get; set; }
}

이 시점에서 이전 모델 버전에서 어떤 데이터를 저장한다면, 코드 상에 정의된 Realm과 디스크 보이는 Realm의 불일치가 발생합니다. 불일치가 있고 마이그레이션이 수행되지 않으면 기존 파일을 열 때 예외를 발생시킨다.

마이그레이션 수행하기

기본적으로 스키마 버전을 올려야 합니다. 기존에 지정하지 않았다면 realm은 스키마 버전을 0으로 설정합니다. 스키마를 변경하면 설정에서 스키마 버전을 올려야 합니다. 이 방법으로 실수로 인한 스키마 변경이 잠재적인 데이터를 파괴하지 않도록 막습니다.

var config = new RealmConfiguration() { SchemaVersion = 1 };

var realm = Realm.GetInstance(config);

만약에 이렇게 해두고 기존의 스키마로 생성된 데이터베이스 파일을 실행하면 전체 FirstNameLastName 속성이 삭제되고 모든 Person은 비어있는 FullName 속성을 가지게 됩니다. Age 속성은 양쪽 버전에 있고 같은 형이라 그대로 남게 됩니다. 이런 변화를 의도하지는 않았을테니 Realm에 마이그레이션을 다루는 방법을 지정해야 합니다.

이를 위해서는 MigrationCallback 함수를 지정합니다. 이 함수는 Migration 객체를 받으며, 여기에는 두 개의 Realm 속성, OldRealmNewRealm이 있습니다. 이로써 예전 스키마에서 새로운 스키마로 데이터를 복사하고 적용할 수 있습니다.

클래스나 속성만 새로 이름짓는 경우에는 Realm이 이 사실을 감지하지 못하므로 마이그레이션에서 데이터를 복사해야 합니다.

이 콜백 함수는 낮은 버전의 realm이 열릴 때 정확히 한번 호출됩니다. 이 호출 동안 갱신된 클래스 전체를 마이그레이션 해야합니다.

Person 모델은 더 이상 FirstNameLastName 속성을 포함하지 않으니 형에 관한 API를 통해 접근할 수 없습니다. 하지만 동적 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 모델에서 더 나아가 Age 필드에서 Birthday 필드로 변경해 보겠습니다.

public class Person : RealmObject
{
    public string FullName { get; set; }
    public int Age { get; set; }
    public DateTimeOFfset Birthday { get; set; }
}

스키마 버전 2입니다. 마이그레이션 설정은 아래와 같습니다.

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

암호화

미국으로부터의 수출 제한이나 금수 조치가 있는 국가에 거주하는 경우 Realm 사용에 대한 제한이 있을 수 있으므로, 저희 라이센스의 수출 규정 준수 조항을 참고하십시오.

Realm이 생성될 때 64바이트 암호화 키를 제공해 AES-256+SHA2를 이용하여 디스크의 데이터베이스 파일을 암호화하는 것을 제공합니다.

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" 생성하거나 읽을 것입니다

필요에 따라 디스크에 저장되는 모든 데이터에 AES-256와 SHA-2 HMAC에 의해 검증되는 암호화와 복호화를 투명하게 적용할 수 있습니다. 이를 적용하면, Realm 인스턴스를 얻을 때 마다 같은 암호키를 전달해야 합니다.

앱에 대해 유일한 키를 사용해야 합니다. 위의 예제에 사용된 키는 절대 안됩니다. 더 나아가 사용자마다 개인화된 키를 원한다면 Xamarin.Auth API를 살펴보세요.

암호화된 Realm에 대해 잘못된 키를 지정하거나 키를 지정하는데 실패하면 GetInstance를 호출 할 때 RealmFileAccessErrorException을 얻게 됩니다.

Realm 암호화를 사용할 때 작은 성능 하락이 있습니다. (일반적으로 10% 이하 느려집니다.)

동기화

Realm 모바일 플랫폼 (RMP)은 Realm 모바일 데이터 베이스를 네트워크로 확장하여 자동으로 여러 장비간의 데이터를 동기화합니다. 동기화된 Realm 지원을 위해 새로운 타입과 클래스들이 제공됩니다. 새롭게 추가된 객체들은 기존 Realm 모바일 데이터베이스에 추가되었고 여기에 다룹니다.

Realm 모바일 플랫폼 활성화

Realm Mobile Platform을 사용하려면 명시적 단계가 필요하지 않습니다. 표준 Realm NuGet에 기본적으로 설치됩니다.

동기화는 아직 Windows에 구현되지 않았으며, 현재 iOS와 안드로이드에만 적용됩니다.

사용자

Realm 오브젝트 서버의 중심적인 객체는 Realm 사용자 컨셉입니다. User는 동기화된 Realm과 관련됩니다. User는 사용자 이름 / 암호 스키마나 여러 제3자 인증 방법에 의해 인증됩니다.

사용자를 만들고 로그인하기 위해 두 가지 객체가 필요합니다.

  • Realm 인증 서버가 접속할 (문자열로 된) Uri
  • 사용자 인증 메카니즘에 적합한 Credentials (예: 사용자 / 암호, 엑세스 키, 기타)

두 객체는 User 객체를 생성할 때 사용됩니다.

Credential 만들기

Realm 오브젝트 서버 인증에서 제3자 인증 정보를 생성하는 페이지를 찾을 수 있습니다.

Credential을 위한 추가 사항은 다음과 같습니다.

사용자 / 암호
var credentials = Credentials.UsernamePassword(username, password, createUser: false);

UsernamePassword()의 세 번째 인자는 createUser 플래그로, 사용자를 생성해야 한다는 의미로 항상 처음에는 true로 설정해야 합니다. 한번 사용자가 생성이 되면 인자는 false여야 합니다.

혹은 관리자 대시 보드를 사용해서 모든 사용자가 서버에 미리 만들어지도록 요청하고, 항상 createUser 매개 변수에 false를 전달하도록 할 수 있습니다.

Google
var token = "..."; // a string representation of a token obtained by Google Login API
var credentials = Credentials.Google(token);
Facebook
var token = "..."; // a string representation of a token obtained by Facebook Login API
var credentials = Credentials.Facebook(token);
Azure Active Directory
var token = "..."; // a string representation of a token obtained by logging in with Azure Active Directory
var credentials = Credentials.AzureAD(token);

사용자 로그인

사용자에 요구되는 모든 인자가 준비되었기 때문에 Realm 오브젝트 서버에 로그인할 수 있습니다.

var authURL = new Uri("http://my.realm-auth-server.com:9080/auth");
var user = await User.LoginAsync(credentials, authURL);

로그아웃

다음처럼 간단하게 동기화 Realm에서 로그아웃할 수 있습니다.

user.LogOut();

사용자가 로그아웃하면 동기화가 중지됩니다. 로그아웃한 사용자는 더 이상 SyncConfiguration를 사용해서 Realm을 열 수 없습니다.

사용자 저장 설정

기본값으로 Realm은 애플리케이션이 시작할 때마다 로그인한 사용자를 저장합니다. User.ConfigurePersistence를 호출해서 해당 동작을 수정할 수 있습니다. 사용할 수 있는 modes는 다음과 같습니다.

  • Disabled: 사용자가 Realm에 저장되지 않습니다. 즉 사용자 인증 정보를 수동으로 저장하거나, 매번 사용자에게 로그인을 요청해야 합니다.
  • NotEncrypted: 사용자 인증 정보가 암호화되지 않은 상태로 저장됩니다. 이 옵션은 사용자 ID를 도용하기 위한 공격에 노출될 수 있으므로 상용 애플리케이션에 권장되지 않습니다.
  • Encrypted: 사용자 인증 정보가 암호화되어 저장됩니다. 이 옵션을 위해서는 유효한 encryptionKey를 제공해야 합니다.

기본적으로 Realm은 iOS에서 Encrypted 모드를 제공하고 encryptionKey를 iOS 키체인에 저장합니다. 안드로이드에서는 AndroidKeyStore API가 API 레벨 18 이상에서만 제공되기 때문에 NotEncrypted가 사용됩니다. 안드로이드에서 Encrypted 모드를 선택해서 애플리케이션마다 고유한 encryptionKey를 제공하거나, API 레벨 18으로 타깃을 지정하고 KeyStore에 사용자마다 고유한 encryptionKey를 저장하는 것을 권장합니다.

사용자 작업하기

독립적으로 작동하는 Realm에서는 Realm의 부가사항을 설정하기 위해 RealmConfiguration를 사용합니다. Xamarin용 Realm 모바일 플랫폼은 확장된 설정 클래스 SyncConfiguration를 사용합니다.

설정은 인증된 사용자와 싱크 서버 URL에 묶여 있습니다. 싱크 서버 URL은 틸드 문자(“~”)를 포함할 수 있습니다. 이는 사용자의 유일한 식별자를 투명하게 확장하는데 사용될 수 있습니다. 스키마는 손쉽게 개별 사용자를 앱에서 대응할 수 있게 합니다. 디스크에서 저장될 공유 Realm의 위치는 프레임워크에 의해 관리되지만 필요에 의해 변경될 수 있습니다.

var serverURL = new Uri("realm://my.realm-server.com/~/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의 연결은 세션 객체로 표현됩니다. 세션 객체는 realm.GetSession()을 호출해서 얻을 수 있습니다.

기본 세션의 상태는 State 속성을 통해 확인할 수 있습니다. 이를 통해 세션이 활성화되어 있는지, 서버에 연결되지 않았는지, 혹은 에러 상태인지 알 수 있습니다.

Progress Notifications

세션 객체를 사용하면 IObservable을 등록해서 앱에서 세션이 Realm 오브젝트 서버와 업로드 혹은 다운로드하는 상태를 모니터링할 수 있습니다. 이는 [session.GetProgressObservable(https://realm.io/docs/xamarin/latest/api/reference/Realms.Sync.Session.html#Realms_Sync_Session_GetProgressObservable_Realms_Sync_ProgressDirection_Realms_Sync_ProgressMode_)을 호출해서 얻을 수 있습니다.

구독 콜백은 동기화 서브시스템에 의해 주기적으로 호출됩니다. 필요한 만큼의 구독자들을 세션 객체에 동시에 등록할 수 있습니다. 구독자들은 업로드 진행률을 보고하거나 진행률을 다운로드하도록 구성할 수 있습니다. 구독자가 호출될 때마다 이미 전송된 현재 바이트 수에 대한 정보와, 이미 전송된 바이트 수에 전송 대기 중인 바이트 수를 더한 값으로 정의되는 전송 가능한 총 바이트 수에 대한 정보가 들어있는 SyncProgress 인스턴스가 전달됩니다.

구독자가 등록될 때마다 IDisposable 토큰이 반환됩니다. 프로그레스 알림이 필요하지 않을 때까지 이 토큰의 참조를 저장해야 합니다. 받고있는 알림을 중단하려면 토큰에서 Dispose를 호출하세요.

프로그레스 구독에는 두 가지 모드가 있습니다.

  • ReportIndefinitely 모드에서는 Dispose가 명시적으로 호출될 때까지 구독이 활성화된 상태이며, 항상 가장 최신의 전송 가능한 바이트 수를 보고합니다. 이 콜백 타입을 사용해서 네트워크 표시기 UI를 제어할 수 있습니다. 예를 들어 업로드나 다운로드가 활발히 진행되는 경우에 색이 변하거나 특정 UI가 등장하도록 할 수 있습니다.
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 확장 메서드를 사용했습니다. 실제 사용에서는 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 모바일 플랫폼은 유연한 접근 제어 방식을 제공해서 어떤 사용자가 어떤 Realm 파일과 동기화할 수 있는지에 대한 권한을 허용할 수 있게 합니다. 예를 들어 여러 사용자가 동일한 Realm에 쓸 수 있는 협업 앱을 만들 수 있습니다. 또한 한 사용자가 만든 데이터를 읽기 퍼미션을 가진 여러 사용자와 공유하는 게시자/구독자 시나리오에도 활용할 수 있습니다.

Realm의 접근 수준, 즉 권한 제어는 세 가지 축으로 분류됩니다.

  • mayRead Realm으로부터 사용자가 읽을 수 있는지를 표기합니다.
  • mayWrite Realm에 사용자가 쓸 수 있는지를 표기합니다.
  • mayManage 사용자가 Realm의 권한을 변경할 수 있는지 여부를 표기합니다.

권한을 명시적으로 바꾸지 않으면 Realm의 소유자만 접근할 수 있습니다. 예외는 관리자들입니다. 그들은 언제나 서버의 모든 Realm의 모든 권한을 가집니다.

접근 제어에 대한 자세한 내용은 Realm 오브젝트 서버 문서의 접근 제어 섹션을 참조하세요.

관리 Realm

모든 접근 레벨 관리 작업은 관리 Realm에 기록하는 것으로 수행할 수 있습니다. 관리 Realm은 일반적인 동기화 Realm과 마찬가지로 읽고 쓸 수 있지만, Realm 오브젝트 서버는 이 관리 Realm의 변경 사항에 대응하도록 특별히 설계됐습니다. 권한 변경된 객체를 관리 Realm에 추가해서 Realm 파일의 접근 제어 설정을 수정할 수 있습니다.

특정 사용자에 대한 관리 Realm을 얻으려면 user.GetManagementRealm() 확장 메서드를 호출합니다.

var managementRealm = user.GetManagementRealm();

권한 수정

Realm 파일에 대한 접근 제어 설정을 수정하려면 관리 Realm에 IPermissionObject 인스턴스를 추가합니다. 현재 두 가지 사용 방법을 제공합니다.

PermissionChange

PermissionChange 객체로 Realm의 접근 제어 설정을 직접 조작할 수 있습니다. 권한을 주고자 하는 사용자의 ID들을 아는 경우나 모든 사용자에게 권한을 줄 때 유용합니다.

var permissionChange = new PermissionChange(realmUrl,
                                            anotherUserId,
                                            mayRead: true,
                                            mayWrite: true,
                                            mayManage: false);

managementRealm.Write(() =>
{
    managementRealm.Add(permissionChange);
});

동기화 사용자가 관리하는 모든 Realm에 대한 권한 변경 사항을 적용하려면, realmURL 값을 *로 지정하세요.

오브젝트 서버에 허가된 모든 동기화 사용자에 대한 사용 권한 변경 사항을 적용하려면 userID 값을 *로 지정하세요.

만약 mayRead, mayWrite, mayManage 값을 제공하지 않거나 null 값을 넣으면 read, write, manage 권한은 변경되지 않습니다. read 권한만 사용자에게 주고 write 권한을 이미 가진 사용자들의 권한은 유지하고 싶은 경우 등에서 유용합니다.

managementRealm.Write(() =>
{
    foreach (var id in userIds)
    {
        var permissionChange = new PermissionChange(realmUrl, userId, mayRead: true);
        managementRealm.Add(permissionChange);
    }
});

이렇게하면 목록 내의 모든 사용자에게 read 권한이 부여되지만 writemanage 접근 권한을 가진 사람들의 권한은 그대로 유지됩니다.

PermissionOffer/PermissionResponse

PermissionOfferPermissionOfferResponse 클래스 쌍을 사용해서 초대자가 토큰을 생성하고 한 명 이상의 사용자에 의해 소비되도록 할 수 있습니다.

var permissionOffer = new PermissionOffer(realmUrl,
                                          mayRead: true,
                                          mayWrite: true,
                                          mayManage: false,
                                          expiresAt: DateTimeOffset.UtcNow.AddDays(7));

managementRealm.Write(() =>
{
    managementRealm.Add(permissionOffer);
});

/* Wait for the offer to be processed */

var token = permissionOffer.Token;

/* Send token to the other user */

PermissionChange와 마찬가지로 제공된 realmUrl에서 read, write, manage 접근을 제어하는 인자가 있습니다. 다른 추가 인자인 expiresAt는 토큰이 더 이상 소비되지 않도록 시점을 조절합니다. 값을 전달하지 않거나 null을 전달하면 토큰은 만료되지 않습니다. 토큰을 이미 사용한 사용자는 만료된 후에도 접근 권한을 잃지 않습니다.

다른 사용자가 token을 얻으면 아래와 같이 소비할 수 있습니다.

var offerResponse = new PermissionOfferResponse(token);
var managementRealm = anotherUser.GetManagementRealm();
managementRealm.Write(() =>
{
    managementRealm.Add(offerResponse);
});

/* Wait for the offer to be processed */

var realmUrl = offerResponse.RealmUrl;

// Now we can open the shared realm:
var configuration = new SyncConfiguration(anotherUser, new Uri(realmUrl));
var offeredRealm = Realm.GetInstance(configuration);

PermissionOffer를 사용해서 추가된 권한은 부가적입니다. 만약 사용자가 이미 write 권한을 가지고 있는 상태에서 PermissionOffer를 통해 read 권한을 받는다면 기존의 write 권한은 유지됩니다.

PermissionOffer를 취소하려면 관리 Realm에서 삭제하거나 ExpiresAt의 날짜를 과거로 설정합니다. 이렇게 하면 새 사용자가 PermissionOffer를 수락하지 못하지만, 이미 사용한 사용자의 사용 권한은 취소하지 않습니다.

서버에서 알림받기

오브젝트 서버가 IPermissionObject로 인코딩된 작업을 처리하면 해당 객체의 StatusStatusMessage속성이 설정됩니다.

해당 객체의 PropertyChanged를 구독해서 권한 변경 작업에 대한 결과를 알림받을 수 있습니다. 객체 알림 세션을 확인하세요.

권한 변경 작업의 상태를 알림받는 방법입니다.

permissionObject.PropertyChanged += (s, e) =>
{
    if (e.PropertyName == nameof(IPermissionObject.Status))
    {
        switch (permissionObject.Status)
        {
            case ManagementObjectStatus.NotProcessed:
                // Handle case
                break;
            case ManagementObjectStatus.Success:
                // Handle case
                break;
            case ManagementObjectStatus.Error:
                // Handle case
                break;
        }
    }
};

에러 보고

특정 동기화 관련 API를 통해 실패할 수 있는 비동기 작업을 수행할 수 있으며, 실패할 경우 예외가 발생해서 try-catch 블록에서 확인할 수 있습니다.

Session.Error 이벤트를 구독할 것을 강력히 추천 합니다. 전역 동기화 서브시스템이나, 서버와 동기화를 위해 열린 Realm을 나타내는 특정 세션과 관련된 오류는 이 에러 핸들러를 통해 보고됩니다. 오류가 발생하면 에러 핸들러는 에러를 포함한 ErrorEventArgs와, 에러가 발생한 세션을 나타내는 Session과 함께 호출됩니다.

Session.Error += (session, errorArgs) =>
{
    // handle error
};

Errors

Realm 모바일 플랫폼 에러는 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 객체 서버에 손상이 발생하고 백업에서 복원해야 하는 경우, 동기화 Realm의 클라이언트 재설정 을 앱에서 수행해야 할 수 있습니다. 이는 문제가 되는 Realm의 로컬 버전이 서버의 동일 Realm 버전보다 클 때 발생합니다. 예를 들어 Realm 오브젝트 서버의 백업 이후 애플리케이션이 변경 사항을 만들었지만, 이 변경 사항이 서버 손상 이전에 동기화되지 않았기 때문에 복원이 필요한 경우입니다.

클라이언트를 재설정하는 절차는 다음과 같습니다. 로컬 Realm 파일의 백업 사본이 만들어지면 Realm 파일이 삭제됩니다. 다음에 앱이 Realm 오브젝트 서버에 접속하고 Realm을 열면 새로운 사본이 다운로드됩니다. Realm 오브젝트 서버가 백업된 이후에 변경돼서 아직 서버로 동기화되지 않은 내용은 Realm의 백업 복사본에 보존되지만, Realm을 재다운로드하는 경우에는 존재하지 않습니다.

클라이언트 재설정 필요 사항은 Session.Error 구독자에게 전달되는 에러로 표시됩니다. sessionException.ErrorCode.IsClientResetError의 결과를 확인하거나 예외를 ClientResetException로 안전하게 캐스팅해서 에러 타입을 확인할 수 있습니다. 이 타입은 추가 속성인 BackupFilePath를 가지며, 여기서 클라이언트 재설정 프로세스가 발생한 다음 Realm 파일의 백업 복사본이 위치할 곳을 알 수 있습니다. 또한 클라이언트 재설정 프로세스를 시작할 수 있는 InitiateClientReset 메서드도 포함됩니다.

만약 클라이언트 재설정 프로세스를 시작하기 위해 메서드를 호출한다면, 메그 전에 문제가 있는 모든 Realm 인스턴스를 Dispose()를 호출해서 반드시 무효화하고 없애야 합니다. 이렇게 하면 클라이언트 재설정 프로세스가 완료된 후 Realm을 즉시 다시 열 수 있으므로 다시 동기화를 시작할 수 있습니다.

InitiateClientReset이 호출되지 않은 경우, 클라이언트 재설정 프로세스는 다음에 앱이 시작될 때 자동으로 실행됩니다. 처음에는 Realm 인스턴스에 접근합니다. 필요한 경우 백업 복사본의 위치를 유지하여 나중에 찾을 수 있도록 앱에서 관리해야 합니다.

재설정이 필요한 Realm을 계속해서 읽고 쓸 수는 있지만, 클라이언트 재설정이 완료되고 Realm이 다시 다운로드될 때까지 이후 변경 사항이 서버에 동기화되지 않습니다. 따라서 애플리케이션이 클라이언트 재설정 에러를 관찰하거나, 최소한 재다운로드된 Realm 복사본에 쓸 수 있도록 클라이언트 재설정이 유발된 후 새로 만들어지거나 수정된 사용자 데이터를 저장해야 합니다.

다음 예제에서 클라이언트 재설정 API를 사용해서 클라이언트를 재설정하는 방법을 보여드립니다.

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 오브젝트 서버가 클라이언트 재설정을 처리하는 방법에 대해 더 자세히 알고 싶다면 서버 문서를 참고하세요.

마이그레이션

동기화 Realm은 자동 마이그레이션이 지원됩니다. 현재까지는 현존 클래스에 클래스나 필드를 추가하는 등의 추가적인 변화만이 지원됩니다. 추가적인 변화가 아니라면 예외가 발생합니다.

마이그레이션은 필요시 자동으로 수행됩니다. 커스텀 마이그레이션 콜백이나 스키마 버전 숫자를 설정할 필요가 없습니다.

충돌 해결

Realm 오브젝트 서버 문서에서 충돌 해결 지원에 관한 내용을 볼 수 있습니다.

현재 제한 사항

Realm은 가능한 제약 사항을 적게 하려고 노력하고 있으며, 커뮤니티의 피드백에 맞춰 새로운 기능을 지속적으로 추가하고 있습니다. 다만 Realm에는 아직까지 제한 사항이 있습니다.

알려준 이슈에 대한 자세한 정보는 GitHub 이슈에서 확인할 수 있습니다.

일반적인 제한

Realm은 유연함과 성능 사이에 균형을 찾는 것을 목표로 합니다. 목적을 이루기 위해 Realm에 정보를 저장하는 여러 측면에 현실적인 제약을 시행하게 됩니다. 예를 들어서 다음과 같습니다.

  1. 클래스 이름은 0부터 57 바이트 사이의 길이여야 합니다. UTF-8 문자열이 지원됩니다. 앱의 초기화 때 제한을 넘게 되면 예외가 던져집니다.
  2. 프로퍼티 이름은 0부터 63 바이트 사이의 길이어야 합니다. UTF-8 문자열이 지원됩니다. 앱의 초기화 때 제한을 넘게 되면 예외가 던져집니다.
  3. iOS 제한: 열린 Realm 파일의 사이즈는 iOS에 연결되어 애플리케이션에 허용된 총 메모리 사이즈보다 클 수 없습니다. 이는 디바이스마다 다르고 메모리 공간이 실행 시점에 어떻게 파편화되었냐에 의존합니다. (여기에 대한 radar 이슈가 있습니다. rdar://17119975) 더 많은 데이터를 저장하길 원한다면 여러 개의 Realm 파일로 분할하고 그들을 필요에 따라 열고 닫을 수 있습니다.

스레드

Realm 파일은 여러 스레드에서 동시에 엑세스할 수 있지만 Realm, Realm 객체, 쿼리와 결과들을 스레드 사이에 넘길 수는 없습니다. 더 자세한 내용은 스레드 섹션을 확인하세요.

파일 사이즈와 중간 버전의 추적

Realm 데이타베이스는 동등한 SQLite 데이터베이스에 비해 일반적으로 디스크 공간을 더 적게 사용합니다. 만약 기대보다 Realm 파일이 크다면 이것은 RealmObject가 데이터베이스 데이터의 오래된 버전을 참조하기 때문입니다.

데이터의 일관된 뷰를 제공하기 위해 Realm은 런 룹 단계의 시작에서만 접근된 활성 버전을 갱신합니다. 만약 Realm으로부터 어떤 데이터를 읽었습니다. 그리고는 다른 스레드에서 Realm에 기록하는 동안 오래 수행되는 연산으로 스레드를 막았습니다. 버전은 갱신되지 못하고 Realm은 당신이 필요하지 않은 중간 버전 데이터를 붙잡고 있습니다. 그 결과로 매 쓰기마다 파일의 크기는 증가하게 됩니다. 여분의 공간은 결국에는 다음 쓰기에 재사용이 됩니다.

저장 공간을 확보하기 위해서는 Realm 압축하기 섹션을 참고하세요.

Windows 데스크탑에서의 제한 사항

Realm Xamarin에서 지원하는 모든 플랫폼 간의 기능을 동일하게 유지할 수 있도록 노력하고 있지만 아직 Windows에 특정한 몇 가지 제한 사항이 존재합니다.

  1. 동기화가 아직 구현되지 않았습니다. Realm의 부모 NuGet 패키지를 Windows 애플리케이션에서 사용하면 빌드타임 에러가 발생합니다. 이 대신 오프라인이 가능한 바이너리를 포함하고 있는 Realm.Database 패키지를 사용하세요.
  2. 아직 암호화가 지원되지 않습니다. null이 아닌 EncryptionKey를 가진 RealmConfiguration으로 Realm.GetInstance를 호출하면 런타임 예외가 발생합니다.
  3. 많은 다른 프로세스가 같은 realm 파일을 열면 일부 구독자는 변경 알림을 받지 못할 수 있습니다.

쿼리응답

일반

Realm은 오픈소스인가요?

맞습니다. Realm 내부의 C++ 저장소 엔진과 그 위의 언어 SDK들은 전적으로 오픈소스이고 Apache 2.0 라이선스입니다. 또 Realm은 선택적으로 사용하는 Realm 플랫폼 확장을 가지고 있고 이는 비공개 소스이자만 Realm을 임베디드 데이터베이스로 사용할 때 필수인 건 아닙니다.

내 앱을 빌드할 때 Mixpanel에 네트워크 호출을 하는 것을 보았는데 이건 무엇인가요?

RealmObject를 포함하고 있는 여러분의 어셈블리에 대해 Realm 어셈블리 위버를 호출할 때 Realm은 익명의 통계를 수집합니다. 완전히 익명이고 어떤 Realm 버전을 쓰고 어떤 OS를 쓰는지 등을 파악해서 어떻게 제품을 향상하고 어떤 지원을 중단할 수 있는지 우리가 파악할 수 있게 합니다. 실제 앱이 수행될 때 혹은 여러분의 디바이스에서 수행될 때는 어떤 호출도 없습니다. - 빌드 과정에 Fody에 의해 여러분의 어셈블리를 위브할때만 호출이 됩니다. 정확히 어떻게 무엇을 수집하는지는 소스 코드에서 확인하실 수 있습니다.

포터블 (PCL) 라이브러리와 함께 쓸 수 있나요?

실행파일에 iOS, 안드로이드, Win32 버전의 Realm을 사용할 필요가 있지만 우리는 PCL을 제공하고 있고 Realm API를 컴파일할 때 사용할 수 있습니다. NuGet 유인 상술입니다. NuGet을 통해 Realm을 PCL 프로젝트에 추가할 때 이는 실행 파일에 추가되고 동작됩니다.

이 PCL이 Realm을 임의의 .NET 플랫폼에서 사용할 수 있다는 의미는 아닙니다. Realm은 각 플랫폼으로 포팅되어야 할 C++ 코어에 근간하고 있습니다.

트러블 슈팅

크래쉬 보고

우리는 애플리케이션에서 크래쉬 리포터를 사용하는 것을 장려합니다. 많은 Realm 연산은 (다른 디스크 IO와 같이) 잠재적으로 실시간에 실패할 수 있습니다. 애플리케이션으로 부터 수집된 크래쉬 보고는 문제가 여러분(이나 우리)에게 있는지 확인할 수 있고 에러 처리를 향상하고 크래쉬 버그를 잡게 합니다.

대부분의 상업 크래쉬 리포터들은 로그 수집 옵션이 있습니다. 우리는 강하게 이 기능을 활성화하는 것을 추천합니다. Realm은 예외가 발생하거나 복구불가능한 상황일 때 (사용자 데이터에 관련되지 않은) 메타데이터 정보를 로그로 남깁니다. 이 메시지들은 문제가 발생했을 때 디버그 할 때 도움이 됩니다.

Realm 이슈 보고하기

Realm에 이슈를 찾으면 우리가 이슈를 이해하거나 재연할 수 있도록 최대한 많은 정보를 담아 GitHub에 이슈를 발행하거나 help@realm.io로 메일을 주세요.

아래의 정보는 우리에게 매우 유용합니다.

  1. 목표
  2. 기대 결과
  3. 실제 결과
  4. 재연할 수 있는 단계
  5. 이슈를 특정짓는 코드 샘플 (빌드할 수 있는 전체 Xamarin 프로젝트가 우리에게 이상적입니다)
  6. Realm, OS X나 Windows, Xamarin/Visual Studio의 버전 정보들
  7. 대상이 iOS면 Xcode, 대상이 안드로이드인 경우 NDK 정보
  8. 버그가 발생한 플랫폼, OS 버전, 아키텍쳐 (예: 64 비트 iOS 8.1)
  9. 크래쉬 로그, 스택 트레이스. 자세한 것은 크래쉬 보고를 살펴보세요.

클래스에 속성이 없다는 예외가 발생하면

System.InvalidOperationException를 “No properties in class, has linker stripped it?” 메시지와 함께 받을 수 있습니다.

알려진 두가지 원인이 있습니다.

  1. 거의 Fody가 잘못되어서 위브된 RealmObjects가 없는 경우이거나,
  2. RealmObjects가 스트립된 속성을 가지고 있어 Realm에 비어있게 전달된 경우.

첫 번째 경우는 https://realm.io/docs/xamarin/latest/api/reference/Realms.Schema.RealmSchema.html에서 예외가 발생합니다. 자세한 내용은 위브에 실패할 때를 보세요.

두 번째 경우는 ObjectSchema로부터 예외가 발생합니다. 몇몇 사용자들이 Linker BeahviourLink All로 설정하고 그들의 클래스 정의에 [Preserve(AllMembers = true)]을 누락했을 때 문제가 발생하는 것을 확인했습니다. 링커들은 코드에서 명시적으로 참조하는 것만 보호합니다. 영속적으로 보관되어야 속성들이 있는데 어디에서도 참조되지 않아 제거되면 데이터베이스가 불일치에 빠질 수 있습니다.

기본적으로 Realm은 어셈블리에 있는 모든 RealmObject로 표현되는 스키마를 빌드합니다. 이는 지연 으로 이뤄지고 GetInstance()가 처음 호출될 때 까지 호출되지 않습니다. 이는 최대 한번 이루어지며 메모리 상에 결과가 캐쉬됩니다.

주어진 어셈블리에서 클래스 중 어떤 것만 저장하고 싶다면 클래스 부분집합을 정의하고 GetInstance(myConf)의 설정을 이용하세요. 이는 전체 빌드를 막고 또 예외도 막습니다.

아니면 사용하지 않는 클래스에 Preserve 특성을 지정하세요.

Fody: 처리되지 않는 예외 발생

이런 기본적인 실패는 이미 빌드된 프로젝트에 새로운 RealmObject 서브클래스를 추가하는 것으로 간단히 발생시킬 수 있습니다.

Build or Run을 선택하면 Fody Weaver를 실행할만큼 충분히 프로젝트를 재빌드하지 않습니다.

단순히 프로젝트에 대해 리빌드를 선택하면 에러없이 빌드됩니다.

위브에 실패할 때

클래스가 위브되지 않았다는 클래스에 관한 빌드 로그에서 워닝을 볼 수 있습니다. 이는 Fody 에 패키지가 완전히 설치되지 않았다는 것을 의미합니다.

첫 번째로 RealmWeaver에 대한 항목이 FodyWeavers.xml에 있는지 확인하세요.

설치는 RealmWeaver.Fody.dll를 여러분의 솔루션 디렉토리의 Tools에 직접적으로 복사합니다. (이것은 보통 프로젝트 디렉토리의 이웃입니다.) 이것은 프로젝트의 어디에도 연결되어 있지 않지만 Fody가 DLL을 찾기 위해 그 자리에 있어야 합니다.

Fody의 설치가 실패했을 가능성도 있습니다. Visual Studio 2015와 3.2 버전 이전의 NuGet 패키지 매니저를 쓸 때 경험할 수 있습니다. 이 창이 뜨면 .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 답변를 살펴보세요.

Realm Core Binary 다운로드 실패

Realm을 빌드할 때 처리의 한 부분으로 코어 라이브러리를 정적 라이브러리로 다운받고 Realm 프로젝트에 통합하는 과정이 있습니다. 이것은 wrappers의 한 부분으로 Makefile로 빌드됩니다.

어떤 경우에 아래의 에러와 함께 코어 바이너리 다운이 실패합니다.

Downloading core failed. Please try again once you have an Internet connection.

아래의 이유에 따라 에러가 발생합니다.

  1. 미국의 금수조치 목록에 포함된 지역의 IP 주소를 사용하는 경우. 미국 법률에 따라 Realm은 이 지역에서 사용할 수 없습니다. 자세한 정보를 알고 싶다면 라이선스를 살펴보세요.
  2. 중국 본토에 있다면 국가 수준의 방화벽 때문에 CloudFlare나 Amazon AWS S3 서비스를 상황에 따라 사용할 수 없을 수 있습니다. 자세한 내용은 우리 다른 제품의 해당 이슈를 참조하세요.
  3. Amazon AWS S3의 서비스에 이슈가 있을 수 있습니다. AWS Service 상태 대시보드를 참고하고 다시 시도하세요.