[2020.02.11 기준으로 수정과 동시에 내용을 추가하고 있습니다.]
[2020.03.27 기준으로 글을 새롭게 작성하고 있습니다. 따라서 새로 작성되는 포스팅이 완료되면 해당 포스팅은 삭제될 예정입니다.]
작성중인 글 : Room 개념편
[2020.04.25 기준 홍보 내용 추가]
현재 신입 개발자를 위한 Repository를 운영하고 있습니다. 💻 신입 개발자로서 준비를 하기 위해 지식을 정리하는 공간 👨‍💻이며, 운영체제, 자바, 네트워크, 데이터베이스 등의 내용이 정리되어 있습니다. 계속해서 꾸준히 추가 중이니 한번씩 들러서 봐주시면 감사하겠습니다. 또한, 도움이 된다면 Star를 꾹~ 눌러주시면 저에게 큰 힘이 됩니다 :)

Android Architecture Component 중 하나인 Room에 대해 알아보려고 한다.

[ORM]

Room을 알아보기 전에 ORM이 무엇인지 살펴보자. ORM이란 Object Relational Mapping으로 데이터베이스와 객체 지향 프로그래밍 언어간의 호환되지 않는 데이터를 변환하는 프로그래밍 기법으로 DB 테이블과 매핑되는 객체를 만들고 그 객체에서 DB를 관리하는 것이다.

[Room]

Room은 SQLite의 기능을 최대한 활용하는 동시에 데이터베이스를 원활하게 접근할 수 있도록 SQLite 위에 추상화 계층을 제공 라이브러리라고 생각하면 된다.

  • 적은 양의 정형화된 데이터를 처리하는 애플리케이션은 로컬에서 해당 데이터를 유지함으로써 큰 이점을 얻을 수 있다.
    • ex) 관련 데이터를 캐시하는 것
  • 이렇게 하면 장치가 네트워크에 액세스할 수 없는 경우에도 사용자가 오프라인 상태일 때 해당 콘텐츠를 계속 탐색할 수 있다. 이후에 장치가 다시 온라인 상태가 되면 사용자가 시작한 모든 콘텐츠 변경 내용이 서버에 동기화된다.

[사용해보기]

  1. gradle에 의존성을 추가한다.
  • 먼저, build.gradle 파일의 dependencies에 추가해준다.그래야지 Room을 사용할 수 있다.
  • roomVersion은 사용하는 시점에 맞는 버전을 사용하면 된다.
1
2
3
//room
implementation "android.arch.persistence.room:runtime:$roomVersion"
kapt "android.arch.persistence.room:compiler:$roomVersion"

[3가지 주요 컴포넌트]

Room은 엔티티(Entity), 데이터 접근 객체(Data Access Object), 룸 데이터 베이스(Room Database) 이렇게 총 3가지 구성요소로 나뉜다.

  1. Database
  • 데이터베이스 홀더를 포함하고 앱의 지속적이고 관계가 있는 데이터에 대한 기본 연결을 위한 기본 액세스 지점 역할을 한다.
  • @Database로 어노테이션된 클래스는 다음의 조건을 만족해야 한다.
    • RoomDatabase를 상속받는 클래스는 추상 클래스이어야 한다.
    • 어노테이션 내에 데이터베이스와 연관된 (즉, 데이터베이스에 들어갈 테이블) 엔티티의 목록을 포함한다.
    • 파라미터가 0개인 추상 메소드를 포함하고 @Dao로 어노테이션된 클래스를 반환한다.
  • 런타임에 Room.databaseBuilder() 또는 Room.inMemoryDatabaseBuilder()를 호출하여 데이터베이스 인스턴스를 얻을 수 있다.
1
2
3
4
5
6
7
8
9
10
// User and Book are classes annotated with @Entity.
@Database(version = 1, entities = {User.class, Book.class})
abstract class AppDatabase extends RoomDatabase(){
// BookDao is a class annotated with @Dao.
abstract public BookDao bookDao();
// UserDao is a class annotated with @Dao.
abstract public UserDao userDao();
// UserBookDao is a class annotated with @Dao.
abstract public UserBookDao userBookDao();
}
  • 데이터베이스에서 쿼리를 직접 실행하는 대신 Dao 클래스를 만드는 것이 좋다. Dao 클래스를 사용하면 보다 논리적인 계층에서 데이터베이스 통신을 추상화할 수 있다. 이 계층은 직접 sql 쿼리를 실행하는 것에 비해 테스트를 더 쉽게 할 수 있다.
  • Room은 애플리케이션이 컴파일 되는 동안 Dao 클래스의 모든 쿼리를 확인하여 쿼리 중 문제가 있는 경우 즉시 사용자에게 알려준다.
  1. Entity
  • 데이터베이스 내에서 테이블을 나타낸다.
  • room을 사용할 때 관련 필드 집합을 엔티티들로 정의한다.
  • 각 엔티티에 대해 항목(아이템)을 보관하기 위해 연결된 데이터베이스 객체 내에 테이블이 생성된다.
  • 데이터베이스 클래스의 엔티티 array를 통해 엔티티 클래스를 참조해야 한다.
1
2
3
4
5
6
7
8
@Entity
public class User{
@PrimaryKey
public int id;

public String firstName;
public String lastName;
}
  • 필드를 유지하려면, 룸은 필드에 접근할 수 있어야 한다.
  • 필드를 public으로 만들거나, getter / setter를 제공할 수 있다.
  • getter / setter method를 사용한다면 룸에서 JavaBeans 규칙을 기반으로 한다.[멤버 변수는 private형을 지정하여 선언한다.]

주의

엔티티에는 빈 생성자 (해당 DAO 클래스가 각 지속 필드를 액세스할 수 있는 경우) 또는 매개 변수에 엔티티 필드와 일치하는 유형과 이름이 포함된 생성자가 있을 수 있다. 룸은 일부 필드만 수신하는 생성자와 같이 전체 또는 부분 생성자를 사용할 수도 있다.

User a Primary Key

  • 각 Entity는 최소 하나 이상의 필드를 기본키로 지정해야 한다.
  • 필드가 하나만 있는 경우에도 @PrimaryKey 어노테이션을 사용하여 필드에 주석을 달 필요가 있다.
  • 또한, 룸에서 엔티티에 자동으로 ID를 할당하도록 하려면 @PrimaryKey의 autoGenerate 속성을 설정할 수 있다.(기본은 false. ex: index가 자동으로 증가하는 경우와 같은)
  • 엔티티가 복합 PrimaryKey가 있는 경우 @Entity 어노테이션의 primaryKeys 속성을 사용할 수 있다.
1
2
3
4
5
@Entity(primaryKeys = {"firstName", "lastName"})
public class User{
public String firstName;
public String lastName;
}
  • 기본적으로 룸은 클래스 이름을 데이터베이스 테이블 이름으로 사용한다.
  • 만약 테이블이 다른 이름을 가지게 하고 싶다면, @Entity 어노테이션의 tableName 속성을 설정하면 된다.
1
2
3
4
@Entity(tableName = "users")
public class User{
///
}

주의 : SQLite의 테이블 이름은 대소문자를 구분하지 않는다.

  • tableName 속성과 비슷하게 룸은 필드 이름을 데이터베이스의 column 이름으로 사용한다.
  • 만약 column 이름을 다르게 하고 싶다면 @ColumnInfo 어노테이션을 추가하면 된다.
1
2
3
4
5
6
7
8
9
10
11
@Entity(tableName = "users")
public class User{
@PrimaryKey
public int id;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;
}

Ignore fields

  • 기본적으로 룸은 엔티티에 정의된 각 필드에 대한 Column을 생성한다.
  • 엔티티에 지속하고 싶지 않은 필드가 있는 경우 @Ignore를 사용하여 필드에 어노테이션을 추가할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
@Entity(tableName = "users")
public class User{
@PrimaryKey
public int id;

public String firstName;
public String lastName;

@Ignore
Bitmap picture;

}
  • 상위 엔티티에서 필드를 상속하는 경우, 일반적으로 @Entity 특성의 ignoredColumns 속성을 더 쉽게 추가해서 사용할 수 있다.
1
2
3
4
5
6
7
@Entity(ignoredColumns = "picture")
public class RemoteUser extends User {
@PrimaryKey
public int id;

public boolean hasVpn;
}

Provide table search support

추가 예정~~

  1. DAO(Data Access Object)
  • 데이터베이스에 접근할 수 있는 메소드를 포함하며, 데이터 접근 객체이다.
  • 인터페이스로서 쿼리를 사용하는 메소드를 정의한다.
  • 룸을 사용해 앱의 데이터에 접근하려면 DAO를 사용한다.
  • 각 DAO에는 앱의 데이터베이스에 대한 추상적 액세스를 제공하는 방법이 포함되어 있으므로(interface내에 쿼리와 함께 함수만 정의) 이 Dao 객체들은 룸의 주요 구성요소를 형성한다.
  • 쿼리 builder나 직접적인 쿼리 대신 DAO 클래스를 사용하여 데이터베이스에 접근하여 데이터베이스 구조의 다양한 구성 요소를 분리할 수 있다.
  • 또한, DAO를 사용하면 애플리케이션을 테스트할 때 데이터베이스 접근을 쉽게 할 수 있다.
  • DAO는 인터페이스 또는 추상 클래스일 수 있다.
    • 추상 클래스인 경우 선택적으로 RoomDatabase를 유일한 매개 변수로 사용하는 생성자를 가질 수 있다.
    • Room은 Compile time에 각 DAO 구현을 생성한다.

Define methods for convenience

  • DAO 클래스를 사용하여 나타낼 수 있는 여러가지 편리한 쿼리가 있다.

1. Insert

  • DAO 메소드를 작성하고 @Insert로 어노테이션을 지정할 때, Room은 단일 트랜잭션의 데이터베이스에 모든 매개변수를 삽입하는 구현을 생성한다.
1
2
3
4
5
6
7
8
9
10
11
@Dao
public interface MyDao{
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);

@Insert
public void insertBothUsers(User user1, User user2);

@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}
  • @Insert 메소드는 매개 변수를 1개만 받으면 삽입된 항목의 새 rowId인 long 타입의 값을 반환할 수 있다. 매개변수가 배열 또는 집합인 경우 Long[] 또는 List을 대신 반환해야 한다.
  • @Insert에 onConflict 속성을 지정할 수 있다. 테이블에 Entity를 삽입할 때 같은 값인 경우, 충돌이 발생하는데 이 충돌을 어떻게 해결할지를 정의할 수 있다.
    • 위에서는 Replace로 지정하여 충돌 발생 시 새로 들어온 데이터로 교체한다.

2. update

  • update 방법은 테이터베이스에서 매개 변수로 지정된 엔티티 집합을 수정한다. 각 엔티티의 기본 키와 일치하는 조회를 사용한다.
1
2
3
4
5
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
  • 일반적으로 필요하진 않지만, update를 사용하면 데이터베이스에서 업데이트된 행 수를 나타내는 int 값을 반환할 받을 수 있다.

3. Delete

  • Delete 방법은 매개 변수로 지정된 엔티티 집합을 데이터베이스에서 제거한다. 기본키를 사용하여 삭제할 엔티티를 찾는다.
1
2
3
4
5
@Dao
public interface MyDao{
@Delete
public void deleteusers(User... users);
}
  • update와 비슷하게 삭제를 하면 데이터베이스에서 제거된 행 수를 나타내는 int 값을 반환할 수 있다.

4. Query

  • @Query는 DAO 클래스에서 사용되는 주석이다.
  • 데이터베이스에서 읽기 / 쓰기 작업을 수행할 수 있다.
  • 각 @Query 메소드는 Compile time에 확인되므로 쿼리에 문제가 있으면 Runtim Error 대신 Compile Error가 발생한다.
  • 또한, 룸은 반환된 객체의 필드 이름이 쿼리 응답의 해당 열 이름과 일치하지 않는 경우 룸은 다음 두 가지 방법 중 하나로 경고를 표시한다.
    • 일부 필드 이름만 일치하는 경우 경고를 표시한다.
    • 필드 이름이 일치하지 않으면 오류가 발생한다.
1
2
3
4
5
@Dao
public interface MyDao{
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
  • 모든 사용자를 load하는 간단한 쿼리이다.
  • Compile time에 룸은 사용자 테이블의 모든 Column을 쿼리한다는 것을 알고 있다. 쿼리에 구문 오류가 있거나 사용자 테이블이 데이터베이스에 없는 경우, Room은 앱 컴파일 시 적절한 메시지가 포함된 오류를 표시한다.

5. Passing parameters into the query

  • 대부분의 경우 특정 연령보다 나이가 많은 사용자만 표시하는 등의 필터링 작업을 수행하려면 매개 변수를 쿼리에 전달해야 한다.
  • 다음에서 확인할 수 있다.
1
2
3
4
5
@Dao
public interface MyDao{
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
  • 이 쿼리를 컴파일할 때, Room은 :minAge bind 매개 변수와 minAge 메소드 매개변수를 일치시킨다. Room은 매개 변수 이름을 사용하여 매치를 수행한다. 일치하지 않는 경우 앱 컴파일 시 오류가 발생한다.
  • 다음과 같이 여러 매개변수를 전달하거나 조회에서 여러 번 참조할 수도 있다.
1
2
3
4
5
6
7
8
9
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

@Query("SELECT * FROM user WHERE first_name LIKE :search " +
"OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}

6. Returning subsets of columns

  • 대부분의 경우 엔티티의 몇 가지 필드만 있으면 된다.
  • 예를 들어 UI는 사용자에 대한 모든 세부 정보가 아니라 사용자의 성과 이름만 표시할 수 있다. 앱의 UI에 표시되는 열만 가져오면 리소스가 절약되고 쿼리가 더 빨리 완료된다.
  • Room을 사용하면 결과 Column 집합을 반환될 개체로 매핑할 수 있는 쿼리에서 Java 기반 개체를 반환할 수 있다.
1
2
3
4
5
6
7
8
// Java Object
public class NameTuple {
@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;
}
  • Query 메소드에서 이 자바 객체를 사용할 수 있다.
1
2
3
4
5
@Dao
public interface MyDao{
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
  • Room은 쿼리가 first_name, last_name 열에 대한 값을 반환하고 이러한 값을 NameTuple 클래스의 필드에 매핑할 수 있음을 이해한다.
  • 따라서 룸에서 적절한 코드를 생성할 수 있다.
  • 쿼리가 너무 많은 열을 반환하거나 NameTuple 클래스에 없는 열을 반환하면 룸에 경고가 표시된다.

7. Passing a collection of arguments

  • 일부 쿼리는 런타임까지 알 수 없는 정확한 수의 매개 변수를 사용하여 많은 매개 변수를 전달해야 할 수 있다.
  • 예를 들어, regions의 하위 집합에서 모든 사용자에 대한 정보를 검색할 수 있다.
  • Room은 매개변수가 집합을 나타내는 시점을 파악하고 제공된 매개변수 수에 따라 런타임에 매개변수를 자동으로 확장한다.
1
2
3
4
5
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

8. Observable queries

  • 쿼리를 수행하며 데이터가 변경될 때 앱의 UI가 자동으로 업데이트 되는 경우가 많다.
  • 이를 수행하기 위해서는 쿼리 메소드 description에 LiveData 유형의 반환 값을 사용한다.
  • Room은 데이터베이스가 업데이트될 때 LiveData를 업데이트하는데 필요한 모든 코드를 생성한다.
1
2
3
4
5
@Dao
public interface MyDao{
@Query("SELECT first_name, last_name FROM user WHEHE region IN (:regions)")
public LiveData<List<User>> loadUserFromRegionsSync(List<String> regions);
}

9. Reactive queries with RxJava

추가 예정~~

[연습해보기]

  • Entity : database의 row와 mapping되는 class. 즉, table의 구조를 나타내는데 Database에서 entities 함수를 통해 접근할 수 있다.

@Ignore를 붙이지 않는한 DB에 지속적으로 유지된다.
entity는 empty 생성자나, 부분 field만 갖는 생성자, full field에 대한 생성자 모두를 가질 수 있다.

  • DAO : database를 접근하는 함수들이 정의되는 class or interface이다. @Database로 정의된 class는 내부에 인자가 없고 @Dao annotation이 되어 있는 class를 return하는 abstract 함수를 포함하고 있다.

아래의 예제는 하나의 entity와 하나의 Dao를 갖는 형태이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// User.java
@Entity
public class User{
@PrimaryKey
private int uid;

@ColumnInfo(name = "first_name")
private String firstName;

@ColumnInfo(name = "last_name")
private String lastName;

// 생략
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// UserDao.java
@Dao
public interface UserDao{
@Query("SELECT * FROM user")
List<User> getAll();

@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);

@Query("SELECT * FROM user WHERE first_name LIKE :first AND " + "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);

@Insert void
insertAll(User... users);

@Delete void
delete(User user);
}
1
2
3
4
5
// AppDatabase.java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase{
public abstract UserDao userDao();
}
  • 위 3개의 class가 완성되면 아래와 같이 DB의 instance를 얻을 수 있다.
1
AppDatabase db = Roo.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build();

AppDatabase object를 생성하는 코드는 비용이 많이 들기 때문에 싱글톤으로 구현해야 한다.

Entities

@Database의 annotation에 속성으로 포함된 entitiy는 실제 @Entity annotation을 붙인 class로 만들어야 한다. 이는 각 table로 생성되며, 실제 colume으로 만들고 싶지 않은 field가 있다면 @Ignore를 붙인다.

1
2
3
4
5
6
7
8
9
10
11
@Entity
class User {
@PrimaryKey
public int id;

public String firstName;
public String lastName;

@Ignore
Bitmap picture;
}

Field는 public 형태이어야 하며, private으로 할 경우 getter와 setter를 java beans convention에 따라 만들어야 한다.

Primary Key

column이 하나라도 PK는 지정되어야 한다. @PrimaryKey annotation을 이용하거나 복합 PK의 경우 @Entity 속성으로 primarykeys를 이용한다.

또한, Id를 autogenerate하려면 @PrimaryKey 속성에 autoGenerate 속성(true)을 넣는다.

추가적으로 class 이름은 table 명이 된다. table 명을 바꾸고 싶다면 @Entity 속성으로 tableName을 넣으면 된다.

만약 field 이름을 column으로 쓰고 싶지 않다면 @ColumnInfo로 표기해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 복합 PK 사용시
@Entity(primaryKeys = {"firstName", "lastName"})
class User{
public String firstName;
public String lastName;

@Ignore
Bitmap picture;
}

// 테이블 이름을 직접 지정할 때
@Entity(tableName = "users")
class User{
// 생략
}

// Column을 직접 지정할 때
@Entity(tableName = "users")
class User{
@PrimaryKey
public int uid;

@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}

Indices and uniqueness

Indext는 아래와 같이 만들 수 있다.(결합 index도 생성 가능)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity(indices = {@Index("name"),@Index(value = {"last_name","address"})})
public class Users {
@PrimaryKey
public int uid;


public String firstName;
public String address;


@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}
  • Unique 제약 조건은 아래와 같이 표기할 수 있다.
  • 예제) 결합 조건에 대한 unique index
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
public class Users {
@PrimaryKey
public int uid;


@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name = "last_name")
public String lastName;

@Ignore
Bitmap picture;
}
  • foreignKey도 설정할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_od"))
public class Book {
@PrimaryKey
public int bookId;

public String title;

@ColumnInfo(name = "user_id")
public int useerId;
}

Nested objects

Entity 클래스가 field로 object를 갖는 경우 @Embeded를 사용한다. 단, 해당 table에는 Embeded된 클래스의 column도 똑같은 하나의 column으로 취급된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Address{
public String street;
public String state; public String city;

@ColumnInfo(name = "post_code")
public int postCode;

}

@Entity
class User{
@PrimaryKey
public int id;

public String firstName;

@Embeded
public Address address;
}

즉, 위 예제에서 User table에는 id, firstName, street, state, city, post_cde 컬럼이 존재한다. Embeded 안에서 embeded를 가질 수 있으며, 만약 column 이름이 중복되는 경우 prefix를 사용하여 unique하게 column 이름을 설정한다.

DAO(Data Access Objects)(DAOs)

Dao는 abstract class나 interface가 될 수 있다. RoomDatabase를 인자로 받는 생성자를 만드는 경우에만 abstract class가 될 수 있다. Room은 절대로 main thread에서 query 작업을 하지 않는다. allowMainThreadQueries()를 호출하더라도 불가능하다.
LiveData는 return하는 비동기 query의 경우에는 가능하다.(어차피 background에서 수행되므로) - 무슨 말이지…?

Insert

@Insert로 표기하며, single transaction으로 처리된다.

1
2
3
4
5
6
7
8
9
10
11
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);

@Insert
public void insertBothUser(User user1, User user2);

@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}

@Entity로 정의된 class만 인자로 받거나, 그 class의 collection 또는 array만 인자로 받을 수 있다. 또한, 인자가 하나인 경우 long type의 return(insert된 값의 rowId)을 받을 수 있고, 여러 개인 경우 long[], List을 받을 수 있다.

Update

Update를 사용하여 Entity set을 update 한다. return 값으로 변경된 rows 수를 받을 수 있다. update는 PK 기준으로 한다.

1
2
3
4
5
@Dao
public interface MyDao{
@Update
public void updateUsers(User... user);
}

Delete

Delete를 사용하여 Entity set을 delete 한다. return 값으로 변경된 rows 수를 받을 수 있다. 삭제 key는 PK를 기준으로 한다.

1
2
3
4
5
@Dao
public interface MyDao{
@Delete
public void deleteUsers(User... users);
}

Query

@Query를 사용하여 DB를 조회할 수 있다. compile time에 return되는 object의 field와 sql 결과로 나오는 column의 이름이 맞는지 확인하여 일부가 match되면 warning, match 되는게 없다면 error를 보낸다.

1
2
3
4
5
@Dao
public interface MyDao{
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
  • select문에 parameter가 들어가야 하는 경우 아래와 같이 넣을 수 있다.
1
2
3
4
5
@Dao
public interface MyDao{
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
}
  • 아래와 같이 여러 개의 parameter도 사용할 수 있다.
1
2
3
4
5
6
7
@Dao public interface MyDao { 
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

@Query("SELECT * FROM user WHERE first_name LIKE :search " + "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
}
  • 만약 일부 컬럼만 조회하고 싶다면 따로 return class를 만들어서 요청할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// return 받을 class를 정의
public class NameTuple{
@ColumnInfo(name = "first_name")
public String firstName;

@ColumnInfo(name="last_name")
public String lastName;
}

@Dao
public interface MyDao{
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
  • 또한, 정해지지 않은 개수의 parameter가 넘어가야 하는 경우 아래와 같이 수행 가능하다.
1
2
3
4
5
@Dao 
public interface MyDao {
@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")
public List<NameTuple> loadUsersFromRegions(List<String> regions);
}

기타 등등의 기능들을 포함하고 있다. 하지만, 아래는 필요에 따라 찾아보면서 기능을 사용하면 될 것 같다.

Using tye converters

Room은 primitive type(원시 타입 ex. int, String 등등)과 그 wrapping 타입만 지원한다. 하지만, 그 외에 type을 사용할 경우 TypeConverter를 사용하여 type을 치환해야 한다.

예를 들어, DB에서는 timestamp로 되어 있고, java code에서는 Date class로 되어 있는 경우 우선 아래와 같이 converter를 만든다.

1
2
3
4
5
6
7
8
9
10
11
public class Converters{
@TypeConverter
public static Date fromTimestamp(Long value){
return value == null? null : new Date(value);
}

@TypeConverter
public static Long dateToTimestamp(Date date){
return date == null? null : date.getTime();
}
}
  • 이 두개의 converting 함수는 서로 converting 해주고 있다.
  • @TypeConverters를 이용하여 적용할 곳에 넣는다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// AppDatabase.java
@Database(entities = {User.java}, version= 1)
@TypeConverters({Converters.class})
public abstract class AppDatabase extends RoomDatabase{
public abstract UserDao userDao();
}

// User.java
@Entity
public class User{
...

private Date birthday;
}

// UserDao.java
@Dao
public interface UserDao{
...
@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")
List<User> findUsersBornBetweenDates(Date from, Date to);
}
  • @TypeConverter를 지정하는 위치에 따라 scope이 달라진다.
  • 예제처럼 Database에 넣으면 Dao와 entity 모두 영향을 받는다.
  • Dao나 Entity, POJO에 넣을 수도 있고, Entity의 특정 field, Dao의 특정 method or 특정 parameter에 넣을 수 있다.

Database migration

database migration이 필요한 경우 entity class에 수정 항목을 반영해야 한다. 또한 데이터를 날리지 않기 위해서 mirgration을 할 수 있는 방법을 제공한다.

migration을 등록하면, runtime에 migration을 수행하며 정해놓은 순서대로 migration이 가능하다. migration을 등록할 때는 시작 버전과 끝 버전을 넣어야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();

static final Migration MIGRATION_1_2 = new Migration(1,2){
@Override public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, " + "`name` TEXT, PRIMARY KEY(`id`))");
}
};

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book " + " ADD COLUMN pub_year INTEGER");
}
};
  • migration 코드가 없으면 Room은 DB를 그냥 rebuild한다.(기존 데이터는 날아간다.)
  • 또한, migration에 들어가는 query는 상수에 넣지말고, 직접 넣는게 migration 로직을 유지하는데더 좋다.
  • migration이 끝나고 나면, schema에 대한 유효성을 확인을 하고, 문제가 있을 경우 mismatch된 부분에 정보를 담은 exception을 발생 시킨다.

참고