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월 24일 월요일

[안드로이드] 커스텀 Logger 클래스 만들기

배경

개발을 하다보면 여러가지 이유로 로그를 남기게 되는데, 여기저기 남길 로그들을 real 버전에서 다 지우기란 여간 버거운 일이 아니다. 그리고, 추후에 유지보수할 때 다시 로그를 찍는 것도 매우 비효율적이다. 그래서, 디버그 상태일때만 로그를 남길 수 있도록 메서드를 추가했다.
public class MyActivity extends Activity {
private static final String LOG_TAG = "MyActivity";

...
public static void logVerbose(String msg) {
// isDebuggable() 이라는 메서드로 디버그 모드인지 확인(이부분은 앱마다 알아서...)
if (!AppConfig.isDebuggable()) return;
Log.v(LOG_TAG, msg);
}
...
}
안드로이드 공식문서의 컨벤션대로 상수를 이용해 클래스 이름을 TAG로 이용했는데, 문제는... 저 형식에 맞추기 위해서 로그가 필요한 모든 클래스에 TAG와 메서드를 만드는 사태가 벌어졌다.

커스텀 Logger 클래스

모든 클래스의 로그메서드를 걷어내기 위해, Logger 클래스를 하나 선언하고 다음과 같이 static 메서드로 로그 메서드를 추가했다.
public class Logger {
private static final String LOG_TAG = "앱 이름";
private static final String FORMAT = "[%s]: %s";

public static void logVerbose(String msg) {
if (!AppConfig.isDebuggable()) return;
Log.v(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void logDebug(String msg) {
if (!AppConfig.isDebuggable()) return;
Log.d(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void logInfo(String msg) {
if (!AppConfig.isDebuggable()) return;
Log.i(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void logWarn(String msg) {
if (!AppConfig.isDebuggable()) return;
Log.d(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

public static void logError(String msg) {
if (!AppConfig.isDebuggable()) return;
Log.e(LOG_TAG, String.format(FORMAT, getCallerInfo(), msg));
}

private static String getCallerInfo() {
StackTraceElement[] elements = new Exception().getStackTrace();
String className = elements[2].getClassName();
return className.substring(className.lastIndexOf(".") + 1, className.length()) + "_" + elements[2].getLineNumber();
}
}
이렇게 추출한 로그 메서드에, 로그를 남기는 클래스이름과 실행라인을 얻어오기 위하여 StackTraceElement 배열을 얻어오는 방법을 이용했다. 이 배열은 다음의 2가지 방법으로 얻어올 수 있다.
  • Thread.currentThread().getStackTrace()
  • new Exception().getStackTrace();
첫번째 방법은 현재 스레드에서의 CallStack을 얻는 것이고, 두번째 방법은 익셉션을 생성해서 익셉션의 CallStack을 얻는 것이다. (2개의 CallStack 순서가 미묘하게 다르니 필요한 경우 테스트해보면 좋다.)
StackTraceElement 객체는 스택에 있는 클래스, 메서드 이름을 불러올 수 있으며, 호출된 라인도 가지고 있다. 필요한 정보를 다 로그에 출력해준다.
Tip : 안드로이드 스튜디오는 클래스 바로가기, 라인 바로가기가 존재한다.(맥 기준으로 설명)
클래스 바로가기 : Editor에서 Shift 두번 입력 후, 팝업이 뜨면 원하는 클래스 이름을 입력한다.
라인 바로가기 : Editor에서 찾고자 하는 클래스를 들어간 후에, cmd + l 입력 후, 팝업이 뜨면 원하는 라인을 입력한다.

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