레이블이 query인 게시물을 표시합니다. 모든 게시물 표시
레이블이 query인 게시물을 표시합니다. 모든 게시물 표시

2016년 10월 26일 수요일

[안드로이드] MediaStore에서 파일 다루기

MediaStore

  • 안드로이드 시스템에서 제공하는 미디어 데이터 DB
  • 시스템은 파일시스템에 저장되어 있는 미디어 파일들을 이 DB에 추가하여, 여러 앱에서 이용할 수 있도록 한다.
  • 시스템이 제공하는 Provider를 이용해 미디어 파일(오디오, 이미지, 비디오)를 쿼리할 수 있다.
  • API 11부터는 미디어 파일 이외에도, 일반 파일을 쿼리할 수 있는 API가 추가되었다.

MediaStore.Files

  • 파일을 다루기위해 추가된 API
  • getContentUri() 메서드를 호출하여 쿼리할 Uri를 얻을 수 있다.
    • 인자로 internalexternal을 넘겨야 하며, 이 값으로 스토리지 영역을 구분한다.

MediaStore.Files.FileColumns

예제 소스

  • 다음 예제는 확장자가 txt인 파일을 쿼리하는 예제이다.
public static void queryFiles(Context context) {
    // External 스토리지의 URI 획득
    final Uri uri = MediaStore.Files.getContentUri("external");
    //ID, 파일명, mimeType, 파일크기 을 가져오도록 설정
    final String[] projection = new String[] {MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.MIME_TYPE, MediaStore.Files.FileColumns.SIZE};
    //확장자가 txt인 mimeType을 쿼리
    final String selection = MediaStore.Files.FileColumns.MIME_TYPE + "=?";
    final String[] selectionArgs = new String[] {MimeTypeMap.getSingleton().getMimeTypeFromExtension("txt")};

    // 쿼리 수행 후, 컬럼명, 값 출력
    Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
    while (cursor != null && cursor.moveToNext()) {
        int columnCount = cursor.getColumnCount();
        for (int i = 0 ; i < columnCount ; i++ ) {
            Logger.logDebug(cursor.getColumnName(i) + " : " + cursor.getString(i));
        }
        Logger.logDebug("----------------------------");
    }
}
  • 다음은 audio, image, video를 제외한 형식의 파일 목록을 얻는 예이다
public static void queryFiles(Context context) {
    // External 스토리지의 URI 획득
    final Uri uri = MediaStore.Files.getContentUri("external");
    //ID, 파일명, mimeType, 파일크기 을 가져오도록 설정
    final String[] projection = new String[] {MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.MIME_TYPE, MediaStore.Files.FileColumns.SIZE};
    //MEDIA_TYPE_NONE인 데이터 쿼리
    final String selection = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?";
    final String[] selectionArgs = new String[] {MediaStore.Files.FileColumns.MEDIA_TYPE_NONE};

    // 쿼리 수행 후, 컬럼명, 값 출력
    Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
    while (cursor != null && cursor.moveToNext()) {
        int columnCount = cursor.getColumnCount();
        for (int i = 0 ; i < columnCount ; i++ ) {
            Logger.logDebug(cursor.getColumnName(i) + " : " + cursor.getString(i));
        }
        Logger.logDebug("----------------------------");
    }
}
  • 다음은 external storage에서 특정 검색 단어를 쿼리하는 예이다
public static void queryFiles(Context context) {
    // External 스토리지의 URI 획득
    final Uri uri = MediaStore.Files.getContentUri("external");
    //ID, 파일명, mimeType, 파일크기 을 가져오도록 설정
    final String[] projection = new String[] {MediaStore.Files.FileColumns._ID, MediaStore.Files.FileColumns.DISPLAY_NAME, MediaStore.Files.FileColumns.MIME_TYPE, MediaStore.Files.FileColumns.SIZE};
    //파일명에 Keyword 가 있으면 쿼리
    String keyword = "2016"
    final String selection = MediaStore.Files.FileColumns.TITLE + " LIKE ?";
    final String[] selectionArgs = new String[] {"%" + keyword + "%"};

    // 쿼리 수행 후, 컬럼명, 값 출력
    Cursor cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, null);
    while (cursor != null && cursor.moveToNext()) {
        int columnCount = cursor.getColumnCount();
        for (int i = 0 ; i < columnCount ; i++ ) {
            Logger.logDebug(cursor.getColumnName(i) + " : " + cursor.getString(i));
        }
        Logger.logDebug("----------------------------");
    }
}
  • 다음은 미디어 DB에 들어있는 하나의 Row를 전부 출력한 내용이다.
  • 기존 미디어 DB를 그대로 이용하기 때문에, 미디어 데이터에 최적화되어 있다. 그러므로 파일 데이터에는 불필요한 컬럼이 많으니, 적절한 projection을 작성할 필요가 있다.
[MediaFileTest_23]: _id : 15355
[MediaFileTest_23]: _data : /storage/emulated/0/data/colornote/backup/1477006699767-AUTO.doc
[MediaFileTest_23]: _size : 37244
[MediaFileTest_23]: format : 47747
[MediaFileTest_23]: parent : 54
[MediaFileTest_23]: date_added : 1477042269
[MediaFileTest_23]: date_modified : 1477006700
[MediaFileTest_23]: mime_type : application/msword
[MediaFileTest_23]: title : 1477006699767-AUTO
[MediaFileTest_23]: description : null
[MediaFileTest_23]: _display_name : 1477006699767-AUTO.doc
[MediaFileTest_23]: picasa_id : null
[MediaFileTest_23]: orientation : null
[MediaFileTest_23]: latitude : null
[MediaFileTest_23]: longitude : null
[MediaFileTest_23]: datetaken : null
[MediaFileTest_23]: mini_thumb_magic : null
[MediaFileTest_23]: bucket_id : 1750785639
[MediaFileTest_23]: bucket_display_name : backup
[MediaFileTest_23]: isprivate : null
[MediaFileTest_23]: title_key : null
[MediaFileTest_23]: artist_id : null
[MediaFileTest_23]: album_id : null
[MediaFileTest_23]: composer : null
[MediaFileTest_23]: track : null
[MediaFileTest_23]: year : null
[MediaFileTest_23]: is_ringtone : null
[MediaFileTest_23]: is_music : null
[MediaFileTest_23]: is_alarm : null
[MediaFileTest_23]: is_notification : null
[MediaFileTest_23]: is_podcast : null
[MediaFileTest_23]: album_artist : null
[MediaFileTest_23]: duration : null
[MediaFileTest_23]: bookmark : null
[MediaFileTest_23]: artist : null
[MediaFileTest_23]: album : null
[MediaFileTest_23]: resolution : null
[MediaFileTest_23]: tags : null
[MediaFileTest_23]: category : null
[MediaFileTest_23]: language : null
[MediaFileTest_23]: mini_thumb_data : null
[MediaFileTest_23]: name : null
[MediaFileTest_23]: media_type : 0
[MediaFileTest_23]: old_id : null
[MediaFileTest_23]: storage_id : 65537
[MediaFileTest_23]: is_drm : 0
[MediaFileTest_23]: width : null
[MediaFileTest_23]: height : null
[MediaFileTest_23]: is_sound : 0
[MediaFileTest_23]: year_name : <unknown>
[MediaFileTest_23]: genre_name : <unknown>
[MediaFileTest_23]: recently_played : 0
[MediaFileTest_23]: most_played : 0
[MediaFileTest_23]: recently_added_remove_flag : 0
[MediaFileTest_23]: is_favorite : 0
[MediaFileTest_23]: resumePos : 0
[MediaFileTest_23]: isPlayed : 0
[MediaFileTest_23]: face_count : -1
[MediaFileTest_23]: scan_pri : 0
[MediaFileTest_23]: weather_ID : 0
[MediaFileTest_23]: recordingtype : -1
[MediaFileTest_23]: group_id : 0
[MediaFileTest_23]: city_ID : 0
[MediaFileTest_23]: spherical_mosaic : 0
[MediaFileTest_23]: label_id : 0
[MediaFileTest_23]: is_memo : 0
[MediaFileTest_23]: addr : null
[MediaFileTest_23]: langagecode : null
[MediaFileTest_23]: is_secretbox : 0
[MediaFileTest_23]: sampling_rate : 0
[MediaFileTest_23]: bit_depth : 0
[MediaFileTest_23]: is_360_video : 0
[MediaFileTest_23]: pic_rating : -1
[MediaFileTest_23]: sef_file_type : -1
[MediaFileTest_23]: reusable : 0
[MediaFileTest_23]: recorded_number : null
[MediaFileTest_23]: title_bucket : null
[MediaFileTest_23]: title_label : null
[MediaFileTest_23]: recording_mode : 0
[MediaFileTest_23]: is_ringtone_theme : 0
[MediaFileTest_23]: is_notification_theme : 0
[MediaFileTest_23]: is_alarm_theme : 0
[MediaFileTest_23]: type3dvideo : <unknown>
[MediaFileTest_23]: video_view_mode : -1
[MediaFileTest_23]: video_codec_info : <unknown>
[MediaFileTest_23]: audio_codec_info : <unknown>
[MediaFileTest_23]: sef_file_sub_type : -1
[MediaFileTest_23]: smartcrop_rect : <unknown>

2016년 10월 17일 월요일

[안드로이드] ORM 데이터베이스 Realm 사용하기

Realm

Realm이란 무엇인가?

  • 모바일용 크로스 플랫폼 데이터베이스
  • SQLite 기반의 ORM 프레임워크

시작하기

프로젝트에 Realm 추가

  1. 프로젝트의 build.gradle 파일에 다음과 같이 classpath를 추가한다.
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath "io.realm:realm-gradle-plugin:1.2.0"
    }
}
  1. 앱의 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에 대한 설정정보를 설정한다.
  • 다음 예시처럼 여러가지 설정정보를 변경할 수 있다.
// The RealmConfiguration is created using the builder pattern.
// The Realm file will be located in Context.getFilesDir() with name "myrealm.realm"
RealmConfiguration config = new RealmConfiguration.Builder()
  .name("myrealm.realm")
  .encryptionKey(getKey())
  .schemaVersion(42)
  .modules(new MySchemaModule())
  .migration(new MyMigration())
  .build();
// Use the config
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()) {
    // No need to close the Realm instance manually
}

Models

Models

  • Realm은 기본적으로 RealmObject를 상속받은 class들을 모델로 인식한다.
  • RealmModel 인터페이스를 구현하고, @RealmClass 어노테이션을 붙인 일반 class도 모델로 인식한다.
  • RealmObject를 상속받은 객체는 Realm 프레임워크가 제공하는 인스턴스 메서드들을 그냥 사용하면 되지만, RealmModel를 구현하는 객체는 RealmObject 클래스의 static 메서드를 이용한다.
  • 일반 자바 객체처럼, getter, setter 이외의 일반 메서드도 사용할 수 있다.
// RealmObject 상속
public class User extends RealmObject {

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

//RealmModel 구현
@RealmClass
public class User implements RealmModel {
    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; }
}

// 모델 객체 사용하기
// With RealmObject
user.isValid();
user.addChangeListener(listener);

// With RealmModel
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값 표현이 가능하다.
  • RealmObjectRealmList<? 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 객체 생성
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

속성 인덱싱

  • 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) {
        // This will create a new object in Realm or throw an exception if the
        // object already exists (same primary key)
        // realm.copyToRealm(obj);

        // This will update an existing object with the same primary key
        // or create a new object if an object with no primary key = 42
        realm.copyToRealmOrUpdate(obj);
    }
});

커스터마이징 객체

  • RealmObject는 거의 POJO처럼 이용가능하다.
  • Realm에서 관리되는 객체를 만드려면, createObject()나 copyToRealm() 메서드를 이용해야 한다.

제한

  • finaltransientvolatile 키워드를 지원하지 않는다.
  • RealmObject대신 다른 클래스를 상속받는 클래스는 허용되지 않는다.
  • 기본생성자(인자가 없는 생성자)는 반드시 비어있어야 한다.

Relationship

  • RealmObject는 서로 연결될 수 있다.
  • 일반적인 Join 연산과 다르게, Realm에서는 일반적으로 가벼운 연산에 속한다.
  • has-a 관계이다.

Many - to - 1

  • 메인 RealmObject 서브클래스 모델에, 연결할 RealmObject의 서브클래스를 필드로 추가한다.
public class Email extends RealmObject {
    private String address;
    private boolean active;
    // ... setters and getters left out
}

public class Contact extends RealmObject {
    private String name;
    private Email email;
    // ... setters and getters left out
}
  • 위 예제에서, 하나의 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이 있는 객체를 연결하여 쿼리할 수 있다.
  • 관계가 있는 필드의 속성을 조회하려면 . 구분자를 이용해 경로를 명시한다.
    • 조건을 거는 메서드로는 equalTolessThengreaterThen 등을 이용한다.
관계 예제
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();

Write

  • 읽기 연산은 아무때나 가능하지만 쓰기연산은 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메서드를 이용하면, beginTransactioncommitTransaction / 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 트렌젝션은 백그라운드에서 실행할 필요가 있다.
  • OnSuccessOnError 콜백을 통해 트렌젝션이 완료된 후의 콜백을 받는다.
    • 콜백은 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;
    }
});

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;

    // 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
    }
    ...
}