Realm이란 무엇인가?
- 모바일용 크로스 플랫폼 데이터베이스
- SQLite 기반의 ORM 프레임워크
시작하기
프로젝트에 Realm 추가
- 프로젝트의
build.gradle
파일에 다음과 같이 classpath를 추가한다.
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "io.realm:realm-gradle-plugin:1.2.0"
}
}
- 앱의
build.gradle
파일에 다음과 같이 플러그인을 추가한다.
apply plugin: 'realm-android'
Realm 설정
초기화
- Realm을 사용하기 전에, 반드시 초기화해야 한다.
Realm.init()
메서드를 이용해 초기화한다. 파라미터로 Context
를 받는다.
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Realm.init(this);
}
}
설정정보 변경
RealmConfiguration
객체를 이용해 Realm에 대한 설정정보를 설정한다.
- 다음 예시처럼 여러가지 설정정보를 변경할 수 있다.
RealmConfiguration config = new RealmConfiguration.Builder()
.name("myrealm.realm")
.encryptionKey(getKey())
.schemaVersion(42)
.modules(new MySchemaModule())
.migration(new MyMigration())
.build();
Realm realm = Realm.getInstance(config);
- 설정정보를 여러개 만들어서 각각의 데이터베이스를 구성할 수도 있다.
RealmConfiguration myConfig = new RealmConfiguration.Builder()
.name("myrealm.realm")
.schemaVersion(2)
.modules(new MyCustomSchema())
.build();
RealmConfiguration otherConfig = new RealmConfiguration.Builder()
.name("otherrealm.realm")
.schemaVersion(5)
.modules(new MyOtherSchema())
.build();
Realm myRealm = Realm.getInstance(myConfig);
Realm otherRealm = Realm.getInstance(otherConfig);
inMemotry()
메서드를 이용하면, disk에 저장되지 않고 메모리에만 임시로 있는 데이터베이스를 만들 수 있다.
RealmConfiguration myConfig = new RealmConfiguration.Builder()
.name("myrealm.realm")
.inMemory()
.build();
Realm instance Closing
- Realm은 레퍼런스 카운트 방식을 도입했기 때문에,
getInstance()
메서드를 호출해서 Realm 객체를 얻어왔으면, close()
메서드를 통해 객체를 닫아주어야 한다.
- 그러나 Realm 객체는 싱글턴이므로
getInstance()
메서드를 여러번 이용해도 동일 객체가 나온다.
closeable
인터페이스를 구현한 클래스이므로, JDK 7 & minSdkVersion >= 19
에서는 다음과 같이 사용할 수 있다.
try (Realm realm = Realm.getDefaultInstance()) {
}
Models
Models
- Realm은 기본적으로
RealmObject
를 상속받은 class들을 모델로 인식한다.
RealmModel
인터페이스를 구현하고, @RealmClass
어노테이션을 붙인 일반 class도 모델로 인식한다.
- RealmObject를 상속받은 객체는 Realm 프레임워크가 제공하는 인스턴스 메서드들을 그냥 사용하면 되지만, RealmModel를 구현하는 객체는 RealmObject 클래스의 static 메서드를 이용한다.
- 일반 자바 객체처럼, getter, setter 이외의 일반 메서드도 사용할 수 있다.
public class User extends RealmObject {
private String name;
private int age;
@Ignore
private int sessionId;
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; }
}
@RealmClass
public class User implements RealmModel {
private String name;
private int age;
@Ignore
private int sessionId;
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; }
}
user.isValid();
user.addChangeListener(listener);
RealmObject.isValid(user);
RealmObject.addChangeListener(user, listener)
Field 타입
- boolean, byte, short, int, long, float, double, String, Date, byte[] 타입이 사용가능하다.
- 자바의 Boxed 타입인 Boolean, Byte, Short, Integer, Long, Float, Double 또한 사용할 수 있다.
- Boxed타입을 사용하면
null값 표현이 가능
하다.
RealmObject
, RealmList<? extends RealmObject>
타입을 이용하여 객체간 관계를 표현한다.
Required Fields & null 값
- null값이 들어갈 가능성이 있는 타입의 필드 중, null값을 허용하지 않으려면
@Required
어노테이션을 붙인다.
- Boolean, Byte, Short, Integer, Long, Float, Double, String, byte[], Date 타입에
@Required
어노테이션을 붙일 수 있다.
- 그 외의 타입들은
@Required
어노테이션을 붙이게 되면, 컴파일이 실패하게 된다.
- Primitive 타입, RealmList 타입은 암시적으로 Required 상태이다.
- RealmObject 타입은 항상 nullable 이다.
Ignoring 속성
- DB에 저장할 필요가 없는 필드에
@Ignore
속성을 붙이면, 그 속성은 DB에 저장되지 않는다.
자동 갱신 객체
- RealmObject 객체의 필드값을 변경하면, 그 결과는 즉시 쿼리에 반영된다.(쿼리를 갱신할 필요가 없다.)
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);
}
});
myDog.getAge();
속성 인덱싱
- Realm객체의 필드에
@Index
어노테이션을 추가하면, 그 필드를 기준으로 인덱싱된다.
- 인덱싱을 하면, 데이터 추가가 느려지고, 파일이 커지는 단점이 있다.
- 대신, 쿼리가 빨라진다.
- Realm DB는 String, byte, short, int, long, boolean, Date 필드에만 인덱싱을 허용한다.
Primary Key
- Realm 객체에서 Primary Key를 추가하려면, 키로 이용하려는 필드에
@PrimaryKey
어노테이션을 붙인다.
- Primary Key로 이용하려면
문자열(String)
타입이거나 정수(byte, short, int, long / Byte, Short, Integer, Long)
이어야 한다.
- 여러 필드를 함께 Primary Key로 쓸 수 없다.(Compund Key는 불가능)
@PrimaryKey
어노테이션은 암시적으로 @Index
어노테이션을 포함하고 있다.
@PrimaryKey
어노테이션은 필드값에 null을 허용한다. null값 허용을 막으려면 @Required
어노테이션과 함꼐 사용해야 한다.
- Primary Key를 지정하면,
copyToRealmOrUpdate()
메서드를 이용할 수 있따.
- 업데이트 시, Key를 이용해 객체를 찾으면 그 객체를 업데이트하고, 객체를 못찾으면 새로운 객체를 생성한다.
- Primary Key 없이 메서드를 호출하면 exception이 발생한다.
Realm.createObject()
메서드로 Realm 객체를 생성하면, 모든 필드에 기본값이 채워지게 되는데, 이 경우 Key가 기본값으로 중복된 객체가 생성될 수 있다.
copyToRealm()
메서드를 이용하면 이 문제를 피할 수 있다.
final MyObject obj = new MyObject();
obj.setId(42);
obj.setName("Fish");
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
realm.copyToRealmOrUpdate(obj);
}
});
커스터마이징 객체
- RealmObject는 거의 POJO처럼 이용가능하다.
- Realm에서 관리되는 객체를 만드려면,
createObject()
나 copyToRealm()
메서드를 이용해야 한다.
제한
final
, transient
, volatile
키워드를 지원하지 않는다.
RealmObject
대신 다른 클래스를 상속받는 클래스는 허용되지 않는다.
- 기본생성자(인자가 없는 생성자)는 반드시 비어있어야 한다.
Relationship
- RealmObject는 서로 연결될 수 있다.
- 일반적인 Join 연산과 다르게, Realm에서는 일반적으로 가벼운 연산에 속한다.
- has-a 관계이다.
Many - to - 1
- 메인 RealmObject 서브클래스 모델에, 연결할 RealmObject의 서브클래스를 필드로 추가한다.
public class Email extends RealmObject {
private String address;
private boolean active;
}
public class Contact extends RealmObject {
private String name;
private Email email;
}
- 위 예제에서, 하나의 Contract 객체는 하나의 Email객체를 가진다. 반면에, 하나의 Email 객체는 여러 Contract 객체에 속할 수 있다.
- 종종
1-to-1
로도 이용된다.
- RealmObject 필드에 null 값을 넣어서 연결을 끊을 수 있다. 연결이 끊어져도 객체는 그대로 DB에 남아있다.
Many - to - Many
- 메인 RealmObject 서브클래스 모델에,
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의 서브클래스만 포함할 수 있다.
- 자바의 List와 메서드가 동일하다.
- 재귀 관계도 가능하다.
- RealmList에 null값을 지정하면 리스트는 지워진다. 리스트가 지워져도 객체는 그대로 DB에 남아있다.
- RealmList는 절대 null을 반환하지 않는다. 반환된 리스트는 비어있는(size == 0) 리스트이다.
Link Query
- Relationship이 있는 객체를 연결하여 쿼리할 수 있다.
- 관계가 있는 필드의 속성을 조회하려면
.
구분자를 이용해 경로를 명시한다.
- 조건을 거는 메서드로는
equalTo
, lessThen
, greaterThen
등을 이용한다.
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;
}
- 위의 예시에서, 색깔이 Brown인 강아지를 가지고 있는 사람을 조회하려면 다음과 같다.
RealmResults<Person> persons = realm.where(Person.class)
.equalTo("dogs.color", "Brown")
.findAll();
equalTo
메서드를 연달아 사용하면 기본 쿼리에서 And 를 표현할 수 있다.
RealmResults<Person> r1 = realm.where(Person.class)
.equalTo("dogs.name", "Fluffy")
.equalTo("dogs.color", "Brown")
.findAll();
- 쿼리 결과내에서 쿼리를 다시 진행하려면 다음과 같이 빌더 패턴을 이용한다.
RealmResults<Person> r2 = realm.where(Person.class)
.equalTo("dogs.name", "Fluffy")
.findAll()
.where()
.equalTo("dogs.color", "Brown")
.findAll();
.where()
.equalTo("dogs.color", "Yellow")
.findAll();
Write
- 읽기 연산은 아무때나 가능하지만 쓰기연산은
transaction 안에서만 가능
하다.
- 쓰기 transaction은 모두 저장되거나, 모두 롤백된다.
Realm realm = Realm.getDefaultInstance();
realm.beginTransaction();
realm.commitTransaction();
- Write Transaction은 다른 동작을 block하게 된다. 그러므로 UI와 background 스레드에서 동시에 Write Transaction을 수행하면 ANR 에러가 발생한다.
- 이를 해결하기 위해 async transaction을 이용
- 읽기 연산은 block되지 않는다.
- Realm은 Exception이 발생해도 이를 내부적으로 처리하기 때문에 크래쉬가 발생하지 않는다. 그러므로 적절하게 transaction을 취소하는 처리가 필요하다.
오브젝트 생성
- RealmObject는 Realm 클래스와 강하게 연관되어 있어서, Realm 클래스를 통해서 생성하는 것을 원칙으로 한다.
- Realm 클래스를 통해 생성하려면
createObject
메서드를 이용한다.
realm.beginTransaction();
User user = realm.createObject(User.class);
user.setName("John");
user.setEmail("john@corporation.com");
realm.commitTransaction();
- RealmObject의 확장성을 고려해, 일반 생성자를 이용해 객체를 먼저 생성한 후, Realm 클래스에 등록하는 방법도 있다.
- Realm 클래스에 등록하려면
copyToRealm
메서드를 이용한다.
- 여러 타입의 생성자를 지원하지만, 생성자를 커스터마이징 하려면 반드시 기본생성자를 구현하되, 생성자 내부는 아무 동작도 처리되지 않도록 한다.
- Realm 클래스에 등록하고 나면, 처음 생성한 object는 더이상 영구적으로 관리되지 않는다.
copyToRealm
메서드를 통해 반환받은 object만 영구적으로 관리된다.
User user = new User("John");
user.setEmail("john@corporation.com");
realm.beginTransaction();
User realmUser = realm.copyToRealm(user);
realm.commitTransaction();
Transaction Block
executeTransaction
메서드를 이용하면, beginTransaction
, commitTransaction / cancelTransaction
을 대체할 수 있다.
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");
}
});
Asynchronous Transactions
- 트렌젝션들은 다른 트렌젝션에 의해 블락되므로, Realm 트렌젝션은 백그라운드에서 실행할 필요가 있다.
OnSuccess
, OnError
콜백을 통해 트렌젝션이 완료된 후의 콜백을 받는다.
- 콜백은
Looper
에 의해 관리되므로, Looper가 있는 스레드에서만 이용할 수 있다.
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) {
}
});
executeTransactionAsync()
메서드는 RealmAsyncTask
객체를 반환한다. 이 객체를 이용하면 트렌젝션이 완료되지 않았을 때, 원하는 시점에 cancel할 수 있다.
class MyActivity extends Activity {
...
private RealmAsyncTask transaction = 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);
...
@Override
public void onStop() {
super.onStop();
if (transaction != null && transaction.isCancelled()) {
transaction.cancel();
}
}
...
}
String & byte arrays 갱신
- Realm은 String 이나 byte array의 개별 원소들의 갱신이 불가능하다.
- 스레드간의 데이터 무결성을 지키기 위해 전체 String, byte array가 하나의 단위로 움직인다.
- 다음의 예시는 byte array의 5번째 원소를 변경하는 예제이다.
realm.executeTransaction(new Realm.Transaction() {
@Override
public void execute(Realm realm) {
bytes[] bytes = realmObject.binary;
bytes[4] = 'a';
realmObject.binary = bytes;
}
});
Query
- Realm 객체의
where()
메서드를 이용해 RealmQuery
객체를 얻을 수 있다.
RealmQuery
클래스는 체이닝 메서드를 지원하므로 편한 방법으로 쿼리문을 만들 수 있고, 마지막으로 findAll()
메서드를 호출하여 RealmResults
객체를 얻는다.
findAll()
메서드는 쿼리결과가 같더라도 항상 새로운 RealmResults
객체를 반환한다.
- 다음은 User 클래스에서 name이 Jone이거나 Peter 인 객체를 찾는 예제이다.
public class User extends RealmObject {
@PrimaryKey
private String name;
private int age;
@Ignore
private int sessionId;
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; }
}
RealmQuery<User> query = realm.where(User.class);
query.equalTo("name", "John");
query.or().equalTo("name", "Peter");
RealmResults<User> result1 = query.findAll();
RealmResults<User> result2 = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAll();
RealmResults
클래스는 AbstractList
클래스를 상속받은 클래스로, List와 사용방법이 비슷하다.
- 쿼리결과가 없을 경우에는 빈 Result(
size() == 0
)가 반환된다.
RealmResults
객체를 수정하거나 변경하려면 반드시 Write Transaction 안에서 수행해야 한다.
조건
- 다음과 같은 조건들이 있다.
- 범위비교 : between(), greaterThan(), lessThan(), greaterThanOrEqualTo(), LessThanOrEqualTo()
- 동등 : equalTo(), notEqualTo()
- 문자열 : contains(), beginsWidth(), endsWidth()
- Null : isNull(), isNotNull()
- Empty : isEmpty(), isNotEmpty()
Modifiers
- 문자열 조건은
Case.INSENSITIVE
키워드를 이용해 대소문자를 무시할 수 있다.
논리연산
- 여러 조건이 있을 때, 각 조건은 기본적으로
&&
연산으로 결합된다.
||
연산은 명시적으로 or()
메서드를 호출해야 한다.
beginGroup()
, endGroup()
메서드를 이용해 (
, )
를 표현할 수 있다.
RealmResults<User> r = realm.where(User.class)
.greaterThan("age", 10)
.beginGroup()
.equalTo("name", "Peter")
.or()
.contains("name", "Jo")
.endGroup()
.findAll();
정렬
- 정렬을 하려면 쿼리를 수행한 후,
RealmResults
객체의 sort()
메서드를 이용해 정렬한다.
RealmResults<User> result = realm.where(User.class).findAll();
result = result.sort("age");
result = result.sort("age", Sort.DESCENDING);
체이닝 쿼리
RealmResults
객체를 다시 쿼리하여 결과를 얻을 수 있다.
RealmResults<Person> teenagers = realm.where(Person.class).between("age", 13, 20).findAll();
Person firstJohn = teenagers.where().equalTo("name", "John").findFirst();
- 객체 간에 Relationship 관계가 있을 경우, 그 Relationship이 있는 필드를 쿼리하는 것도 가능하다.
RealmResults<Person> teensWithPups = realm.where(Person.class).between("age", 13, 20).equalTo("dogs.age", 1).findAll();
- 체이닝 쿼리는
RealmResults
클래스에서 제공하는 기능이다. RealmQuery
클래스에서 조건을 추가한다면, 그건 체이닝 쿼리가 아니고 기존 쿼리문을 수정하는 것이다.
자동 업데이트
RealmResults
객체는 자동으로 변경된 값이 적용되기 때문에, 다시 쿼리할 필요가 없다.
RealmChangeListener
를 등록하여, 값이 변경되는 시점의 콜백을 받을 수 있다.
- 값 변경에 따라 UI를 변경해야 할 경우, 이 시점에서 업데이트 해준다.
final RealmResults<Dog> puppies = realm.where(Dog.class).lessThan("age", 2).findAll();
puppies.size();
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.size();
puppies.size();
}
});
- 자동으로 결과가 변경되기 때문에, 결과의 인덱스나 갯수는 변경될 수 있으므로 사용할 때 유의해야 한다.
Aggregation
RealmResults
클래스는 필드의 합,평균 등을 구할 수 있는 메서드를 가지고 있다.
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();
Iterations
RealmResults
클래스는 Iterable
이므로 다음 두가지 방법으로 for문을 이용할 수 있다.
RealmResults<User> results = realm.where(User.class).findAll();
for (User u : results) {
}
for (int i = 0; i < results.size(); i++) {
User u = results.get(i);
}
- 반복문을 이용할 때, 결과 자체를 수정하거나 삭제하지 않고, 결과의 값을 수정하거나 삭제하면 무결성이 깨져서 앱이 종료될 수 있으므로 주의한다.
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);
}
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.deleteFromRealm(5);
results.deleteAllFromRealm();
}
});
비동기 쿼리
- 대부분의 쿼리는 UI 스레드에서 실행해도 될 정도로 빠르지만, 데이터가 많거나 복잡한 쿼리의 경우 백그라운드 스레드로 실행해야 한다.
- 비동기 쿼리를 실행하는 스레드는 반드시
Looper
가 존재해야 한다.(없을 경우 IllegalStateException
발생)
쿼리 생성
findAll()
대신 findAllAsync()
메서드를 이용한다.
findAllAsync()
메서드는 바로 리턴되며, 쿼리의 추가 작업은 백그라운드 스레드에서 계속 진행될 것이다. 작업이 완료되면 자동으로 RealmResults
객체를 갱신해준다.
- 결과가 로드되었는지 확인하려면
isLoaded()
메서드를 이용한다. (비동기 쿼리가 아닌 경우에는 무조건 true를 반환한다.)
RealmResults<User> result = realm.where(User.class)
.equalTo("name", "John")
.or()
.equalTo("name", "Peter")
.findAllAsync();
if (result.isLoaded()) {
}
Callback 등록
RealmChangeListener
인터페이스를 이용하면, 비동기 쿼리가 완료되었을 때의 콜백을 받을 수 있다.
RealmResults
에 addChangeListener()
메서드로 등록하고, removeChangeListener()
메서드로 해제한다.
class MyActivity extends Activity {
...
private RealmChangeListener callback = new RealmChangeListener() {
@Override
public void onChange(RealmResults<User> results) {
}
};
...
@Override
public void onStart() {
super.onStart();
RealmResults<User> result = realm.where(User.class).findAllAsync();
result.addChangeListener(callback);
}
@Override
public void onStop() {
super.onStop();
result.removeChangeListener(callback);
result.removeChangeListeners();
}
...
}
작성자가 댓글을 삭제했습니다.
답글삭제realm.executeTransaction(new Realm.Transaction(){
답글삭제안에서 다시 realm.beginTransaction(); 가 포함된 함수를 호출할려면 어떻게 해야 하나요? 트랜스액션이 이미 있다고 에러가 뜨면서 종료됩니다.
첫번째 트랜스액션을 종료시키고 다음 함수를 호출해야 할듯 한데 방법을 모르겠어요???
executeTransaction은 이미 내부적으로 트랜잭션을 가지고 있습니다.
삭제그러므로 다시 트랜잭션을 시작할 필요가 없습니다.
만약 트랜잭션을 명시적으로 관리하려고 하신다면, executeTransaction 을 이용하지 않고, 명시적으로 beginTransaction / commitTransaction 을 이용해주어야 합니다.