5일차 [안드로이드] Room
[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) 관련 데이터를 캐시하는 것
- 이렇게 하면 장치가 네트워크에 액세스할 수 없는 경우에도 사용자가 오프라인 상태일 때 해당 콘텐츠를 계속 탐색할 수 있다. 이후에 장치가 다시 온라인 상태가 되면 사용자가 시작한 모든 콘텐츠 변경 내용이 서버에 동기화된다.
[사용해보기]
- gradle에 의존성을 추가한다.
- 먼저, build.gradle 파일의 dependencies에 추가해준다.그래야지 Room을 사용할 수 있다.
- roomVersion은 사용하는 시점에 맞는 버전을 사용하면 된다.
1 | //room |
[3가지 주요 컴포넌트]
Room은 엔티티(Entity), 데이터 접근 객체(Data Access Object), 룸 데이터 베이스(Room Database) 이렇게 총 3가지 구성요소로 나뉜다.
- Database
- 데이터베이스 홀더를 포함하고 앱의 지속적이고 관계가 있는 데이터에 대한 기본 연결을 위한 기본 액세스 지점 역할을 한다.
@Database
로 어노테이션된 클래스는 다음의 조건을 만족해야 한다.- RoomDatabase를 상속받는 클래스는 추상 클래스이어야 한다.
- 어노테이션 내에 데이터베이스와 연관된 (즉, 데이터베이스에 들어갈 테이블) 엔티티의 목록을 포함한다.
- 파라미터가 0개인 추상 메소드를 포함하고 @Dao로 어노테이션된 클래스를 반환한다.
- 런타임에
Room.databaseBuilder() 또는 Room.inMemoryDatabaseBuilder()
를 호출하여 데이터베이스 인스턴스를 얻을 수 있다.
1 | // User and Book are classes annotated with @Entity. |
- 데이터베이스에서 쿼리를 직접 실행하는 대신 Dao 클래스를 만드는 것이 좋다. Dao 클래스를 사용하면 보다 논리적인 계층에서 데이터베이스 통신을 추상화할 수 있다. 이 계층은 직접 sql 쿼리를 실행하는 것에 비해 테스트를 더 쉽게 할 수 있다.
- Room은 애플리케이션이 컴파일 되는 동안 Dao 클래스의 모든 쿼리를 확인하여 쿼리 중 문제가 있는 경우 즉시 사용자에게 알려준다.
- Entity
- 데이터베이스 내에서 테이블을 나타낸다.
- room을 사용할 때 관련 필드 집합을 엔티티들로 정의한다.
- 각 엔티티에 대해 항목(아이템)을 보관하기 위해 연결된 데이터베이스 객체 내에 테이블이 생성된다.
- 데이터베이스 클래스의 엔티티 array를 통해 엔티티 클래스를 참조해야 한다.
1 |
|
- 필드를 유지하려면, 룸은 필드에 접근할 수 있어야 한다.
- 필드를 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 | "firstName", "lastName"}) (primaryKeys = { |
- 기본적으로 룸은 클래스 이름을 데이터베이스 테이블 이름으로 사용한다.
- 만약 테이블이 다른 이름을 가지게 하고 싶다면, @Entity 어노테이션의 tableName 속성을 설정하면 된다.
1 | "users") (tableName = |
주의 : SQLite의 테이블 이름은 대소문자를 구분하지 않는다.
- tableName 속성과 비슷하게 룸은 필드 이름을 데이터베이스의 column 이름으로 사용한다.
- 만약 column 이름을 다르게 하고 싶다면
@ColumnInfo
어노테이션을 추가하면 된다.
1 | "users") (tableName = |
Ignore fields
- 기본적으로 룸은 엔티티에 정의된 각 필드에 대한 Column을 생성한다.
- 엔티티에 지속하고 싶지 않은 필드가 있는 경우
@Ignore
를 사용하여 필드에 어노테이션을 추가할 수 있다.
1 | "users") (tableName = |
- 상위 엔티티에서 필드를 상속하는 경우, 일반적으로 @Entity 특성의 ignoredColumns 속성을 더 쉽게 추가해서 사용할 수 있다.
1 | "picture") (ignoredColumns = |
Provide table search support
추가 예정~~
- 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 |
|
- @Insert 메소드는 매개 변수를 1개만 받으면 삽입된 항목의 새 rowId인 long 타입의 값을 반환할 수 있다. 매개변수가 배열 또는 집합인 경우 Long[] 또는 List
을 대신 반환해야 한다. - @Insert에
onConflict
속성을 지정할 수 있다. 테이블에 Entity를 삽입할 때 같은 값인 경우, 충돌이 발생하는데 이 충돌을 어떻게 해결할지를 정의할 수 있다.- 위에서는 Replace로 지정하여 충돌 발생 시 새로 들어온 데이터로 교체한다.
2. update
update
방법은 테이터베이스에서 매개 변수로 지정된 엔티티 집합을 수정한다. 각 엔티티의 기본 키와 일치하는 조회를 사용한다.
1 |
|
- 일반적으로 필요하진 않지만, update를 사용하면 데이터베이스에서 업데이트된 행 수를 나타내는 int 값을 반환할 받을 수 있다.
3. Delete
Delete
방법은 매개 변수로 지정된 엔티티 집합을 데이터베이스에서 제거한다. 기본키를 사용하여 삭제할 엔티티를 찾는다.
1 |
|
- update와 비슷하게 삭제를 하면 데이터베이스에서 제거된 행 수를 나타내는 int 값을 반환할 수 있다.
4. Query
@Query
는 DAO 클래스에서 사용되는 주석이다.- 데이터베이스에서 읽기 / 쓰기 작업을 수행할 수 있다.
- 각 @Query 메소드는 Compile time에 확인되므로 쿼리에 문제가 있으면 Runtim Error 대신
Compile Error
가 발생한다. - 또한, 룸은 반환된 객체의 필드 이름이 쿼리 응답의 해당 열 이름과 일치하지 않는 경우 룸은 다음 두 가지 방법 중 하나로 경고를 표시한다.
- 일부 필드 이름만 일치하는 경우 경고를 표시한다.
- 필드 이름이 일치하지 않으면 오류가 발생한다.
1 |
|
- 모든 사용자를 load하는 간단한 쿼리이다.
- Compile time에 룸은 사용자 테이블의 모든 Column을 쿼리한다는 것을 알고 있다. 쿼리에 구문 오류가 있거나 사용자 테이블이 데이터베이스에 없는 경우, Room은 앱 컴파일 시 적절한 메시지가 포함된 오류를 표시한다.
5. Passing parameters into the query
- 대부분의 경우 특정 연령보다 나이가 많은 사용자만 표시하는 등의 필터링 작업을 수행하려면 매개 변수를 쿼리에 전달해야 한다.
- 다음에서 확인할 수 있다.
1 |
|
- 이 쿼리를 컴파일할 때, Room은 :minAge bind 매개 변수와 minAge 메소드 매개변수를 일치시킨다. Room은 매개 변수 이름을 사용하여 매치를 수행한다. 일치하지 않는 경우 앱 컴파일 시 오류가 발생한다.
- 다음과 같이 여러 매개변수를 전달하거나 조회에서 여러 번 참조할 수도 있다.
1 |
|
6. Returning subsets of columns
- 대부분의 경우 엔티티의 몇 가지 필드만 있으면 된다.
- 예를 들어 UI는 사용자에 대한 모든 세부 정보가 아니라 사용자의 성과 이름만 표시할 수 있다. 앱의 UI에 표시되는 열만 가져오면 리소스가 절약되고 쿼리가 더 빨리 완료된다.
- Room을 사용하면 결과 Column 집합을 반환될 개체로 매핑할 수 있는 쿼리에서 Java 기반 개체를 반환할 수 있다.
1 | // Java Object |
- Query 메소드에서 이 자바 객체를 사용할 수 있다.
1 |
|
- Room은 쿼리가 first_name, last_name 열에 대한 값을 반환하고 이러한 값을 NameTuple 클래스의 필드에 매핑할 수 있음을 이해한다.
- 따라서 룸에서 적절한 코드를 생성할 수 있다.
- 쿼리가 너무 많은 열을 반환하거나 NameTuple 클래스에 없는 열을 반환하면 룸에 경고가 표시된다.
7. Passing a collection of arguments
- 일부 쿼리는 런타임까지 알 수 없는 정확한 수의 매개 변수를 사용하여 많은 매개 변수를 전달해야 할 수 있다.
- 예를 들어, regions의 하위 집합에서 모든 사용자에 대한 정보를 검색할 수 있다.
- Room은 매개변수가 집합을 나타내는 시점을 파악하고 제공된 매개변수 수에 따라 런타임에 매개변수를 자동으로 확장한다.
1 |
|
8. Observable queries
- 쿼리를 수행하며 데이터가 변경될 때 앱의 UI가 자동으로 업데이트 되는 경우가 많다.
- 이를 수행하기 위해서는 쿼리 메소드 description에 LiveData 유형의 반환 값을 사용한다.
- Room은 데이터베이스가 업데이트될 때 LiveData를 업데이트하는데 필요한 모든 코드를 생성한다.
1 |
|
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 | // User.java |
1 | // UserDao.java |
1 | // AppDatabase.java |
- 위 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 |
|
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 | // 복합 PK 사용시 |
Indices and uniqueness
Indext는 아래와 같이 만들 수 있다.(결합 index도 생성 가능)
1 | "name"), (value = {"last_name","address"})}) (indices = { ( |
- Unique 제약 조건은 아래와 같이 표기할 수 있다.
- 예제) 결합 조건에 대한 unique index
1 | "first_name", "last_name"}, (indices = { (value = { |
- foreignKey도 설정할 수 있다.
1 | (foreignKeys = (entity = User.class, |
Nested objects
Entity 클래스가 field로 object를 갖는 경우 @Embeded
를 사용한다. 단, 해당 table에는 Embeded된 클래스의 column도 똑같은 하나의 column으로 취급된다.
1 | class 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 |
|
@Entity로 정의된 class만 인자로 받거나, 그 class의 collection 또는 array만 인자로 받을 수 있다. 또한, 인자가 하나인 경우 long type의 return(insert된 값의 rowId)을 받을 수 있고, 여러 개인 경우 long[], List
Update
Update를 사용하여 Entity set을 update 한다. return 값으로 변경된 rows 수를 받을 수 있다. update는 PK 기준으로 한다.
1 |
|
Delete
Delete
를 사용하여 Entity set을 delete 한다. return 값으로 변경된 rows 수를 받을 수 있다. 삭제 key는 PK를 기준으로 한다.
1 |
|
Query
@Query
를 사용하여 DB를 조회할 수 있다. compile time에 return되는 object의 field와 sql 결과로 나오는 column의 이름이 맞는지 확인하여 일부가 match되면 warning, match 되는게 없다면 error를 보낸다.
1 |
|
- select문에 parameter가 들어가야 하는 경우 아래와 같이 넣을 수 있다.
1 |
|
- 아래와 같이 여러 개의 parameter도 사용할 수 있다.
1 | public interface MyDao { |
- 만약 일부 컬럼만 조회하고 싶다면 따로 return class를 만들어서 요청할 수 있다.
1 | // return 받을 class를 정의 |
- 또한, 정해지지 않은 개수의 parameter가 넘어가야 하는 경우 아래와 같이 수행 가능하다.
1 |
|
기타 등등의 기능들을 포함하고 있다. 하지만, 아래는 필요에 따라 찾아보면서 기능을 사용하면 될 것 같다.
Using tye converters
Room은 primitive type(원시 타입 ex. int, String 등등)과 그 wrapping 타입만 지원한다. 하지만, 그 외에 type을 사용할 경우 TypeConverter를 사용하여 type을 치환해야 한다.
예를 들어, DB에서는 timestamp로 되어 있고, java code에서는 Date class로 되어 있는 경우 우선 아래와 같이 converter
를 만든다.
1 | public class Converters{ |
- 이 두개의 converting 함수는 서로 converting 해주고 있다.
@TypeConverters
를 이용하여 적용할 곳에 넣는다.
1 | // AppDatabase.java |
- @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 | Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build(); |
migration 코드가 없으면 Room은 DB를 그냥 rebuild한다.
(기존 데이터는 날아간다.)- 또한, migration에 들어가는 query는 상수에 넣지말고, 직접 넣는게 migration 로직을 유지하는데더 좋다.
- migration이 끝나고 나면, schema에 대한 유효성을 확인을 하고, 문제가 있을 경우 mismatch된 부분에 정보를 담은 exception을 발생 시킨다.