Realm

2019. 10. 14. 15:21모바일/Android_Java

Realm

  • 모바일용 크로스 플랫폼 데이터베이스
  • SQLite기반
  • 빌더패턴을 사용한다
    • 빌더 패턴은 객체 생성을 깔끔하고 유연하게 하기 위한 기법이다.
    • 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라
    • 기존에 진행하던 생성자패턴은 점층적 생성자 패턴
Member customer = Member.build()
    .name("홍길동")
    .age(30)
    .build();

장단점

장점

  • 성능이 좋다
  • Android,IOS DB 공유가 가능하다
  • 데이터가 실시간으로 업데이트되는 UI에 적합하다.

단점

  • 바이너리 용량이 늘어난다
  • 다양한 쿼리를 지원하지 않는다.

함수들

  • inmemory() : disk에 저장되지 않고 메모리에만 임시로 있는 데이터베이스를 만들 수 있다.

RealmObject, RealmModel

  • Realm은 RealmObject를 상속받은 class를 모델로 인식함
  • RealmModel 인터페이스를 구현하고, @RealmClass 어노테이션을 붙인 일반 class도 모델로 인식한다.
// 모델 객체 사용하기
// With RealmObject
user.isValid();
user.addChangeListener(listener);

// With RealmModel
RealmObject.isValid(user);
RealmObject.addChangeListener(user, listener)

자동갱신객체

  • RealmObject객체의 필드값을 변경하면 그 결과는 즉시 쿼리에 반영된다.
// Realm 객체 생성
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(); // => 2

Link Query

  • Relationship이 있는 객체를 연결하여 쿼리 가능
  • 관계가 있는 필드으 ㅣ속성 조회하려면 . 구분자 이용해 경로 명시
    • 조건을 거는 메서드로는 equalTo, lessThen, greaterThen등을 이용

사진1첨부

public class Person extends RealmObject {
  private String id;
  private String name;
  private RealmList<Dog> dogs;
  // getters and setters
}

public class Dog extends RealmObject {
  private String id;
  private String name;
  private String color;
  // getters and setters
}
위의 예시에서, 색깔이 Brown인 강아지를 가지고 있는 사람을 조회하려면 다음과 같다.
// persons => [U1,U2]
RealmResults<Person> persons = realm.where(Person.class)
                                .equalTo("dogs.color", "Brown")
                                .findAll();
equalTo 메서드를 연달아 사용하면 기본 쿼리에서 And 를 표현할 수 있다.
// 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();

쓰장

  • 쓰기연산은 transaction안에서만 가능하다.
읽기 연산은 아무때나 가능하지만 쓰기연산은 transaction 안에서만 가능하다.
쓰기 transaction은 모두 저장되거나, 모두 롤백된다.
// Obtain a Realm instance
Realm realm = Realm.getDefaultInstance();

realm.beginTransaction();

//... add or update objects here ...

realm.commitTransaction();
// Transaction을 취소하려면 다음과 같이 실행
// realm.cancelTransaction();
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); // Create a new object
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");

// Copy the object to Realm. Any further changes must happen on realmUser
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() {
                // Transaction was a success.
            }
        }, new Realm.Transaction.OnError() {
            @Override
            public void onError(Throwable error) {
                // Transaction failed and was automatically canceled.
            }
        });
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;
    }
});

쿼리

  • where()로 RealmQuery객체 얻음
  • 체이닝메서드 지원, 쿼리문생성 쉽다
다음은 User 클래스에서 name이 Jone이거나 Peter 인 객체를 찾는 예제이다.
public class User extends RealmObject {

    @PrimaryKey
    private String          name;
    private int             age;

    @Ignore
    private int             sessionId;

    // Standard getters & setters generated by your 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; }
}

// Build the query looking at all users:
RealmQuery<User> query = realm.where(User.class);

// Add query conditions:
query.equalTo("name", "John");
query.or().equalTo("name", "Peter");

// Execute the query:
RealmResults<User> result1 = query.findAll();

// Or alternatively do the same all at once (the "Fluent interface"):
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)  //implicit AND
                            .beginGroup()
                                .equalTo("name", "Peter")
                                .or()
                                .contains("name", "Jo")
                            .endGroup()
                            .findAll();
정렬
정렬을 하려면 쿼리를 수행한 후, RealmResults 객체의 sort()메서드를 이용해 정렬한다.
RealmResults<User> result = realm.where(User.class).findAll();
result = result.sort("age"); // Sort ascending
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(); // => 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 and puppies point are both up to date
      results.size(); // => 1
      puppies.size(); // => 1
    }
});
자동으로 결과가 변경되기 때문에, 결과의 인덱스나 갯수는 변경될 수 있으므로 사용할 때 유의해야 한다.
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) {
    // ... do something with the object ...
}

for (int i = 0; i < results.size(); i++) {
    User u = results.get(i);
    // ... do something with the object ...
}
반복문을 이용할 때, 결과 자체를 수정하거나 삭제하지 않고, 결과의 값을 수정하거나 삭제하면 무결성이 깨져서 앱이 종료될 수 있으므로 주의한다.
// 앱이 죽는 예
final RealmResults<User> users = getUsers();
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        users.get(0).deleteFromRealm(); // indirectly delete object
    }
});

for (User user : users) {
    showUser(user); // Will crash for the deleted user
}

// 올바른 접근
final RealmResults<User> users = getUsers();
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        users.deleteFromRealm(0); // Delete and remove object directly
    }
});

for (User user : users) {
    showUser(user); // Deleted user will not be shown
}
삭제
다음의 예시처럼, 쿼리 결과를 Realm으로부터 삭제할 수 있다.
// obtain the results of a query
final RealmResults<Dog> results = realm.where(Dog.class).findAll();

// All changes to data must happen in a transaction
realm.executeTransaction(new Realm.Transaction() {
    @Override
    public void execute(Realm realm) {
        // remove single match
        results.deleteFirstFromRealm();
        results.deleteLastFromRealm();

        // remove a single object
        Dog dog = results.get(5);
        dog.deleteFromRealm();

        // remove a single object using other way
        results.deleteFromRealm(5);

        // Delete all matches
        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()) {
    // Result are now available
}
Callback 등록
RealmChangeListener 인터페이스를 이용하면, 비동기 쿼리가 완료되었을 때의 콜백을 받을 수 있다.
RealmResults에 addChangeListener() 메서드로 등록하고, removeChangeListener() 메서드로 해제한다.
class MyActivity extends Activity {
    ...
    private RealmChangeListener callback = new RealmChangeListener() {
        @Override
        public void onChange(RealmResults<User> results) {
            // called once the query complete and on every update
        }
    };
    ...

    @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); // remove a particular listener
        // or
        result.removeChangeListeners(); // remove all registered listeners
    }
    ...
}

출처 : https://johngrib.github.io/wiki/builder-pattern/

'모바일 > Android_Java' 카테고리의 다른 글

액티비티 전환 플래그  (0) 2019.10.16
Activity 관련  (0) 2019.10.16
DataBinding  (0) 2019.10.14
안드로이드에서 자주쓰는 함수  (0) 2019.10.08
RecyclerView  (0) 2019.10.08