최신 영문 문서를 보려면 이곳을 참고하세요.
Realm 자바는 손 쉽게 앱 모델 영역을 안전하고, 영속적이고 빠르게 만듭니다. 아래 코드 예제를 확인해보세요.
// RealmObject를 확장하여 모델을 정의합니다
public class Dog extends RealmObject {
private String name;
private int age;
// ... 생성된 getter와 setter
}
public class Person extends RealmObject {
@PrimaryKey
private long id;
private String name;
private RealmList<Dog> dogs; // 일 대 다 관계를 정의합니다
// ... 생성된 getter와 setter
}
// 일반적인 자바 객체처럼 사용합니다
Dog dog = new Dog();
dog.setName("Rex");
dog.setAge(1);
// 패키지의 "files" 디렉터리의 Realm파일을 저장하는 RealmConfiguration 생성하기.
RealmConfiguration realmConfig = new RealmConfiguration.Builder(context).build();
Realm.setDefaultConfiguration(realmConfig);
// 이 스레드의 Realm 인스턴스 얻습니다
Realm realm = Realm.getDefaultInstance();
// 2살 미만의 모든 개에 대한 Realm 질의합니다
final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => Realm에 아직 개가 추가되지 않았기 때문에 0
// 트랜잭션을 통해 데이터를 영속화합니다
realm.beginTransaction();
final Dog managedDog = realm.copyToRealm(dog); // 비관리 객체를 영속화합니다
Person person = realm.createObject(Person.class); // 관리 객체를 직접 만듭니다
person.getDogs().add(managedDog);
realm.commitTransaction();
// 데이터가 변하면 리스너들에게 알림이 갑니다
puppies.addChangeListener(new RealmChangeListener<RealmResults<Dog>>() {
@Override
public void onChange(RealmResults<Dog> results) {
// 질의 결과는 실시간으로 갱신됩니다
puppies.size(); // => 1
}
});
// 백그라운드에서 비동기적으로 갱신합니다
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
Dog dog = bgRealm.where(Dog.class).equalTo("age", 1).findFirst();
dog.setAge(3);
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// 원래 질의와 Realm 객체는 자동으로 갱신됩니다
puppies.size(); // => 0. 2살 미만의 강아지가 더 이상 없기 때문에
managedDog.getAge(); // => 3. 강아지의 나이가 갱신되었습니다
}
});
시작하기
Android Realm 다운로드 또는 깃헙 저장소 realm-java에서 소스코드를 직접 받을 수 있습니다.
요구사항
- 현재로서는 Android 이외의 Java 환경은 지원하지 않습니다.
- Android Studio >= 1.5.1
- 최신 버전의 Android SDK
- JDK 버전 >= 7.
- API Level 9 이상의 모든 Android 버전을 지원합니다. (Android 2.3 진저브레드 버전 이상)
설치하기
Realm은 Gradle 플러그인으로 설치됩니다.
Realm을 Gradle 플러그인으로 설치하는 것은 두 단계의 과정입니다.
단계 1: 아래의 클래스 패스 의존성을 프로젝트 수준 build.gradle 파일에 추가합니다.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:1.0.0"
}
}
프로젝트 수준의 build.gradle
파일은 여기에 위치합니다.
단계 2: realm-android
플러그인을 어플리케이션 수준 build.gradle
파일에서 적용시킵니다.
apply plugin: 'realm-android'
한번 이 두 가지가 변경이 되면 단순히 그래들 의존성을 리프레쉬하세요. 만약 v0.88
이전 버전에서 업그레이드한다면 남겨진 기존 설치 파일을 제거하기 위해 그래들 프로젝트를 클린 할 필요가 있습니다. (./gradlew clean
)
수정된 두 build.gradle
파일에 대한 샘플은 여기에서 찾을 수 있습니다.
다른 빌드 시스템
Maven과 Ant 빌드 시스템은 지원되지 않습니다. 이 빌드 시스템의 지원에 관심이 있다면 아래 이슈에서 관심을 표현해주세요.
Realm 개발팀은 여러분의 의견을 듣고 Ant와 Maven 플러그인에 대해 무엇을 언제 할지 결정하게 하고 있습니다.
v1.0.0
이상에서 Elipse는 더 이상 지원되지 않습니다. 우리는 Android Studio로 이전하는 것을 제안합니다.
ProGuard
ProGuard 설정이 Realm 라이브러리에 포함되어있습니다. Realm에 특화된 ProGuard 규칙을 추가할 필요가 없다는 의미입니다.
Realm 브라우저
.realm 데이터베이스를 읽고 수정하기 위해서 맥앱 스토어에서 어플리케이션을 받아 사용하실 수 있습니다.
Tools > Generate demo database 를 사용하여 새로운 테스트 데이터베이스를 예제 데이터와 함께 생성할 수 있습니다.
Realm 파일을 찾는데 도움이 필요하면 StackOverflow 답변에서 상세한 설명을 읽어보세요.
Realm 브라우저는 맥 앱스토어에서 찾을 수 있습니다. GitHub 페이지에서 다운로드할 수도 있습니다.
윈도우즈와 리눅스에서는 현재 브라우저가 지원되지 않습니다. 다른 플랫폼에서 사용한다면 대신에 Stetho-Realm를 사용하세요. Stetho는 Facebook이 만든 크롬 브라우저를 위한 안드로이드 디버그 브릿지입니다.
API 문서
전체 API 문서에서 모든 클래스, 메서드 와 그 외 다양한 부분을 살펴볼 수 있습니다.
예제
실제 앱에서 Realm이 실제적으로 쓰이는 코드를 확인하기 위해서는 examples에서 확인하세요. Android Studio에서 Import Project
를 선택한 후 run
을 실행해보세요.
introExample는 현재 API를 어떻게 사용하는지를 보여주는 간단한 예제를 담고 있습니다.
gridViewExample은 GridView을 위한 뒷단의 스토어로 Realm을 어떻게 쓰는지 보여주는 작은 앱입니다. 또한 이 앱은 GSON을 이용해서 JSON으로 데이터베이스를 채우는 것과 최종 APK의 크기를 줄이기 위해 ABI를 어떻게 나누는지를 보여주고 있습니다.
threadExample은 다중 스레등 환경에서 Realm을 어떻게 쓰는지 보여주는 간단한 앱입니다.
adapterExample는 RealmResults를 편리하게 안드로이드 리스트에 연결하기 위해 RealmBaseAdapter를 어떻게 쓰는지 보여줍니다.
adapterExample는 Realm
과 ListView, RecyclerView를 우아하게 같이 쓰기 위해 어떻게 RealmBaseAdapter
와 RealmRecyclerViewAdapter
를 써야하는지 보여줍니다.
jsonExample은 새 Realm JSON 기능을 어떻게 사용하는지 설명합니다.
encryptionExample은 Realm에서 암호화를 어떻게 해야 하는지 보여줍니다.
rxJavaExamples는 RxJava와 어떻게 같이 동작하는지 보여줍니다.
unitTestExample는 Realm과 유닛 테스트를 함께 쓰는 법을 설명합니다.
도움을 구하려면
- 여러분의 코드에 관련된 도움이 필요하세요? 스택 오버플로우에 질문하세요. 우리는 적극적으로 스택오버플로우를 살펴보고 있고 답변하고 있습니다!
- 제보할 버그가 있나요? 우리의 리포지토리에 이슈를 열어주세요. 가능하면 Realm의 버전, 전체 로그, Realm 파일, 이슈를 보여줄 수 있는 프로젝트를 포함해주세요.
- 요청할 기능이 있나요? 우리의 리포지토리에 이슈를 열어주세요. 우리에게 어떤 기능이 있어야 하는지 왜 필요한지 알려주세요.
- 다음에 무엇이 나올지 알고 싶으신가요? 변경 내역을 살펴보세요. 로그는 최근에 추가된 것과 앞으로 변경될 점, Realm이 어떻게 진화해왔는지 역사를 보여줍니다.
- 한국에서 격월로 ‘Realm 사용자 모임’을 하고 있습니다. 저희 페이스북 페이지, 페이스북 그룹을 참고해주세요. 한국어 이메일로 이메일 주셔도 됩니다.
모델
Realm 모델 클래스는 RealmObject
기반 클래스를 상속받아서 생성합니다.
public class User extends RealmObject {
private String name;
private int age;
@Ignore
private int sessionId;
// IDE에 의해 생성된 표준 게터와 세터들...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public int getSessionId() { return sessionId; }
public void setSessionId(int sessionId) { this.sessionId = sessionId; }
}
Realm 모델 클래스는 또 커스텀 메서드에 대한 public
, protected
, private
필드를 지원합니다.
public class User extends RealmObject {
public String name;
public boolean hasLongName() {
return name.length() > 7;
}
@Override
public boolean equals(Object o) {
// Custom equals comparison
}
}
필드 타입
Realm은 다음의 필드 타입을 지원합니다. boolean
, short
, int
, long
, float
, double
, String
, Date
, byte[]
. 정수 타입의 short
, int
, long
은 Realm 내에서 모두 같은 long
타입으로 대응됩니다. 추가로, RealmObject
의 서브클래스와 RealmList<? extends RealmObject>
가 관계 모델을 지원합니다.
박스 타입 Boolean
, Byte
, Short
, Integer
, Long
, Float
, Double
도 모델에 사용할 수 있습니다. 이 타입들을 사용하면 필드에 null
값을 넣을 수 있습니다.
Required 필드와 널 값
null
이 필드의 값으로 부적당한 경우가 있습니다. @Required
어노테이션을 null
값을 허용하지 않기를 강제하기 위해 사용할 수 있습니다. 단지 Boolean
, Byte
, Short
, Integer
, Long
, Float
, Double
, String
, byte[]
, Date
만 Required로 지정될 수 있습니다. 다른 형에 대해 @Required
어노테이션이 주어지면 컴파일을 실패합니다. 기본형과 RealmList
형에 대한 필드들은 암묵적으로 Required로 취급됩니다. RealmObject
형의 필드들은 항상 널이 가능합니다.
무시된 속성
@Ignore
어노테이션을 사용하여 필드가 디스크에 저장되지 않도록 할 수 있습니다. 필드를 무시하는 것은 사용자의 입력이 모델의 필드보다 많을 때, 쓰지 않을 데이터 필드를 다루는 특별 케이스를 만들 필요를 없애 주기 때문에 매우 유용합니다.
자동 갱신 객체
Realm 객체와 RealmResults
는 기반 데이터에 의해 라이브로 자동 갱신되는 뷰입니다. 즉, 결과를 매번 다시 가져올 필요가 없다는 의미입니다. 오브젝트를 수정하면 질의는 즉시 결과로 반영됩니다.
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Dog myDog = realm.createObject(Dog.class);
myDog.setName("Fido");
myDog.setAge(1);
}
});
Dog myDog = realm.where(Dog.class).equalTo("age", 1).findFirst();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Dog myPuppy = realm.where(Dog.class).equalTo("age", 1).findFirst();
myPuppy.setAge(2);
}
});
Realm 객체와 RealmResults
의 이런 특징은 Realm을 더 빠르고 효율적으로 하는 동시에 여러분의 코드를 더 간결하고 반응성 있게 만들어줍니다. 예를 들어 액티비티와 프래그먼트가 질의의 결과에 의존하고, 매 접근에 앞서 데이터를 최신으로 갱신할 필요 없이 Realm 객체나 RealmResults
를 필드에 저장하여 쓸 수 있습니다.
Realm 데이터 갱신을 알고 그에 따라 앱의 UI를 변경하기 위해 RealmResults
를 다시 가져오는 것 대신에 Realm 알림을 구독할 수도 있습니다.
색인된 속성
@Index
어노테이션은 해당 필드에 검색 색인을 추가합니다. 삽입 처리가 느리며 데이터의 파일을 크게 만들지만 질의 처리를 빠르게 할 수 있습니다. 읽기 성능에 특정해서 최적화를 하는 경우에만 색인을 추가하길 추천합니다. String
, byte
, short
, int
, long
, boolean
, Date
필드용 색인을 지원합니다.
기본 키
필드를 기본 키로 다루기 위해서 @PrimaryKey
어노테이션을 사용해야 하며 필드 타입은 문자열(String
)이거나 정수(byte
, short
, int
, long
)나 대응되는 박스형(Byte
, Short
, Integer
, Long
)이어야 합니다. 여러 필드(혼합 키)를 기본 키로 사용할 수는 없습니다. 문자열을 기본 키로 사용하면 해당 필드는 인덱싱됩니다. (@PrimaryKey
어노테이션은 암묵적으로 @Index
어노테이션을 설정합니다.)
Realm 객체가 생성될 때 모든 필드는 초기 값이 설정됩니다. 같은 기본 키 값을 가지고 있는 항목과 충돌을 피하기 위해서는 독립적인 객체를 만들고 필드의 값을 할당을 한 후 (copyToRealm()
메서드를 사용하여) Realm에 객체를 복사하길 권장합니다. 독립적인 객체를 다루기 위해서 아래를 참고하세요. 그리고, createOrUpdate()
메서드를 사용하여 객체를 생성하거나 수정할 수 있습니다. 해당 메서드는 기본 키 값이 존재하지 않으면 새 객체를 생성합니다. 객체가 이미 존재한다면 수정됩니다.
기본 키를 사용하여 속도를 향상시킬 수 있습니다. 질의는 다소 속도 향상을 기대할 수 있는 반면에 객체를 생성하고 수정할 때에는 다소 속도 하락이 있을 수 있습니다. 데이터 크기에 따라 속도 편차가 존재하기 때문에 정확한 수치를 명시하기 어렵습니다.
Realm.createObject()
을 호출하면 모든 필드가 기본 값으로 설정된 새로운 객체를 얻습니다. 이 경우 기본 키가 기본 값으로 설정이 되어 기존 객체와 충돌할 수 있습니다. 이것을 막기 위해 비관리(unmanaged) 객체를 만들어 값을 설정한 후 copyToRealm()
를 통해 Realm으로 복사하는 것을 권장합니다.
final MyObject obj = new MyObject();
obj.setId(42);
obj.setName("Fish");
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
// 이것은 Realm의 새 클래스를 만들거나 이미 객체가 있다면 예외를 던집니다.
// (동일한 기본 키)
// realm.copyToRealm(obj);
// 이는 같은 기본 키가 있다면 기존 객체를 갱신하거나
// 기본 키 = 42인 객체가 없다면 새로운 객체를 만듭니다
realm.copyToRealmOrUpdate(obj);
}
});
문자열 (String
)과 박스 정수형 (Byte
, Short
, Integer
, Long
)으로 된 기본 키는 @PrimaryKey
어노테이션과 함께 @Required
어노테이션을 쓰지 않은 한 null
값을 가질 수 있습니다.
객체 꾸미기
RealmObject
를 거의 POJO 처럼 쓸 수 있습니다. RealmObject
를 확장하고 모든 필드를 공개하고 세터와 게터 대신에 대입하기만 하면 됩니다. 이런 모델 클래스의 예입니다.
public class Dog extends RealmObject {
public String name;
public int age;
}
Dog
을 다른 클래스처럼 사용할 수 있습니다. 관리된 Dog
객체를 Realm에서 만드려면 createObject()
나 copyToRealm()
메서드를 쓸 수 있습니다.
realm.executeTransaction(new Realm.Transaction() {
@Overrride
public void execute(Realm realm) {
Dog dog = realm.createObject(Dog.class);
dog.name = "Fido";
dog.age = 5;
}
};
요구사항에 더 맞도록 세터와 게터들에 로직을 추가할 수 있습니다. Realm에 값을 저장하기 전에 검증을 하는 등의 용도로 유용합니다. 또 RealmObject
에 커스텀 메서드도 쉽게 추가할 수 있습니다.
제약사항
현재 final
, transient
, volatile
필드를 지원하지 못합니다. 이것은 Realm에 의해 관리되는 객체와 비관리 객체 간의 불일치를 막기 위해서입니다.
Realm 모델 클래스는 RealmObject
외의 다른 객체의 상속을 허용하지 않습니다. 한번 정의되면 기본 생성자(패러미터가 없는 생성자)는 비어있어야 합니다. 이유는 기본 생성자는 Realm 인스턴스가 주어져 있다고 가정하는 메서드를 호출 하기 때문입니다. 하지만 인스턴스는 생성자가 반환하기 전까지 만들어지지 않습니다. 편의를 위해서라면 다른 생성자를 추가하세요.
RealmModel 인터페이스
RealmObject
기반 클래스를 상속받는 대신 RealmModel
인터페이스를 구현하고 @RealmClass
어노테이션을 추가할 수 있습니다.
@RealmClass
public class User implements RealmModel {
}
RealmObject
의 모든 메서드는 스태틱 메서드를 통해 사용할 수 있습니다.
// RealmObject을 쓸 때
user.isValid();
user.addChangeListener(listener);
// RealmModel을 쓸 때
RealmObject.isValid(user);
RealmObject.addChangeListener(user, listener);
관계
모든 두 RealmObject는 같이 연결할 수 있습니다.
public class Email extends RealmObject {
private String address;
private boolean active;
// ... 세터와 게터들이 위치합니다
}
public class Contact extends RealmObject {
private String name;
private Email email;
// ... 세터와 게터들이 위치합니다
}
Realm에서 관계는 연산에 부담이 없습니다. 관계 연산은 속도 면에서 부담이 없고 관계를 표현하는 내부 구현은 메모리 사용에 상당히 효율적입니다.
다 대 일
RealmObject 서브클래스 타입으로 속성을 선언하세요.
public class Contact extends RealmObject {
private Email email;
// 다른 필드들...
}
각 contact(Contact
인스턴스)는 0 혹은 1 개의 email(Email
인스턴스)을 갖습니다. Realm에서는 여러 contact에서 동일한 email 객체를 사용할 수 있고 이러한 관계가 다-대-일 관계가 될 수 있습니다. 보통 이러한 구현은 일-대-일 관계가 일반적입니다.
RealmObject
필드를 null
로 설정하면 레퍼런스는 정리하지만 다른 객체들은 Realm에서 지워지지 않습니다.
다 대 다
RealmList<T>
필드 선언을 통해서 하나의 객체에서 어떤 수의 객체와 관계를 설정할 수 있습니다. 예로 연락처에 여러 이메일 주소가 있다고 가정해봅시다.
public class Contact extends RealmObject {
public String name;
public RealmList<Email> emails;
}
public class Email extends RealmObject {
public String address;
public boolean active;
}
RealmList
는 기본적으로 RealmObject
들의 컨테이너입니다. RealmList
는 일반적인 자바의 List
와 매우 흡사하게 행동합니다. 다른 RealimList
들에서 같은 객체를 두번 (이상) 쓰는데 제한은 없습니다. 이를 두 개의 일대다 관계로 만들 수도 있고 다대다 관계로 만들 수 있습니다.
객체들을 만들고 Email
객체들을 Contact
객체에 추가하기 위해 RealmList.add()
를 쓸 수 있습니다.
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
Contact contact = realm.createObject(Contact.class);
contact.name = "John Doe";
Email email1 = realm.createObject(Email.class);
email1.address = "john@example.com";
email1.active = true;
contact.emails.add(email1);
Email email2 = realm.createObject(Email.class);
email2.address = "jd@example.com";
email2.active = false;
contact.emails.add(email2);
}
});
특정 데이터 타입을 모델링할 경우엔 재귀적인 관계를 설정할 수 있습니다.
public class Person extends RealmObject {
public String name;
public RealmList<Person> friends;
// 다른 필드들...
}
RealmList
필드에 null
값을 설정하면 리스트가 초기화됩니다. 이것은 리스트가 비는 것(길이가 0)이고요. 어떤 객체도 지워지지 않습니다. RealmList
의 게터는 null
을 절대 반환하지 않습니다. 반환 값은 항상 리스트인데 길이가 0이 될 수 있습니다.
연결 질의
연결 질의나 관계를 사용할 수 있습니다. 아래 모델을 고려해보세요.
public class Person extends RealmObject {
private String id;
private String name;
private RealmList<Dog> dogs;
// 게터와 세터들 ...
}
public class Dog extends RealmObject {
private String id;
private String name;
private String color;
// 게터와 세터들 ...
}
각 Person
객체는 이 테이블 다이어그램에서 보이 듯 여러 개의 관계를 가집니다.
몇몇 사용자들의 연결 질의를 봅시다.
// persons => [U1,U2]
RealmResults<Person> persons = realm.where(Person.class)
.equalTo("dogs.color", "Brown")
.findAll();
먼저 equalsTo
조건의 필드 이름에 ().
으로 분리되는) 관계를 포함한 경로를 사용합니다.
위의 질의는 읽고 ‘Brown’인 개들을 가진 Person들을 전부 찾아야 합니다. 결과가 가진 Dog
객체들이 조건에 맞지 않다는 것을 이해하는 게 중요합니다. 그들은 Person
객체의 부분으로서 온 것입니다.
persons.get(0).getDogs(); // => [A,B]
persons.get(1).getDogs(); // => [B,C,D]
아래의 두 질의를 통해 더 자세히 설명하겠습니다.
// r1 => [U1,U2]
RealmResults<Person> r1 = realm.where(Person.class)
.equalTo("dogs.name", "Fluffy")
.findAll();
// r2 => [U1,U2]
RealmResults<Person> r2 = r1.where()
.equalTo("dogs.color", "Brown")
.findAll();
첫 번째 질의가 어떻게 두 Person
객체를 반환하는 것에 유의합시다. 두 사용자에게 조건이 맞았기 때문입니다. 질의 결과의 각 Person
는 Dog
객체의 목록을 가지고 있고 그들이 가진 전체 개 객체들입니다. (이 개들은 원래 질의 조건에 안 맞을 수 있습니다.) 우리가 개들을 찾는게 아니라 특정 (이름이나 색상의) 개를 가진 사람을 찾는 것을 기억합시다. 두 번째 질의는 첫 번째 Person
질의 결과(r1
)와 각각 Person
에 속해있는 개들을 평가합니다. 두 번째 질의도 각 사용자들이 조건에 맞았는데 이번에는 개들의 색상 때문입니다.
개념을 굳히는데 돕고자 더 깊게 가봅시다. 아래의 예제를 살펴보세요.
// r1 => [U1,U2]
RealmResults<Person> r1 = realm.where(Person.class)
.equalTo("dogs.name", "Fluffy")
.equalTo("dogs.color", "Brown")
.findAll();
// r2 => [U2]
RealmResults<Person> r2 = realm.where(Person.class)
.equalTo("dogs.name", "Fluffy")
.findAll()
.where()
.equalTo("dogs.color", "Brown")
.findAll();
.where()
.equalTo("dogs.color", "Yellow")
.findAll();
첫 번째 질의가 ‘Fluffy’란 이름을 가진 개들을 가진 모든 Person
들을 찾고 Brown
인 모든 개들을 가진 Person
들을 찾아서 교집합을 우리에게 줍니다.
r1
질의가 뒤에서 어떻게 수행되는지 전체적으로 이해해봅시다. 두 조건은 equalTo("dogs.name", "Fluffy")
와 equalTo("dogs.color", "Brown")
입니다. 첫 번째 조건은 U1
과 U2
를 만족시킵니다. 이는 집합 C1
입니다. 두 번째 조건은 U1
과 U2
에 맞습니다. 이는 집합 C2
입니다. 질의에서 논리곱은 두 집합 C1
과 C2
에 대한 교집합과 같습니다. C1
과 C2
의 교집합은 U1
과 U2
입니다. 그래서 r1
은 U1
과 U2
입니다.
r2
의 배후는 조금 다릅니다. 이 질의를 나누어 봅시다. 질의의 첫 번째 부분은 이렇습니다. RealmResults<Person> r2a = realm.where(Person.class).equalTo("dogs.name", "Fluffy").findAll()
이는 U1
과 U2
로 평가됩니다. 그다음으로 r2b = r2a.where().equalTo("dogs.color", "Brown").findAll();
는 역시 U1
과 U2
로 평가됩니다. (두 사용자 모두 브라운 개들을 가집니다.) 마지막 질의로 r2 = r2b.where().equalTo("dogs.color", "Yellow").findAll();
는 단지 U2
만 평가를 합니다. 브라운 개를 결과 집합에 가지면서 노란 개를 가진 사용자는 U2
가 유일하기 때문입니다.
쓰기
읽기 오퍼레이션은 암묵적입니다. 즉, 언제든지 객체를 접근하거나 조회할 수 있습니다. 모든 쓰기 오퍼레이션(추가, 수정, 삭제)은 반드시 쓰기 트랜잭션 내에서 이루어져야 합니다. 쓰기 트랜잭션은 커밋하거나 취소할 수 있습니다. 커밋 도중, 모든 수정사항을 디스크에 쓰고 반영되었다면 커밋은 성공적으로 끝납니다. 쓰기 트랜잭션을 취소한다면 모든 수정사항은 취소됩니다. 쓰기 트랜잭션을 사용해서 데이터를 항상 영구적인 상태로 보존할 수 있습니다.
쓰기 트랜잭션은 쓰레드 안전을 보장하기 위해 사용합니다.
// Realm 인스턴스를 얻습니다
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
//... 객체를 여기에서 추가하거나 갱신합니다 ...
realm.commitTransaction();
쓰기 트랜잭션 내에서 RealmObject를 다루는 작업 동안 변경 사항을 취소하려는 곳에서 영역을 끝내야 합니다. 객체를 커밋하는 대신에 되돌리려면 쓰기 트랜잭션을 간단하게 취소할 수 있습니다.
realm.beginTransaction();
User user = realm.createObject(User.class);
// ...
realm.cancelTransaction();
쓰기 오퍼레이션을 서로를 블로킹하므로 다른 쓰레드에서 쓰기 오퍼레이션이 진행 중이면 해당 쓰레드가 블로킹됩니다. 백그라운드 스레드에서 쓰기 작업 중에 UI 스레드에서 쓰기를 시도하는 것은 ANR 에러를 유발합니다. 이를 막기 위해 UI 스레드에서 쓰기 트랜잭션을 사용할 경우에는 비동기 트랜잭션을 사용합시다.
Realm이 MVCC 아키텍처를 사용한다는 장점 때문에 쓰기 트랜잭션을 열었을 때도 읽기는 막히지 않습니다! 한 번에 여러 스레드에서 동시에 쓸 필요가 없다면 나누어진 많은 쓰기 트랜잭션보다 한 번에 큰 쓰기 트랜잭션을 하는 것을 선호하게 될 겁니다. Realm에 한번 쓰기 트랜잭션을 커밋하면 다른 모든 Realm 인스턴스들은 노티를 받게 되고 암묵적 읽기 트랜잭션은 자동으로 Realm 객체를 갱신하게 됩니다.
Realm의 읽기, 쓰기 접근은 ACID라는 것을 유의하세요.
객체 생성하기
RealmObject
객체들이 Realm과 긴밀하게 연결되어있기 때문에 Realm을 통해 직접적으로 인스턴스가 생성되어야 합니다.
realm.beginTransaction();
User user = realm.createObject(User.class); // 새 객체 만들기
user.setName("John");
user.setEmail("john@corporation.com");
realm.commitTransaction();
대안으로 realm.copyToRealm()를 사용해서 처음에 객체의 인스턴스를 생성하고 나중에 추가할 수 있습니다. Realm은 public no arguments 생성자을 포함해 많은 사용자 정의 생성자를 지원합니다.
User user = new User("John");
user.setEmail("john@corporation.com");
// 객체를 Realm으로 복사하기. 어떤 추가적인 변경도 realmUser에 있을 수 있습니다.
realm.beginTransaction();
User realmUser = realm.copyToRealm(user);
realm.commitTransaction();
realm.copyToRealm()
을 사용할 때 반환되는 객체를 Realm이 관리한다는 점이 중요합니다. 원본 객체애 대한 이후 변경점은 영구적이지 않습니다.
트랜잭션 블록
수동으로 realm.beginTransaction()
, realm.commitTransaction()
, realm.cancelTransaction()
을 관리하는 대신에 자동으로 begin/commit을 관리하고 에러가 발생했을 때 cancel 하도록 지원하는 realm.executeTransaction 메서드를 사용할 수 있습니다.
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
User user = realm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
});
비동기 트랜잭션
트랜잭션이 다른 트랜잭션 들에 의해 블록되기 때문에 UI 스레드를 블록하는 것을 막기 위해 백그라운드 스레드에서 모든 쓰기 작업을 하는 것은 의의가 있습니다. 비동기 트랜잭션을 사용하면 Realm은 트랜잭션을 백그라운드 스레드에서 수행하고 트랜잭션이 끝나면 보고합니다.
realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
User user = bgRealm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
}, new Realm.Transaction.OnSuccess() {
@Override
public void onSuccess() {
// 트랜잭션이 성공하였습니다.
}
}, new Realm.Transaction.OnError() {
@Override
public void onError(Throwable error) {
// 트랜잭션이 실패했고 자동으로 취소되었습니다.
}
});
OnSuccess
와 OnError
콜백은 모두 선택적입니다. 그들이 제공되면 트랜잭션이 성공적으로 완료되거나 실패하면 각자 실행됩니다. 콜백은 Looper
에 의해 제어되고 그들은 Looper 스레드에서만 허용됩니다.
RealmAsyncTask transaction = realm.realm.executeTransactionAsync(new Realm.Transaction() {
@Override
public void execute(Realm bgRealm) {
User user = bgRealm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
}
}, null);
비동기 트랜잭션은 RealmAsyncTask
객체로 표현됩니다. 이 객체는 트랜잭션이 끝나기 전에 액티비티나 프래그먼트를 끝내야 할 때 대기 중인 트랜잭션을 취소하기 위해서도 사용합니다. 트랜잭션을 취소하는 것을 잊으면 콜백이 UI 갱신을 함에 따라 앱이 충돌할 수 있습니다.
public void onStop () {
if (transaction != null && !transaction.isCancelled()) {
transaction.cancel();
}
}
문자열과 바이트 배열 갱신하기
Realm은 전체 필드와 동작하고 문자열이나 바이트 배열의 개별 요소를 갱신하는 게 불가능합니다. 문자열의 다섯 번째 요소를 갱신한다면 아래와 같이 할 수 있습니다.
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
bytes[] bytes = realmObject.binary;
bytes[4] = 'a';
realmObject.binary = bytes;
}
});
Realm의 MVCC 아키텍처가 기존의 데이터를 변경하는 것을 막아 다른 스레드와 프로세스가 안정된 상태의 데이터를 읽기 위함입니다.
질의
(질의와 속성 접근을 포함한) 모든 질의는 바로 처리되지 않습니다. 속성에 접근할 때에만 데이터를 읽습니다.
Realm 질의 엔진은 여러 절(multi-clause)의 질의를 구현하기 위해서 Fluent interface를 사용합니다.
User
클래스를 사용합니다.
public class User extends RealmObject {
@PrimaryKey
private String name;
private int age;
@Ignore
private int sessionId;
// IDE에서 생성된 표준 게터와 세터들...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public int getSessionId() { return sessionId; }
public void setSessionId(int sessionId) { this.sessionId = sessionId; }
}
John이나 Peter라는 이름을 가진 모든 사용자를 질의하기 위해서 다음과 같이합니다.
// Build the query looking at all users:
RealmQuery<User> query = realm.where(User.class);
// 질의 조건을 추가합니다
query.equalTo("name", "John");
query.or().equalTo("name", "Peter");
// 질의를 수행합니다
RealmResults<User> result1 = query.findAll();
// 같은 일들을 한 번에 합니다 ("Fluent interface"):
RealmResults<User> result2 = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAll();
이 명령은 John 혹은 Peter라는 이름을 가진 사용자가 포함된 RealmResults
객체를 반환합니다. 객체는 복사되지 않습니다. 매칭되는 객체 레퍼런스의 리스트를 얻으면 질의 결과에 매칭되는 객체를 직접적으로 다룰 수 있습니다. RealmResults
는 Java의 AbstractList
를 상속받아 유사하게 동작합니다. 예를들어 RealmResults
는 순서가 있는데 개별 객체를 인덱스를 통해 접근할 수 있습니다.
맞는 질의가 없더라도 RealmResults는 null
을 반환하지 않고 size()
메서드가 0을 반환하게 됩니다.
만약 RealmResults의 어느 객체를 수정하거나 삭제하려면 쓰기 트랜잭션을 사용해야 합니다.
조건
이름만으로 설명되는 조건이 있습니다.
between()
,greaterThan()
,lessThan()
,greaterThanOrEqualTo()
,lessThanOrEqualTo()
equalTo()
,notEqualTo()
contains()
,beginsWith()
,endsWith()
isNull()
,isNotNull()
isEmpty()
,isNotEmpty()
모든 조건이 모든 데이터 타입을 지원하진 않습니다. 자세한 정보는 API 레퍼런스를 참고하세요. RealmQuery.
수식어
Case.INSENSITIVE
수식어로 문자열 조건은 A-Z와 a-z 내에서 대소문자를 구별하지 않을 수 있습니다.
논리 연산자
각 조건은 암묵적으로 and 연산이 적용됩니다. or 연산을 위해서는 명시적으로 or()
를 사용하세요.
User 클래스를 사용합니다.
public class User extends RealmObject {
@PrimaryKey
private String name;
private int age;
@Ignore
private int sessionId;
// IDE에서 생성된 표준 게터와 세터들...
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
public int getSessionId() { return sessionId; }
public void setSessionId(int sessionId) { this.sessionId = sessionId; }
}
또한 그룹 조건의 연산 순서를 명시하기 위해서 “괄호”를 사용할 수 있습니다. beginGroup()
은 “왼쪽 괄호”이고 endGroup()
는 “오른쪽 괄호” 역할을 합니다.
RealmResults<User> r = realm.where(User.class)
.greaterThan("age", 10) // 암묵적인 AND
.beginGroup()
.equalTo("name", "Peter")
.or()
.contains("name", "Jo")
.endGroup()
.findAll();
게다가, not()
으로 부정 조건을 만들 수 있습니다. not()
연산자는 하위 조건을 부정하기 위해서는 오직 beginGroup()
/endGroup()
과 함께 사용할 수 있습니다.
정렬
질의를 마쳤을 때 아래와 같이 결과를 정렬할 수 있습니다.
RealmResults<User> result = realm.where(User.class).findAll();
result = result.sort("age"); // 오름차순으로 정렬
result = result.sort("age", Sort.DESCENDING);
연속 질의
결과가 복사되지 않고 요청 시에 바로 연산되기 때문에 데이터를 필터링하기 위해서 효율적으로 연속 질의를 사용하실 수 있습니다.
RealmResults<Person> teenagers = realm.where(Person.class).between("age", 13, 20).findAll();
Person firstJohn = teenagers.where().equalTo("name", "John").findFirst();
연속 질의를 자식 객체들에게도 행할 수 있습니다. 위의 Person
객체가 Dog
개체들의 목록을 가진다고 가정해봅시다.
public class Dog extends RealmObject {
private int age;
// 게터와 세터들 ...
}
public class Person extends RealmObject {
private int age;
private RealmList<Dog> dogs;
// 게터와 세터들 ...
}
1살인 개를 한 마리 이상을 가진 13세와 20세 사이의 모든 사람들을 질의할 수 있습니다.
RealmResults<Person> teensWithPups = realm.where(Person.class).between("age", 13, 20).equalTo("dogs.age", 1).findAll();
연속된 질의가 RealmQuery
가 아닌 RealmResults
에 기반하고 있다는 것을 유의하세요. 더 많은 조건을 기존 RealmQuery
에 추가한다면 이것은 질의를 수정하는 것이고 연속이 아닙니다. 더 자세한 내용은 연결 질의을 참고하세요.
자동 갱신 결과
RealmResults
는 기반 데이터에 의해 라이브로 자동 갱신되는 뷰입니다. 이 것은 결과를 다시 가져올 필요가 없다는 의미입니다. 데이터를 수정하면 질의는 다음 루프 이벤트에 RealmResults
에 반영됩니다.
final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size(); // => 0
realm.executeTransaction(new Realm.Transaction() {
@Override
void public execute(Realm realm) {
Dog dog = realm.createObject(Dog.class);
dog.setName("Fido");
dog.setAge(1);
}
});
puppies.addChangeListener(new RealmChangeListener() {
@Override
public void onChange(RealmResults<Dog> results) {
// results와 puppies는 같은 시간에 값이 올라갑니다
results.size(); // => 1
puppies.size(); // => 1
}
});
이는 RealmResults
전체에 적용됩니다. 모든 객체, 필터처리, 연결된 처리에 사용됩니다.
Realm 객체와 RealmResults
의 이런 특징은 Realm을 더 빠르고 효율적으로 하는 동시에 여러분의 코드를 더 간결하고 반응성 있게 합니다. 예를 들어 액티비티와 프래그먼트가 질의의 결과에 의존하고, 매 접근에 앞서 데이터를 최신으로 갱신할 필요 없이 Realm 객체나 RealmResults
를 필드에 저장하여 쓸 수 있습니다.
Realm 데이터 갱신을 알고 그에 따라 앱의 UI를 변경하기 위해 RealmResults
를 다시 가져오는 것 대신에 Realm 알림을 구독할 수도 있습니다.
결과는 자동 갱신되기 때문에 어떤 고정적으로 유지되는 인덱스나 카운트에 의존하지 않도록 하는 것이 중요합니다.
타입으로부터 객체 가져오기
Realm에서 객체를 가져오는 가장 기본적인 방법은 질의된 모델 클래스의 전체 인스턴스 RealmResults<Foo>
를 반환하는 realm.where(Foo.class).findAll()
입니다.
정렬 기능을 제공하는 특별한 버전의 findAll()
도 있습니다. 예를 들어 필드별로 순서를 정렬할 수도 있습니다. 자세한 내용은 realm.where(Foo.class).findAllSorted()
를 참고하세요.
집합
RealmResults
는 다양한 집합(aggregation) 메서드를 가지고 있습니다.
RealmResults<User> results = realm.where(User.class).findAll();
long sum = results.sum("age").longValue();
long min = results.min("age").longValue();
long max = results.max("age").longValue();
double average = results.average("age");
long matches = results.size();
반복자
RealmResults
의 모든 객체에서 반복자를 사용하기 위해서 Iterable
를 사용할 수 있습니다.
RealmResults<User> results = realm.where(User.class).findAll();
for (User u : results) {
// ... 객체에 대해 뭔가 합시다 ...
}
또는 for
루프를 사용할 수 있습니다.
RealmResults<User> results = realm.where(User.class).findAll();
for (int i = 0; i < results.size(); i++) {
User u = results.get(i);
// ... 객체에 대해 뭔가 합시다 ...
}
RealmResults
는 자동으로 Looper 이벤트 동안 갱신됩니다만 그 사이에 더 이상 질의에 맞지 않거나 삭제된 객체에 대해 접근할 짧은 기회가 있습니다.
final RealmResults<User> users = getUsers();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
users.get(0).deleteFromRealm(); // 비 직접적인 객체 삭제
}
});
for (User user : users) {
showUser(user); // 삭제된 사용자의 충돌이 발생합니다
}
대신에 RealmResults
에 직접적으로 deleteFromRealm()
를 사용하세요.
final RealmResults<User> users = getUsers();
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
users.deleteFromRealm(0); // 직접적으로 객체를 삭제하고 제거합니다
}
});
for (User user : users) {
showUser(user); // 삭제된 사용자는 표시되지 않습니다
}
삭제
Realm에서 질의 결과를 삭제할 수 있습니다.
// 질의의 결과를 얻습니다
final RealmResults<Dog> results = realm.where(Dog.class).findAll();
// 데이터에 대한 모든 변경은 트랜잭션에서 이루어져야 합니다
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
// 맞는 데이터 하나를 삭제합니다
results.deleteFirstFromRealm();
results.deleteLastFromRealm();
// 하나의 객체를 삭제합니다
Dog dog = results.get(5);
dog.deleteFromRealm();
// 전체 맞는 데이터를 삭제합니다
results.deleteAllFromRealm();
}
});
비동기 질의
질의를 백그라운드에서 수행할 수 있습니다.
Realm의 대부분의 질의는 심지어 UI 스레드에서 동기적으로 사용할 수 있을 정도로 충분히 빠릅니다. 하지만 복잡한 질의와 대규모 데이터 집합을 질의 하는 경우, 백그라운드에서 질의하는 것이 나은 경우들이 있습니다. 아래는 비동기 질의를 사용하는 방법을 다룹니다.
예제: “John”과 “Peter” 이름으로 사용자를 찾기
질의 만들기
RealmResults<User> result = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAllAsync();
질의가 블록되지 않고 즉시 RealmResults<User>
을 반환하는 것을 유의하세요. 이것은 표준 자바의 Future와 흡사한 promise 입니다. 이 질의는 백그라운드에서 계속되며 한번 완료되면 반환된 RealmResults
인스턴스를 갱신합니다.
만약 RealmResults
가 갱신이 완료되었을 때 노티를 받기 원한다면 RealmChangeListener
를 등록합니다. 이 리스너는 Realm의 마지막 값이 바뀔 때마다 RealmResults
를 갱신합니다.
콜백 등록하기
private RealmChangeListener callback = new RealmChangeListener() {
@Override
public void onChange() { // 질의과 완료될 때 한번 호출되고 매 업데이트마다 호출됩니다
// use the result
}
};
public void onStart() {
RealmResults<User> result = realm.where(User.class).findAllAsync();
result.addChangeListener(callback);
}
액티비티나 프래그먼트를 나갈 때는 메모리 릭을 막기 위해 리스너들을 해제하는 것을 기억합시다.
public void onStop () {
result.removeChangeListener(callback); // 특정 리스너를 제거합니다
// or
result.removeChangeListeners(); // 전체 등록된 리스너들을 제거한다
}
질의가 끝났는지 확인하기
if (result.isLoaded()) {
// 결과는 지금 사용할 수 있습니다
}
동기적으로 획득한 RealmResults
의 isLoaded() 를 호출하면 항상 true를 반환합니다.
강제로 질의를 동기적으로 로딩하기
선택적으로 질의 완료를 대기할 수 있습니다. 현재 스레드를 블록하고 질의를 다시 동기적으로 합니다. (Future.get()와 같은 컨셉입니다.)
result.load() // 주의하세요. 이 코드는 현재 스레드를 반환될 때까지 멈추게 합니다
비 루퍼 스레드 (Non-Looper threads)
루퍼 스레드에서만 비동기 질의를 사용할 수 있습니다. 비동기 질의는 결과를 일관되게 전달하기 위해 Realm의 Handler를 사용합니다. Looper 없이 스레드 내에서 비동기 질의를 하게 되면 IllegalStateException을 발생시킵니다.
Realms
Realm은 데이터베이스를 의미합니다. 다양한 객체를 가지고 있고 디스크에서 하나의 파일에 대응됩니다.
Realm 설정하기
Realm을 어떻게 생성하는지 모든 측면을 제어하기 위해 RealmConfiguration
객체를 사용합니다. 최소한의 Realm 설정을 위해서는 아래의 것이 사용됩니다.
RealmConfiguration config = new RealmConfiguration.Builder(context).build();
위 설정은 Context.getFilesDir()
에 위치한 default.realm
파일을 지정합니다.
전형적인 설정은 아래의 모습과 같습니다.
// RealmConfiguration은 빌더 패턴에 의해 생성됩니다.
// Realm 파일은 Context.getFilesDir()에 위치하면 이름은 "myrealm.realm"입니다.
RealmConfiguration config = new RealmConfiguration.Builder(context)
.name("myrealm.realm")
.encryptionKey(getKey())
.schemaVersion(42)
.modules(new MySchemaModule())
.migration(new MyMigration())
.build();
// 설정을 사용합니다.
Realm realm = Realm.getInstance(config);
또 여러 개의 RealmConfiguration
을 쓰는 것도 가능합니다. 이런 방법에서는 버전, 스키마, 파일 위치 등을 Realm마다 독립적으로 제어할 수 있습니다.
RealmConfiguration myConfig = new RealmConfiguration.Builder(context)
.name("myrealm.realm").
.schemaVersion(2)
.setModules(new MyCustomSchema())
.build();
RealmConfiguration otherConfig = new RealmConfiguration.Builder(context)
.name("otherrealm.realm")
.schemaVersion(5)
.setModules(new MyOtherSchema())
.build();
Realm myRealm = Realm.getInstance(myConfig);
Realm otherRealm = Realm.getInstance(otherConfig);
Realm의 절대 경로는 항상 Realm.getPath()
을 사용해 절대 경로를 얻을 수 있습니다.
Realm
인스턴스는 스레드 싱글톤이다는 점은 중요한 부분입니다. 매 스레드마다 정적 생성자가 동일한 인스턴스를 반환한다는 것을 의미하기 때문입니다.
기본 RealmConfiguration
RealmConfiguration
를 기본 설정으로 저장할 수 있습니다. 커스텀 Application 클래스에 기본 설정을 넣고 다른 코드에서 사용 가능한지 확인합시다.
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Context.getFilesDir()에 "default.realm"란 이름의 Realm 파일이 위치한다
RealmConfiguration config = new RealmConfiguration.Builder(this).build();
Realm.setDefaultConfiguration(config);
}
}
public class MyActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Realm realm = Realm.getDefaultInstance();
// ... 무언가 합니다 ...
realm.close();
}
}
인 메모리 (In-memory) Realm
비 영속적인 인 메모리 Realm 인스턴스를 정의해봅시다.
RealmConfiguration myConfig = new RealmConfiguration.Builder(context)
.name("myrealm.realm")
.inMemory()
.build();
이렇게 설정하면 디스크에 저장하지 않는 인 메모리 Realm이 생성됩니다. 인 메모리 Realm은 메모리가 부족하면 여전히 디스크를 이용합니다. 하지만 모든 파일들은 메모리 상에서 생성되고 Realm이 닫히면 삭제됩니다.
일반적인 (영속적인) Realm과 같은 이름의 인 메모리 Realm은 허용되지 않는다는 것에 유의해주세요.
개별 이름을 가지는 모든 인 메모리 Realm 인스턴스들은 더 이상 어떤 레퍼런스도 가리키지 않을 때 Realm의 모든 데이터를 해제합니다. 앱의 생애 동안 생성된 인 메모리 Realm은 레퍼런스를 잘 관리해야 합니다.
동적 Realms
전통적인 Realm
을 사용할 때 모델 클래스는 RealmObject
의 서브클래스로 정의하였습니다. 형 안정성에 많은 이점이 있었지만 어떤 경우에는 컴파일 시점에 형이 사용 가능하지 않을 수 있습니다. 마이그레이션 동안이나 CSV 파일과 같은 문자열 기반의 데이터를 다룰 때를 예로 들겠습니다.
DynamicRealm
은 전통적인 Realm
의 변형으로 RealmObject
의 서브클래스 없이 Realm 데이터를 다룰 수 있습니다. 단 모든 접근은 클래스 대신 문자열을 통하게 되죠.
동적 Realm을 열 때의 설정은 전통적인 Realm을 사용할 때와 같습니다. 하지만 동적일 때는 어떤 스키마, 마이그레이션, 스키마 버전은 무시됩니다.
RealmConfiguration realmConfig = new RealmConfiguration.Builder(context).build();
DynamicRealm realm = DynamicRealm.getInstance(realmConfig);
// DynamicRealm에서 모든 객체들은 DynamicRealmObjects입니다
DynamicRealmObject person = realm.createObject("Person");
// 모든 필드는 문자열로 접근가능합니다.
String name = person.getString("name");
int age = person.getInt("age");
// 하부의 스키마는 존재하기에 존재하지 않는 필드에 예외를 발생시킵니다
person.getString("I don't exist");
// 질의는 여전히 평범하게 동작합니다
RealmResults<DynamicRealmObject> persons = realm.where("Person").equalTo("name", "John").findAll();
DynamicRealm
은 안정성과 성능을 유연성과 교환합니다. 그렇기 때문에 정말로 유연성이 필요할 때만 사용하세요.
Realm 객체 닫기
Realm
은 네이티브 메모리 해제와 파일 서술자를 다루기 위해 Closeable
을 구현합니다. Realm을 사용한 후 Realm 객체를 닫는 게 중요합니다.
Realm
인스턴스는 레퍼런스 카운팅 됩니다. 즉, 하나의 쓰레드에서 getInstance()
를 두 번 호출하면 사용이 끝난 후 close()
도 두 번 호출해야 합니다. 어떠한 쓰레드가 실행될지 걱정 없이 Runnable
클래스를 구현하면 됩니다. 간단하게 getInstance()
로 시작해서 close()
로 끝냅니다!
UI 쓰레드를 대상으로 한다면 onDestroy()
메서드에서 realm.close()
을 호출하는 게 가장 간단합니다.
만약 UI 스레드가 아닌 다른 Looper
스레드를 만든다면 이 패턴을 쓸 수 있습니다.
public class MyThread extends Thread {
private Realm realm;
@Override
public void run() {
Looper.prepare();
try {
realm = Realm.getDefaultInstance();
//... Realm 인스턴스를 사용하는 핸들러를 설정합니다 ...
Lopper.loop();
} finally {
if (realm != null) {
realm.close();
}
}
}
}
AsyncTask
를 대상으로 한다면 이러한 패턴이 좋습니다(모든 Closeable
를 의미합니다):
protected Void doInBackground(Void... params) {
Realm realm = null;
try {
realm = Realm.getDefaultInstance();
// ... Realm 인스턴스를 사용합니다 ...
} finally {
if (realm != null) {
realm.close();
}
}
return null;
}
Thread
나 Runnable
를 짧은 생애 작업에 사용한다면 다음의 패턴을 추천합니다.
// Realm 인스턴스와 비 Looper 스레드를 실행합니다.
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Realm realm = null;
try {
realm = Realm.getDefaultInstance();
// ... Realm 인스턴스를 사용합니다 ...
} finally {
if (realm != null) {
realm.close();
}
}
}
});
thread.start();
minSdkVersion >= 19
이상이며 Java >= 7
인 앱에서 동작하길 원하면 try-with-resources를 사용할 수 있습니다.
try (Realm realm = Realm.getDefaultInstance()) {
// 수동으로 Realm 인스턴스를 닫을 필요가 없습니다
}
자동 리프레쉬
Looper (기본적으로 UI 쓰레드)와 관련된 쓰레드에서 Realm 인스턴스를 선언했다면 해당 Realm 인스턴스는 자동 리프레쉬 기능을 지원합니다. 즉, 이벤트 루프 내의 모든 일에서 최신 버전으로 자동 수정된다는 의미입니다. 매우 적은 노력으로 항상 최신 콘텐츠로 UI를 표시할 수 있는 편리한 기능입니다.
Looper
를 사용하지 않는 쓰레드에서 Realm 인스턴스를 생성했다면 그런 인스턴스들의 객체는 waitForChange()
를 호출하기 전까지 자동 리프레쉬되지 않습니다. 오랜 버전의 데이터를 유지하는 건 많은 메모리 사용과 디스크 공간을 필요로 하고 데이터 버전이 증가할수록 더 필요로 합니다. 그래서 해당 쓰레드에서 Realm 인스턴스를 사용한 후 닫아주는 게 중요합니다.
Realm 객체의 자동 리프레쉬 기능이 활성화되어있는지 확인하려면 isAutoRefresh()
메서드를 사용할 수 있습니다.
Realm 파일 찾기
App에서 사용하고 있는 Realm 파일을 찾고 싶으시다면 이 StackOverflow 답변 에서 자세한 방법을 확인하실 수 있습니다.
스레드
Realm을 다양한 스레드에서 Realm을 사용하기 위해서 복잡한 설명이 필요하지 않으며, 사용하는 것도 간단합니다. 여기에서 알아야 할 가장 핵심은 Realm은 여러 스레드에서 데이터를 적은 노력으로 다룰 수 있게 되어 있다는 부분입니다. 객체와 질의가 언제나 자동 업데이트 되기 때문에 불일치나 성능에 대해 걱정할 필요가 없습니다.
다른 스레드에서 라이브 오브젝트를 다룰 수 있고 그들을 읽고 쓸 수 있습니다. 데이터를 바꾸어야 한다면 트랜잭션을 쓸 수 있습니다. 다른 스레드의 다른 객체들은 거의 실시간에 갱신됩니다. (이 말은 현재 다른 프로세스에서 무언가를 하고 있다면 런룹의 다음 차례에 갱신된다는 의미입니다.)
단 하나의 제약은 스레드 간에 Realm 객체를 자유롭게 전달 할 수 없다는 것입니다. 만약 다른 스레드에서 같은 데이터를 원한다면 단지 다른 스레드에서 해당 데이터에 권한 질의를 하면 됩니다. Realm 반응형 아키텍처를 이용해 변경을 볼 수도 있습니다. 스레드 간 모든 데이터는 최신으로 유지되고요. Realm은 데이터가 갱신될 때 알려줍니다
아래의 예제를 살펴보세요.
Realm 스레드 예제
고객 목록을 출력하는 앱을 가정해봅시다. 백그라운드 스레드(안드로이드 인텐트 서비스)이 리모트 백 단에서 새 고객을 가져오고 Realm에 저장합니다. 백그라운드 스래드에서 새 고객을 추가하면 UI 스레드는 데이터를 자동 갱신합니다. RealmChangeListener
는 UI 위젯 자체가 갱신되어야 할 시점에 UI 스레드에 변경을 알립니다. Realm은 항상 최신으로 갱신하기 때문에 재질의할 필요가 없습니다.
// 프래그먼트나 액티비티 등에서
@Override
public void onActivityCreated(Bundle savedInstanceState) {
// ... 간결함을 위해 여러 코드를 생략합니다
realm = Realm.getDefaultInstance();
// 전체 고객을 가져옵니다
RealmResults<Customer> customers = realm.where(Customer.class).findAllAsync();
// ... 리스트 어댑터를 만들고 이것을 ListView, RecyclerView 등에 추가합니다
// Realm 변경 리스너를 설정합니다
changeListener = new RealmChangeListener() {
@Override
public void onChange(RealmResults<Customer> results) {
// 어떤 스레드에서 Realm 데이터베이스가 변경되면 이 메서드가 실행됩니다.
// 변경 리스너는 오로지 루퍼 스레드에서만 돈다는 것을 주의하세요.
// 비 루퍼 스레드에는 대신에 Realm.waitForChange()를 사용해야 합니다.
listAdapter.notifyDataSetChanged(); // UI를 갱신합니다.
}
};
// Realm이 고객 결과가 변경할 때마다 우리의 리스너에게 통보하도록합니다.
// (항목 추가, 삭제, 갱신, 어떤 종류의 정렬 등)
customers.addChangeListener(changeListener);
}
// 백그라운드 서비스 안의 다른 스레드
public class PollingService extends IntentService {
@Override
public void onHandleIntent(Intent intent) {
Realm realm = Realm.getDefaultInstance();
// 네트워크 호출이나 기타 등등을 해서 어떤 데이터를 가져와서 'json' 변수에 채웁니다
String json = customerApi.getCustomers();
realm.beginTransaction();
realm.createObjectFromJson(Customer.class, json); // 새로운 객체들을 저장합니다
realm.commitTransaction();
// 지금 시점에서, UI 스레드의 데이터는 이미 최신입니다.
// ...
}
// ...
}
한번 백그라운드 서비스가 새 고객들을 UI에 넣으면 여러 분이 어떤 추가적인 중재 없이 UI의 customers
목록은 자동으로 갱신됩니다. 개별 객체들도 동일합니다. 단지 하나의 객체만 관리한다고 가정해봅시다. 한 스레드에서 바뀌면 UI 스레드는 자동으로 새로운 데이터를 갖습니다. 만약에 변경에 대해 응답받고 싶다면 그냥 위에 보이듯 리스너를 추가하면 됩니다.
이게 여러분이 해야 할 전부입니다.
여러 쓰레드에서 Realm 사용하기
여러 쓰레드에서 Realm을 사용하기 위한 유일한 규칙은 Realm, RealmObject, RealmResults 인스턴스가 쓰레드 간에 전달될 수 없다는 점입니다. 하지만 비동기 질의와 비동기 트랜잭션을 이용해서 작업을 백그라운드에 보내고 작업 결과를 원 스레드로 가져올 수 있습니다.
동일한 데이터를 여러 쓰레드에서 접근하고 싶다면 새로운 Realm 인스턴스를 생성해야 하고(예. Realm.getInstance(RealmConfiguration config)
나 기타 설정 방법) 질의를 통해 객체에 접근해야 합니다.
객체는 디스크에서 동일한 데이터에 대응되고 어느 쓰레드에서든지 읽고 쓰기가 가능합니다.
스키마
Realm의 기본 스키마는 프로젝트 안의 모든 Realm 모델 클래스를 포함하도록 되어있습니다. 이 것은 변경이 가능합니다. 예를 들어, Realm이 클래스의 일부분만을 포함하도록 할 수 있습니다. 별도의 RealmModule을 생성하여 이 기능을 구현하실 수 있습니다.
// module 생성
@RealmModule(classes = { Person.class, Dog.class })
public class MyModule {
}
// RealmConfiguration 에서 모듈에 대한 설정을 함으로써 선택한 클래스만 허용하도록 할 수 있습니다
RealmConfiguration config = new RealmConfiguration.Builder(context)
.modules(new MyModule())
.build();
// 여러 개의 모델을 하나의 스키마로 합치는 것이 가능합니다
RealmConfiguration config = new RealmConfiguration.Builder(context)
.modules(new MyModule(), new MyOtherModule())
.build();
스키마 공유
라이브러리 개발자 분들께: Realm 을 포함하는 라이브러리는 반드시 그 스키마를 RealmModule을 통해서 노출해야 합니다.
그렇게 함으로써 기본 RealmModule
이 라이브러리 프로젝트용으로 생성되는 것을 방지하여, app의 고유 RealmModule
과 충돌이 나는 것을 방지할 수 있습니다. 라이브러리의 RealmModule
은 또한 라이브러리가 자신의 Realm class를 앱에 노출하는 방법이기도 합니다.
// 라이브러리는 모듈을 생성하고서 library = true 로 설정해야 합니다. 이 설정은 기본 모듈이 생성되는 것을 막아줍니다.
// 라이브러리의 모든 클래스 리스트를 노출하는 대신 라이브러리의 모든 클래스를 위해 allClasses = true를 사용하실 수 있습니다.
@RealmModule(library = true, allClasses = true)
public class MyLibraryModule {
}
// 따라서 라이브러리 프로젝트는 자신만을 위한 모듈을 명시적으로 설정해야 합니다.
RealmConfiguration libraryConfig = new RealmConfiguration.Builder(context)
.name("library.realm")
.modules(new MyLibraryModule())
.build();
// 앱은 라이브러리 RealmModule을 자신의 스키마에 추가할 수 있습니다.
RealmConfiguration config = new RealmConfiguration.Builder(context)
.name("app.realm")
.modules(Realm.getDefaultModule(), new MyLibraryModule())
.build();
RealmModule
이 어떻게 라이브러리와 app 프로젝트에서 동작하는지를 여기에 있는 예제를 통해서 확인해 보세요.
현재는 하나의 파일이 여러 RealmModule
을 가지는 게 불가능합니다. 만약 둘 이상의 RealmModule
가 있다면 정확히 파일당 하나의 정의로 여러 파일로 분리하세요.
JSON
문자열, JSONObject InputStream으로 표현된 JSON을 Realm에 직접 RealmObject로 추가하는 것이 가능합니다. Realm은 RealmObject에 정의되지 않는 JSON 프로퍼티들은 무시합니다. Realm.createObjectFromJson()을 이용해서 단일 객체를 추가할 수 있고 Realm.createAllFromJson()를 이용하여 객체들의 리스트를 추가할 수 있습니다.
// A RealmObject that represents a city
public class City extends RealmObject {
private String city;
private int id;
// 게터와 세터들이 있습니다 ...
}
// 문자열을 삽입합니다
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.createObjectFromJson(City.class, "{ city: \"Copenhagen\", id: 1 }");
}
});
// InputStream을 이용하여 여러 아이템을 삽입합니다
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
try {
InputStream is = new FileInputStream(new File("path_to_file"));
realm.createAllFromJson(City.class, is);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
});
Realm에서 JSON 파싱은 다음의 규칙을 따릅니다.
- null 값을 필드로 가진 JSON으로 객체를 만듭니다.
- 널이 허용된 필드는 기본 값으로
null
을 설정합니다. - 널이 허용되지 않은 필드는 예외를 던집니다.
- 널이 허용된 필드는 기본 값으로
null
값의 필드를 가진 JSON으로 갱신합니다.- 널이 허용된 않는 필드는
null
로 설정합니다. - 널이 허용되지 않은 필드는 예외를 던집니다.
- 널이 허용된 않는 필드는
- JSON이 필드가 없습니다.
- 널이 허용된 필드와 허용되지 않은 필드의 값 모두 그대로 둡니다.
노티피케이션
Change listeners는 루퍼 스레드에서만 동작합니다. 루퍼 스레드 밖에서는 Realm.waitForChange()
를 대신 사용하세요.
백그라운드 스레드에서 데이터를 Realm에 추가하면 UI나 다른 스레드는 Realm에 추가된 리스너에 의해 변경점을 알림 받습니다. 이는 Realm이 (다른 스레드나 프로세스에 의해) 변경될 때마다 이루어집니다.
public class MyActivity extends Activity {
private Realm realm;
private RealmChangeListener realmListener;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
realmListener = new RealmChangeListener() {
@Override
public void onChange(Realm realm) {
// ... 업데이트를 위해 무언가 합니다 (UI, etc.) ...
}};
realm.addChangeListener(realmListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 리스너를 제거합니다.
realm.removeChangeListener(realmListener);
// Realm 인스턴스를 닫습니다.
realm.close();
}
}
전체 리스너를 닫을 필요가 있다면 쉽게 제거할 수 있습니다.
realm.removeAllChangeListeners();
리스너는 Realm에 제한되지 않습니다. 리스너는 RealmObject
에 연결될 수 있고 RealmResults
에 연결될 수도 있습니다. 이를 통해 객체나 질의 결과의 변화에 반응할 수 있습니다. 더 나아가 오브젝트나 결과가 변화될때 변경 리스너가 자동으로 호출됩니다. 객체를 갱신할 필요가 없습니다.
public class MyActivity extends Activity {
private Realm realm;
private RealmChangeListener puppiesListener;
private RealmChangeListener dogListener;
private RealmResults<Dog> puppies;
private Dog dog;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
puppiesListener = new RealmChangeListener() {
@Override
public void onChange(RealmResults<Dog> puppies) {
// ... puppies 인스턴스와 작업합니다.
}};
// Find all the puppies
puppies = realm.where(Dog.class).lessThanOrEqualTo("age", 2).findAll();
puppies.addChangeListener(puppiesListener);
dogListener = new RealmChangeListener() {
@Override
public void onChange(Dog dog) {
// ... Dog 인스턴스를 업데이트하며 작업합니다.
}};
dog = realm.where(Dog.class).equalTo("name", "Fido").findFirst();
dog.addChangeListener(dogListener);
}
@Override
protected void onDestroy() {
super.onDestroy();
// 리스너들을 제거합니다.
puppies.removeChangeListener(puppiesListener);
dog.removeChangeListener(dogListener);
// Realm 인스턴스를 닫습니다.
realm.close();
}
}
참조 형이 변환되었을 때 마지막으로 형 기반의 변경 리스너가 통보를 받습니다. 아래의 예제를 보세요.
Person person = realm.where(Person.class).findFirst();
person.getDogs(); // => 2 - 목록에 두마리 개가 있다고 가정합니다
person.addChangeListener(new RealmChangeListener() {
@Override
public void onChange(Person person) {
// 사람 인스턴스의 변화에 반응합니다.
// 어떤 참조된 개가 갱신되어도 반응합니다.
}
});
Dog dog = person.getDogs().get(0);
realm.beginTransaction();
dog.setAge(5);
realm.commitTransaction();
// 참조된 강아지가 변경되었기 때문에
// 사람 변경 리스너는 런 루프의 다음 단계에서 호출됩니다.
마이그레이션
어떤 데이터베이스와 작업하든 모델 클래스(예를 들어 데이터베이스 스키마)는 항상 변경됩니다. Realm의 모델 클래스가 표준 객체로 정의되어 있어 RealmObject의 서브클래스에 상응하는 인터페이스를 변경하는 것은 쉽습니다.
예전 데이터베이스 스키마가 디스크에 저장한 데이터가 없다면 단지 정의를 바꾸는 것은 괜찮습니다. 이렇게 할 때 Realm은 코드에 정의된 코드와 디스크에 보이는 데이터 Realm을 비교하여 불일치가 있다면 예외를 던지게 됩니다. 이는 RealmConfiguration
에 마이그레이션을 코드와 스키마 버전을 설정함으로써 해결할 수 있습니다.
RealmConfiguration config = new RealmConfiguration.Builder(context)
.schemaVersion(2) // 스키마가 바뀌면 값을 올려야만 합니다
.migration(new MyMigration()) // 예외 발생대신에 마이그레이션을 수행하기
.build()
이를 이용하면 마이그레이션 코드는 필요시 자동으로 수행됩니다. 우리는 디스크의 스키마와 이전 버전 스키마를 위해 저장된 데이터를 갱신하는 여러 내장 메서드를 제공합니다.
// 새 클래스를 추가하는 마이그레이션 예제
RealmMigration migration = new RealmMigration() {
@Override
public void migrate(DynamicRealm realm, long oldVersion, long newVersion) {
// DynamicRealm는 편집 가능한 스키마를 노출합니다
RealmSchema schema = realm.getSchema();
// 버전 1로 마이그레이션: 클래스를 생성합니다
if (oldVersion == 0) {
schema.create("Person")
.addField("name", String.class)
.addField("age", int.class);
oldVersion++;
}
// 버전 2로 마이그레이션: 기본 키를 넣고 객체를 참조합니다
if (oldVersion == 1) {
schema.get("Person")
.addField("id", long.class, FieldAttribute.PRIMARY_KEY)
.addRealmObjectField("favoriteDog", schema.get("Dog"))
.addRealmListField("dogs", schema.get("Dog"));
oldVersion++;
}
}
}
자세한 내용은 마이그레이션 샘플 앱에서 확인하세요.
Realm을 실행할 때 파일이 없다면 마이그레이션은 필요하지 않습니다. Realm은 그냥 새로운 .realm
파일과 코드에 정의된 최신 모델에 기반한 스키마를 만듭니다. 이는 한참 개발중이고 스키마를 자주 바꾸고 전체 데이터를 날려도 된다면 마이그레이션을 작성하는 대신 (포함된 전체 데이터셋이 담긴) .realm
파일을 디스크에서 지울 수 있다는 것입니다. 앱 개발 주기의 초기에서 모델을 덕지덕지 고치며 작업해보는데 도움이 됩니다.
RealmConfiguration config = new RealmConfiguration.Builder(context)
.deleteRealmIfMigrationNeeded()
.build()
암호화
미국으로부터의 수출 제한이나 금수 조치가 있는 국가에 거주하는 경우 Realm 사용에 대한 제한이 있을 수 있으므로, 저희 라이센스의 수출 규정 준수 조항을 참고하십시오.
512 비트(64바이트) 암호화 키를 RealmConfiguration.Builder.encryptionKey()
에 전달하면 Realm 파일을 암호화할 수 있습니다.
RealmConfiguration config = new RealmConfiguration.Builder(context)
.encryptionKey(getKey())
.build();
Realm realm = Realm.getInstance(config);
이런 방법을 통해 디스크에 저장되는 데이터가 표준 AES-256 암호화를 통해 투명한 방법으로 암호화, 복호화 할 수 있습니다. 파일을 생성할 때 같은 암호화 키를 매번 Realm 인스턴스에 제공해야 합니다.
다른 애플리케이션이 볼 수 없는 안드로이드 키 스토어에 키를 저장하는 완전한 예제를 examples/encryptionExample에서 볼 수 있습니다.
안드로이드와 함께
독립적으로 Realm은 안드로이드와 이음새 없이 잘 동작합니다. RealmObject
객체들이 스레드 제약이라는 점은 염두해야 합니다. Realm 오브젝트를 액티비티 간이나 백그라운드 서비스, 브래드 캐스트 리시버 등으로 전달 할 때 이런 부분을 이해하는 것이 매우 중요합니다.
어댑터들
Realm은 OrderedRealmCollection
(RealmResults
와 RealmList
둘 다 이 인터페이스를 구현하고 있습니다)에서 온 데이터를 UI 위젯에 바인딩하는 것을 돕기 위해 추상 유틸리티 클래스들을 제공합니다. RealmBaseAdapter 클래스는 getView()
메서드를 구현하기 위해 필요한 모든 로직을 다루고 있습니다.
RealmBaseAdapter
은ListView
와 함께 사용하세요. 예제는 여기에서 보세요.RealmRecyclerViewAdapter
는RecyclerView
와 함께 사용하세요. 예제는 여기에서 보세요.
RealmBaseAdapter
을 사용하기 위해 어플레케이션 수준 build.gradle
에 추가적인 의존성을 넣어야 한다.
dependencies {
compile 'io.realm:android-adapters:1.2.2'
}
인텐트 서비스
ChangeListener
는 IntentService
내에서 동작하지 않습니다. 비록 Looper
스레드 안에 있어도 매 onHandleIntent()
호출은 독립적인 이벤트이고 “반복”이 아닙니다. 변경 리스너를 등록하는게 불가능하고 그들이 절대 호출되지 않을것이라는 의미입니다.
RealmObject를 직접 전달 할 수 없기 때문에 우리는 사용하는 객체의 식별자를 전달하는 것을 추천합니다. 기본적인 예제는 객체가 기본 키를 가지는 것입니다. 기본 키 값을 인텐트의 엑스트라 번들을 통해 전달합시다.
// @PrimaryKey 'id' 필드를 가진 person객체를 가정합시다...
Intent intent = new Intent(getActivity(), ReceivingService.class);
intent.putExtra("person_id", person.getId());
getActivity().startService(intent);
(액티비티, 서비스, 인텐트 서비스, 브로드캐스트리시버 등의) 수신 컴포넌트에 번들로 전달된 기본 키 값을 받고 수신 컴포넌트에서 Realm을 열고 RealmObject
를 질의합시다.
// onCreate(), onHandleIntent() 등에서...
String personId = intent.getStringExtra("person_id");
Realm realm = Realm.getDefaultInstance();
Person person = realm.where(Person.class).equalTo("id", personId).findFirst();
// 사람에 대해 어떤 작업을 합시다 ...
realm.close();
동작하는 전체 예제는 스레드 예제의 Object Passing
부분에서 찾을 수 있습니다. 예제는 어떻게 id를 전달하고 RealmObject
를 받는지 안드로이드의 기본 사용례로 보여줍니다.
안드로이드 프레임워크 스레드를 다루는 전략
이 클래스들을 사용할 때는 조심하세요.
AsyncTask
클래스는 백그라운드 스레드를 수행하는 doInBackground()
메서드를 포함합니다. IntentSerive
클래스는 워커 스레드를 수행하는 onHandleIntent(Intent intent)
메서드를 가집니다.
이 둘에서 Realm을 쓰기를 원한다면 각각의 메서드에서 Realm을 열고 작업을 수행하고 끝나기 전에 Realm을 닫아야 합니다. 아래 두 예제가 있습니다.
AsyncTask
아래와 같이 doInBackground()
메서드에서 Realm을 열고 닫습니다.
private class DownloadOrders extends AsyncTask<Void, Void, Long> {
protected Long doInBackground(Void... voids) {
// 이제 백그라운드 스레드입니다.
// Realm을 엽니다.
Realm realm = Realm.getDefaultInstance();
// Realm을 사용합니다.
realm.createAllFromJson(Order.class, api.getNewOrders());
Order firstOrder = realm.where(Order.class).findFirst();
long orderId = firstOrder.getId(); // order의 id
realm.close();
return orderId;
}
protected void onPostExecute(Long orderId) {
// 안드로이드 메인스레드로 돌아왔습니다.
// orderId를 가지고 order에 대해 Realm에 질의하거나
// 어떤 연산을 수행합시다.
}
}
IntentService
아래와 같이 onHandleIntent()
메서드에서 Realm을 열고 닫습니다.
public class OrdersIntentService extends IntentService {
public OrdersIntentService(String name) {
super("OrdersIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
// 이제 백그라운드 스레드입니다.
// Realm을 엽시다.
Realm realm = Realm.getDefaultInstance();
// Realm을 사용합니다.
realm.createAllFromJson(Order.class, api.getNewOrders());
Order firstOrder = realm.where(Order.class).findFirst();
long orderId = firstOrder.getId(); // order의 아이디
realm.close();
}
}
다른 라이브러리
이 섹션은 Realm과 안드로이드의 다른 일반적인 라이브러리를 통합하는 법을 설명합니다.
GSON
GSON은 구글이 만든 JSON 비 직렬화, 직렬화 도구입니다. (최신 버전인) GSON 2.3.1을 함께 쓰려면 ExclusionStrategy를 지정해야 합니다.
// Using the User class
public class User extends RealmObject {
private String name;
private String email;
// getters and setters left out ...
}
Gson gson = new GsonBuilder().create();
String json = "{ name : 'John', email : 'john@corporation.com' }";
User user = gson.fromJson(json, User.class);
우리의 GridViewExample에서 어떻게 Realm과 GSON이 상호 작용하는지 예를 보세요.
직렬화
Retrofit과 같은 라이브러리와 잘 호환하기 위해서 객체가 직렬화, 비 직렬화가 되길 원할 수 있습니다. GSON이 게터와 세터 대신에 필드 값을 사용하기 때문에 Realm 객체를 JSON으로 직렬화하는 것이 잘되지 않습니다.
각 객체마다 직렬화가 되도록 하기 위한 커스텀 JsonSerializer를 작성하고 TypeAdapter에 등록해야 GSON 직렬화가 작동합니다.
이 Gist에서 어떻게 해야 하는지를 보여주고 있습니다.
기본형 리스트
어떤 JSON API들은 정수, 문자열 등의 기본형 목록을 반환하는데 아직 Realm이 지원하지 못 하는 이슈 입니다. JSON API를 변경하는 것이 불가능하다면 JSON의 기본형을 Realm의 래퍼 객체로 변환하는 GSON용 TypeAdapter를 만들 수 있습니다.
이 Gist는 정수형들에 대한 래퍼 객체를 사용하는 것을 보여주는데 이런 방법으로 Realm이 지원하는 모든 기본형 데이터 타입 배열에 적용할 수 있습니다.
문제 해결
Realm 객체는 내부적으로 순환 참조를 가질 수 있습니다. 이런 순환 참조는 GSON이 StackOverflowError
를 발생하게 할 수 있습니다. Drawable
필드를 가진 Realm 객체에서도 이런 일을 볼 수 있습니다.
public class Person extends RealmObject {
@Ignore
Drawable avatar;
// 다른 필드와 기타.
}
위의 Person
클래스는 안드로이드 Drawable
을 가지고 있고 @Ignore
을 적용하고 있습니다. GSON의 역직렬화 과정에서 Drawable을 탐색하고 StackOverflowError(GitHub 이슈)를 발생시킵니다. 이를 완화하기 위해 shouldSkipField
에 아래 코드를 추가합니다.
public boolean shouldSkipField(FieldAttributes f) {
return f.getDeclaringClass().equals(RealmObject.class) || f.getDeclaringClass().equals(Drawable.class);
}
Drawable.class
의 평가를 보세요. 이 코드는 역직렬화 과정에서 이 필드를 건너 넘어가게 합니다. 이런 코드를 추가하면 StackOverflowError
를 완화할 수 있습니다.
Jackson-databind
Jackson-databind는 JSON 데이터와 자바 클래스를 연결하는 라이브러리입니다.
Jackson은 데이터 바인딩을 위해 리플렉션을 사용합니다. 이는 Realm의 RxJava 지원과 충돌하고 RxJava는 클래스로더를 사용할 수 없습니다. 이는 다음과 같은 예외를 일으킵니다.
java.lang.NoClassDefFoundError: rx.Observable
at libcore.reflect.InternalNames.getClass(InternalNames.java:55)
...
이는 RxJava를 프로젝트에 추가하거나 아래와 같은 형태의 비어있는 더미 파일을 생성하여 고칠 수 있습니다.
package rx;
public class Observable {
// RxJava가 프로젝트 의존성에 없다면
// Jackson-Databind 지원을 위해 더미 클래스가 필요합니다.
}
이 이슈는 Jackson 프로젝트의 이슈에 보고되어 있습니다.
코틀린
Realm은 코틀린 프로그래밍 언어를 완전히 지원합니다. 하지만 몇 가지 알아야 할 부분이 있습니다.
- 모델 클래스를 open으로 해야 하는 것이 중요합니다.
- 현재 코틀린 어노테이이션 프로세서의 제약 때문에 어떤 경우에
@RealmClass
어노테이션을 추가해야 합니다. - 많은 Realm API가 Java 클래스를 참조하기 때문에 이런 이유로 컴파일 의존성에
org.jetbrains.kotlin:kotlin-reflect:${kotlin_version}
를 추가해야 합니다.
Realm과 코틀린으로 동작하는 앱은 여기 예제를 보세요.
Parceler
Parceler는 Parcelable 인터페이스를 사용하는 객체를 만들기 위해 필요한 보일러플레이트를 자동으로 만드는 라이브러리입니다. Realm에서 프록시 클래스를 사용하기 때문에 Parceler에서 Realm 모델을 쓰기 위해 아래 설정이 필요합니다.
// 모든 클래스는 짝이맞는 RealmProxy 클래스가 어노테이션 프로세서에 의해 생성될 수 있도록 RealmObject을
// 확장해야합니다. Parceller는 이 점을 알고 만들어져야 합니다. 프로젝트가 최소 한번 컴파일되기 전까지는
// 클래스는 사용 가능하지 않다는 점을 주의합시다.
@Parcel(implementations = { PersonRealmProxy.class },
value = Parcel.Serialization.BEAN,
analyze = { Person.class })
public class Person extends RealmObject {
// ...
}
만약 Parceler를 쓰기 위해 그래들을 쓴다면 아래 라인을 확인하세요. (자세한 내용은 여기를 참고하세요.)
compile "org.parceler:parceler-api:1.0.3"
apt "org.parceler:parceler:1.0.3"
Parceler를 쓸 때 알아야 할 몇 가지 중요한 제약들이 있습니다.
- 모델 클래스는
RealmList
를 가지고 있다면 특별 어뎁터를 등록해야합니다. - 객체가 싸여지면 이것은 Realm에서 분리되고 그 시점에 데이터의 스냅션을 담은 독립 객체처럼 행동하게 됩니다. 또 이 객체에 대한 변경은 Realm의 영속화되지 않습니다.
Retrofit
Retrofit은 Square가 만든 라이브러리로 형 안정한 방식으로 REST API를 쉽게 사용할 수 있는 도구입니다.
Realm은 Retrofit 1.이나 2.과 특별히 잘 동작합니다. Retrofit은 자동으로 데이터를 Realm에 넣지는 않습니다. 대신 realm.copyToRealm()
이나 realm.copyToRealmOrUpdate()
메서드를 사용해야 추가하는 것을 유의합시다.
GitHubService service = restAdapter.create(GitHubService.class);
List<Repo> repos = service.listRepos("octocat");
// 객체를 영속화하기 위해 Retrofit으로부터 Realm으로 복사합니다.
realm.beginTransaction();
List<Repo> realmRepos = realm.copyToRealmOrUpdate(repos);
realm.commitTransaction();
Robolectric
Robolectric은 JUnuit 테스트를 폰이나 에뮬레이터 대신 JVM에서 직접 수행하게 해주는 라이브러리입니다. 현재 Roboletric은 번들된 Realm과 같은 네이티브 라이브러리를 지원하지 못합니다. 지금 상황에서는 Realm을 테스트하기 위해 Roboletric을 사용할 수 없다는 의미입니다.
기능 요청에서 진행상황을 확인할 수 있습니다. https://github.com/robolectric/robolectric/issues/1389
RxJava
RxJava는 넷플릭스가 옵저버 패턴를 확장하여 만든 리엑티브 확장입니다. 데이터를 구성가능한 순열로서 관찰할 수 있게 합니다.
Realm은 RxJava를 일급 클래스로 지원합니다. 이는 다음 Realm 클래스들의 Observable
을 노출한다는 것입니다. Realm
, RealmResults
, RealmObject
, DynamicRealm
, DynamicRealmObject
.
// Realm, Retrofit, RxJava를 결합합니다. (간결성을 위해 RetroLambda 문장을 사용합니다.)
// 전체 사람을 불러와서 그들의 Github 최신 상태가 있다면 병합합니다.
Realm realm = Realm.getDefaultInstance();
GitHubService api = retrofit.create(GitHubService.class);
realm.where(Person.class).isNotNull("username").findAllAsync().asObservable()
.filter(persons.isLoaded)
.flatMap(persons -> Observable.from(persons))
.flatMap(person -> api.user(person.getGithubUserName())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(user -> showUser(user));
비동기 질의가 블록하지 않으며 위의 코드는 즉시 RealmResults 인스턴스를 반환한다는 것을 주목하세요. 확실히 읽어온 리스트를 다루기 위해 필터 오퍼레이터를 사용하고 RealmResults<E>.isLoaded()
메서드를 호출하여 리스트를 검사하여 Observable을 필터링합니다. RealmResult가 로딩되었느냐를 확인하면 질의의 완료 여부를 판단할 수 있습니다.
더 자세한 내용은 RxJava sample project을 참고하세요.
설정
RXJava는 선택적인 의존성을 가지고 있고 Realm은 자동으로 이를 포함하지 않습니다. 이는 RxJava의 어떤 버전을 사용할 것인지 선택할 수 있고 RxJava를 사용하지 않는 프로젝트에 메서드 개수를 늘리는 문제를 막을 수 있는 이점이 있습니다. RxJava는 build.gradle
에 직접 추가할 수 있습니다.
dependencies {
compile 'io.reactivex:rxjava:1.1.0'
}
사용자 RxObservableFactory
를 만들어 Realm이 Obserbavle들을 어떻게 생성할지 설정할 수 있습니다. 이는 RealmConfiguration
에서 설정합니다.
``java RealmConfiguration config = new RealmConfiguration.Builder(context) .rxFactory(new MyRxFactory()) .build()
`RxObservableFactory`가 정의되지 않은 경우 Realm은 기본으로 [`RealmObservableFactory`](api/io/realm/rx/RxObservableFactory.html)를 사용합니다. Realm이 제공하는 RxJava <= 1.1.* 버전을 지원하는 클래스입니다.
<span id="debugging"></span>
## 테스팅과 디버깅
Realm에서 JUnit3, JUnit4, Robolectric, Mockito, PowerMock을 어떻게 연동하는지에 대한 정보는 [unitTestExample](https://github.com/realm/realm-java/tree/master/examples/unitTestExample)에서 확인하세요.
### 안드로이드 스튜디오 디버깅
안드로이드 스튜디오나 인텔리제이에서 사용할 때 알아두어야 할 것들이 있습니다. 디버거는 사용하는 디버깅 뷰에 따라 잘못된 값을 제공할 수 있습니다.
예를 들어 `RealmObject`에 대해 안드로이드 스튜디오에서 볼 때 필드의 값이 표시되됩니다. 불운하게도 필드의 값이 사용되지 않기 때문에 이 값들은 틀렸습니다. Realm은 프록시 오브젝트를 뒤에 생성하여 게터와 세터들을 오버라이드합니다. 이 오버라이드된 메서드들이 Realm의 영속 데이터를 참고하지요. (자세한 내용은 [모델](#section-5)을 참고하세요). 이 액세스를 지켜보면 정확한 값을 얻을 수 있습니다. 이미지를 참고하세요.
<img src="../../../../assets/img/docs/java/realmobject-watch-panel.png" class="img-responsive img-rounded center-block" alt="Android Studio, IntelliJ Debug RealmObject" />
위의 이미지에서 디버거는 113라인에 멈추었습니다. 3개의 워치 밸류가 있는데, `person` 변수, `person.getName()`과 `person.getAge()` 억세서입니다. 107 라인부터 111까지 name과 age를 변경해서 `person` 인스턴스를 변경합니다. 이 값들은 트랜잭션에서 영속적이게 됩니다. 디버거가 멈춘 113 라인에서 `person` 웟치 인스턴스는 필드의 값을 보이고 있고 이 값들은 정확하지 않습니다. 억세서 `person.getName()`와 `person.getAge()`를 사용하는 웟치 벨류들은 정확합니다.
`.toString()` 메서드가 항상 정확한 값을 출력하지만 (`RealmObject`의 변수를 웟치할 때) 웟치 패널은 그렇지 못하다는 점을 유의하세요.
### NDK 디버깅
Realm은 네이티브 코드를 가진 라이브러리입니다. 우리는 [Crashlytics](http://www.crashlytics.com)과 같은 크래쉬 리포팅 도구를 쓰는 것을 추천합니다. 네이티브 에러들을 추적할 수 있고 무언가 문제가 생겼을 때 우리가 더 잘 도울 수 있습니다.
NDK 디버깅은 우리가 사용할 수 있는 것이 기본 스택 트레이스의 제한적인 정보밖에 없을 정도로 까다롭습니다. Crashlytics는 의미 있는 NDK 크래쉬 정보도 잡아줍니다. Crashlytics에서 네이티브 크래쉬 보고를 활성화하려면 [여기 가이드라인](https://fabric.io/downloads/gradle/ndk)을 따라가세요.
여러분의 프로젝트에 NDK 크래쉬 보고를 활성화하려면 아래를 루트의 build.gradle 파일에 추가하세요. `androidNdkOut`와 `androidNdkLibsOut` 값은 필요하지 않습니다.
```groovy
crashlytics {
enableNdk true
}
현재 제약 사항
Realm은 모든 제약사항을 없애기 위해 노력하고 있습니다. 현재 커뮤니티에 기반해서 새로운 기능을 지속적으로 추가하고 있습니다. 하지만 Realm은 여전히 몇 가지 제약 사항이 있고 지금까지의 가장 일반적인 제한 사항을 정리해놨습니다.
깃헙 이슈에서 알려진 이슈 전체 목록을 확인할 수 있습니다.
일반적인 제한
Realm은 사용성과 속도의 균형을 목표로 하고 있습니다. 이러한 목표를 달성하기 위해, 현실적인 제한이 Realm에서 정보를 저장하는 다양한 측면에서 존재합니다. 예를 들어:
- 클래스 이름은 57자로 제한됩니다. Android 버전 Realm은
class_
접두어를 모든 클래스 이름에 추가하고 브라우저에서 일부 확인할 수 있습니다. - 필드 이름의 길이는 63자로 제한됩니다.
- 중첩된 트랜잭션은 지원하지 않고 발견 시 예외 처리됩니다.
String
들과 byte array(byte[]
)는 16MB를 넘을 수 없습니다.
문자열에 대한 정렬과 질의
정렬과 대소문자 무관한 문자열 일치는 ‘Latin Basic’, ‘Latin Supplement’, ‘Latin Extended A’, ‘Latin Extended B’ (UTF-8 range 0-591)에 대해서만 지원하고 있습니다. 추가로 equalTo()
, contain()
, endsWith()
or beginsWith()
를 사용하는 질의에서 대소문자 무관한 설정을 하려면 영어 로케일을 써야만 합니다. 이 제한에 대한 자세한 내용은 여기를 보세요.
쓰레드
동시에 다수의 쓰레드에서 Realm 파일에 접근할 수 있지만 쓰레드 간에 Realm, RealmObject, query, results를 전달 할 수 없습니다. 다중쓰레드 환경에서 Realm 사용법은 쓰레드 예제에서 볼 수 있습니다.
Realm 파일은 여러 프로세스에서 동시에 접근할 수 없습니다.
Realm 파일은 동시적으로 여러 스레드에서 접근할 수 있지만 한 번에 하나의 프로세스에서만 접근할 수 있습니다. 다른 프로세스는 Realm 파일을 복사하거나 그들의 새로운 파일을 만들어야 합니다. 다중 프로세스 지원은 곧 나올 예정입니다.
RealmObject의 hashCode
RealmObject
는 라이브 객체이며 다른 스레드에서 변경이 일어나면 갱신됩니다. RealmObject.equals()
를 썼을 때 true
를 리턴하는 두 개의 Realm 객체는 항상 같은 RealmObject.hashCode() 결과를 반환해야 하지만, 그 값은 안정적이지 않으며
HashMap과
HashSet`에서 키로 쓰면 안됩니다.
모범 사용 예
ANR (Application Not Responding) 에러 예방하기
Realm은 일반적으로 안드로이드의 메인 스레드에서 읽고 쓸 만큼 빠릅니다. 하지만 쓰기 트랜젝션은 여러 스레드에서 블록을 유래할 수 있고 우연한 ANR을 막기 위해 모든 쓰기 작업은 메인 스레드가 아닌 백그라운드에서 수행하는 것이 권장됩니다. 백그라운드 스레드에서 연산을 수행하는 것은 Realm 비동기 트랜잭션을 참고하세요.
Realm 인스턴스들의 생명주기 관리하기
RealmObjects
과 RealmResults
는 데이터 전체를 느긋(lazy)하게 가져옵니다. 이런 이유로 Realm 오브젝트나 질의 결과를 접근할 때 가능한 오래 Realm 인스턴스를 유지하는 것이 중요합니다. Realm 데이터 커넥션을 열고 닫는 추가 비용을 줄이기 위해 레퍼런스 카운트화된 캐시를 가집니다. 이는 Realm.getDefaultInstance()
를 같은 스레드에서 여러 번 호출하는 것은 비용이 들지 않고 내부의 리소스는 자체적으로 모든 인스턴스가 닫히면 해제됨을 의미합니다.
모든 액티비티와 프래그먼트의 UI 스레드에서 Realm 인스턴스를 열고 Activity나 Fragment가 파괴될 때 닫는 것은 쉽고 안전한 접근 법입니다.
// 애플리케이션에서 Realm 설정하기
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
RealmConfiguration realmConfiguration = new RealmConfiguration.Builder(this).build();
Realm.setDefaultConfiguration(realmConfiguration);
}
}
// 액티비티들을 전환하며 onCreate()/onDestroy()가 중첩되면 Activity 2의 onCreate가
// Activity 1의 onDestroy()보다 먼저 호출 됩니다.
public class MyActivity extends Activity {
private Realm realm;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
}
@Override
protected void onDestroy() {
super.onDestroy();
realm.close();
}
}
// 프래그먼트에서 onStart()/onStop()를 사용합니다.
// 프래그먼트의 onDestroy()는 호출되지 않을 수 있습니다.
public class MyFragment extends Fragment {
private Realm realm;
@Override
public void onStart() {
super.onStart();
realm = Realm.getDefaultInstance();
}
@Override
public void onStop() {
super.onStop();
realm.close();
}
}
RealmResults와 RealmObject를 재사용하기
모든 RealmObject
와 RealmResults
는 Realm이 갱신되면 자동으로 갱신됩니다. 이에 따라 RealmChangedListener
에 반응해서 객체들을 다시 가져올 필요는 없습니다. 객체는 이미 갱신되었고 화면에 다시 그려질 준비가 되었습니다.
public class MyActivity extends Activity {
private Realm realm;
private RealmResults<Person> allPersons;
private RealmChangeListener realmListener = new RealmChangeListener() {
@Override
public void onChange(Realm realm) {
// 그냥 다시 뷰를 그립니다. `allPersons`는 이미 최신 데이터를
// 가지고 있습니다.
invalidateView();
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
realm = Realm.getDefaultInstance();
realm.addRealmChangeListener(listener);
allPerson = realm.where(Person.class).findAll(); // "라이브" 질의 결과 만들기
setupViews(); // 뷰 초기화 설정
invalidateView(); // 데이터에 따라 뷰를 다시 그리기
}
// ...
}
Realm 레시피
우리는 Realm을 어떤 업무들을 달성하기 위해 어떻게 쓰는지 다루는 모든 레시피를 한 번에 모아두었습니다. 우리는 정기적으로 더 추가할 것이니 종종 되돌아 확인해보세요. 보고 싶은 예제가 있다면 우리에게 알려주세요. help@realm.io
FAQ
어떻게 Realm 파일을 찾고 내부 콘텐츠를 볼 수 있나요?
StackOverflow 질문 에 Realm 파일을 찾는 방법이 소개되어 있습니다. Realm 브라우저를 통해서 내부 콘텐츠를 볼 수 있습니다.
Realm 라이브러리는 크기가 얼마인가요?
앱을 Release 타깃으로 빌드하고 배포했을 경우에 Realm이 앱에서 차지하는 크기는 최대 800KB에 지나지 않습니다. 배포된 Realm은 더 많은 아키텍트(ARM7, ARMv7, ARM64, x86, MIPS)를 지원하기 때문에 크기가 좀 더 큽니다. APK 파일은 모든 지원 가능한 아키텍트를 포함하지만 Android 인스톨러는 오직 해당 디바이스 아키텍트의 네이티브 코드만을 설치합니다. 결과적으로 설치된 앱은 APK 파일보다 크기가 줄어듭니다.
개별 아키텍처 버전으로 나누어 안드로이드 APK 자체의 크기를 줄일 수 있습니다. build.gradle에 다음을 추가하면 Android Build Tool ABI 분할 지원을 이용해 안드로이드 APK를 분리할 수 있습니다.
splits {
abi {
enable true
reset()
include 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'mips', 'x86', 'x86_64'
}
}
독립된 APK에 포함될 아키텍처를 선택하면 별도로 빌드 됩니다. 자세한 내용을 알고 싶다면 Android Tools 문서를 참고하세요.
예제는 GitHub에 포함되어 있습니다.
상용 어플리케이션에도 Realm을 사용할 수 있나요?
Realm은 2012년부터 상용 제품에 사용되었습니다.
Realm의 Java API가 커뮤니티의 피드백과 함께 더 많은 기능과 개선 사항으로 개선되고 있다고 기대하셔도 됩니다.
Realm을 사용하기 위해 비용을 지불해야 하나요?
아닙니다. Realm은 상용 프로젝트를 포함하여 사용이 무료입니다.
어떻게 수익을 낼 계획입니까?
이미 기술을 중심으로 기업제품과 서비스를 판매하여 수익을 창출하고 있습니다. 만약 릴리즈나 realm-java보다 현재상태에 대해 더욱 알고 싶으시다면, 이메일로도 대화를 할 수 있습니다. 그리고 공개적인 realm-java를 개발하기 위해, 또한 무료 및 Apache 2.0 라이센스 하에 오픈소스로 유지 될 수 있도록 최선을 다하고 있습니다.
코드 안에 “core”로 참조되어 있는 것을 보았습니다. 이것은 무엇인가요?
Core 또는 Realm Core는 핵심 C++ 스토리지 엔진의 옛 이름입니다. 그 코어는 현재 오픈 소스가 아니지만, 한번 내부를 정리하고, 이름을 정리하고, 내부의 동기화 구현을 완료한다면, 역시 Apache 2.0 라이센스로 오픈소스화를 할 계획입니다. 또한, 그 바이너리는 Realm Core의 바이너리 라이센스 내에서 사용 할 수 있습니다.
Java 객체와 Realm 객체의 차이가 무엇인가요?
Java 객체는 데이터를 보유하고 있고 Realm 객체는 데이터를 보유하지 않는 대신에 데이터베이스로부터 직접 속성에 접근합니다.
Realm 객체들의 인스턴스들은 관리(Managed)되거나 비관리(Unmanaged)입니다.
-
관리(Managed) 객체는 Realm에 영속적이고 항상 최신이며 스레드에 연결되어 있습니다. 일반적으로는 자비 힙에서 더 적은 용량을 쓰기 때문에 비관리 버전보다 경량화되어 있습니다.
-
비관리(Unmanaged) 객체는 일반적인 자바 객체와 비슷합니다. 영속적이지 않으며 자동으로 갱신되지 않습니다. 이들은 스레드 사이에 자유롭게 이동할 수 있습니다.
두 상태는 Realm.copyToRealm()
와 Realm.copyFromRealm()
로 전환할 수 있습니다.
왜 모델 클래스는 RealmObject를 확장해야 하나요?
우리는 Realm 특화 기능을 여러분의 모델 클래스에 추가하고 싶습니다. 현재는 removeFromRealm() 가 추가되었고 향후 다른 메서드들이 추가될 예정입니다. 그리고 RealmModel
상속이 우리 API의 일반적인 부분을 사용할 수 있게 하고 읽고 쓰기 쉽게 합니다.
RealmProxy 클래스들은 무엇인가요?
RealmProxy 클래스들은 Realm 객체가 어떤 데이터도 자체적으로 가지고 있지 않게 확신하는 방법입니다. 대신 데이터베이스의 데이터에 직접적으로 접근합니다.
프로젝트의 모든 객체 모델 클래스에서 Realm 어노테이션 프로세서는 관련된 RealmProxy 객체를 만들어 냅니다. 이 클래스는 여러분의 클래스를 확장하며 Realm.createObject()을 호출하면 반환되는 것입니다. 하지만 여러분의 코드 관점에서 차이를 느끼기 어렵습니다.
Realm 객체를 쓸 때 트랜잭션을 사용해야 하나요?
트랜잭션은 하나의 원자 오퍼레이션으로 여러 필드가 수정되는 걸 명시하기 위해 사용합니다. 완전히 정상적으로 종료할지 취소할지(에러 혹은 롤백) 수정의 범위를 정의해야 합니다. 트랜잭션 범위를 명시하면서 얼마나 자주(혹은 빨리) 수정 사항을 반영할지 조절할 수 있습니다. (예. 한 오퍼레이션에서 여러 객체 삽입)
SQLite 같은 일반적인 SQL 기반의 데이터베이스에서 삽입을 할 때 한 번에 여러 필드를 삽입합니다. 자동적으로 트랜잭션 내에서 처리되지만 사용자에게 보이진 않습니다. Realm에서는 모든 부분에 명시적입니다.
out-of-memory exception 이 발생하면 어떻게 해야 합니까?
Android용 Realm은 내부의 스토리지 엔진을 사용합니다. 이 스토리지 엔진은 JVM 힙 대신 네이티브 메모리를 할당합니다. 스토리지 엔진이 네이티브 메모리 할당에 실패하거나 파일 시스템이 가득 차면 Realm은 io.realm.internal.OutOfMemoryError
예외를 발생합니다. 이런 메시지를 무시하지 않는 게 중요합니다. 만약 앱이 계속 동작하고 Realm 파일에 접근하면 충돌났거나 불일관성인 상태에 접근하게 됩니다. 이 io.realm.internal.OutOfMemoryError
경우, 앱을 종료하는 것이 안전합니다.
커다란 Realm 파일 사이즈
일반적으로, Realm 데이터베이스 파일이 SQLite 데이터베이스 파일보다 적은 공간을 차지합니다.
데이터에 대한 일관적인 접근을 위해서 Realm은 다양한 Realm 버전에서 동작합니다. Realm에서 데이터를 읽고 다른 쓰레드에서 Realm에 쓰기를 하는 동안 오래 도는 동작으로 인해 쓰레드를 막고 있으면, 해당 버전은 업데이트되지 않고 원치 않는 Realm은 중간 상태 버전의 데이터에서 멈추게 되어 파일 사이즈가 지속적으로 증가하게 됩니다. (이 빈 공간은 언젠가는 미래의 쓰기 동작에서 사용되거나 compactRealm
를 호출 함으로써 제거할 수 있습니다.)
앱을 실행하면 Mixpanel로 네트워크 연결이 발생하는데 이게 뭐예요?
Realm은 소스코드의 바이트코드 변환기가 수행될 때 익명으로 통계를 수집합니다. 완전하게 익명이며 어떤 버전의 Realm을 쓰고 어떤 운영체제를 쓰는지 어떤 환경에서는 더 이상 사용되지 않는지를 확인하여 제품의 개선을 돕습니다. 이 호출은 사용자의 기기에서는 실행되지 않으며, 개발자의 장비에서만 호출이 발생합니다. 개발자의 소스코드가 빌드될 때만 호출됩니다. 실제로 어떻게 무슨 정보를 모으는지는 저희의 소스 코드에서 확인할 수 있습니다.
“librealm-jni.so” 파일을 불러오지 못해요.
앱이 64 비트 아키텍처를 지원하지 못하는 다른 네이티브 라이브러리를 사용하고 있다면 안드로이드에서 Realm은 ARM64 장비에 librealm-jni.so
를 불러오는데 실패합니다. 안드로이드는 32 비트 네이티브 라이브러리와 64 비트 네이티브 라이브러리를 동시에 불러오지 못합니다. 최상의 방법은 모든 라이브러리의 지원 ABIs 집합을 동일하게 제공하는 것입니다. 하지만 써드 파티 라이브러리를 사용함에 있어서 그렇게 하기 힘든 경우가 있습니다. VLC and Realm Library conflicts 문제를 참고하세요.
우회하는 방법은 앱의 build.gradle
에 아래 코드를 추가하여 APK 파일에서 ARM64 라이브러리를 제거하는 방법이 있습니다. 자세한 정보를 보려면 안드로이드의 32 비트와 64 비트 의존성을 참고하세요.
android {
//...
packagingOptions {
exclude "lib/arm64-v8a/librealm-jni.so"
}
//...
}
또 .so 파일을 jar 파일에 부적절하게 패키징하는 안드로이드 그래들 플러그인 1.4.0 베타 버그도 있습니다. Realm Java 이슈 1421를 참고하세요. 안드로이드 그래들 플러그인 1.3.0으로 돌아가가거나 1.5.0 이상의 버전을 쓰면 문제를 해결할 수 있습니다.
어떻게 Realm을 백업하고 복원합니까?
Realm은 파일 시스템의 파일로 저장합니다. getPath()
를 호출하여 Realm 파일의 전체 경로를 알 수 있습니다. Realm 파일을 백업하거나 복원하려면 모든 Realm 인스턴스들이 닫혀있어야 합니다.
구글 드라이브를 이용한 Realm 백업에 대한 연재가 있으니 참고하실 수 있습니다. 파트 1, 파트 2, 파트 3.