Room을 활용하여 Local 데이터베이스에 data를 저장해보자
Room 은 SQLite를 추상계층으로 감싸고 있으며, 쉽게 데이터베이스에 접근하여 SQLite를 마구마구 자유롭게 풀파워로 사용할 수 있다.
Room을 사용한다면 만만치 않은 양의 구조화된 데이터를 영구적으로 저장하고 처리하는 애플리케이션도 그뤠잇 하게 이득을 볼수 있는 부분이 있다.
대부분의 경우는 관련 데이터의 조각들을 캐시화 하는것이였다. 이 방법에서는 기기가 네트워크에 접속할 수가 없을때 유져가 여전히 오프라인동안 콘텐츠를 둘러볼수 있었다.
사용자가 작성하기 시작한 모든 컨텐츠의 변경사항은 기기가 온라인으로 다시 접속되면 동기화가 이루어 진다.
왜냐면 Room은 이러한 부분까지 다 케어 해주니까!(찡긋)
구글은 그래서 Room의 사용을 강추 하고 있습니다. SQLite쓰는거 대신에 말이죵.
어쨌거나, SQLite API를 직접적으로 사용하길 원한다면 말리지는 않습니다.
Room을 사용하기 위해서는 3가지 주요한 구성요소가 있습니다.
- Database : 데이터베이스 홀더를 포함하고, 관계형 데이터 베이스에 접근할 수 있는 액세스 포인트를 제공한다.
@Database라는 애노테이션을 클래스에 달아야 하며, 다음과 같은 조건을 만족해야한다.- RoomDatabase를 상속해야한 abstract class여야 한다.
- 데이터베이스와 관련된 엔티티들을 애노테이션의 인자값으로 포함해야한다.
- abstract method 포함해야하는데, 이 메소드에는 인자가 0개이고 return 되는클래스가 @Dao 애노테이션을 달고 있어야한다.
런타임때에는 Room.databaseBuilder() 또는 Room.inMemoryDatabaseBuilder()를 통해 Database의 객체를 얻어 낼 수 있다.
- Entity : 데이터베이스의 테이블을 표현한다
- DAO : 데이터베이스에 접속하기 위한 메소드를 포함한다.
위의 구성요소와 다른 앱들과의 관계는 아래의 그림에서 확인 할 수 있습니다.
하나의 엔티티와 하나의 DAO를 갖는 데이터베이스 환경설정에 대한 샘플 코드는 다음과 같습니다.
User.java
@Entity public class User { @PrimaryKey private int uid; @ColumnInfo(name = "first_name") private String firstName; @ColumnInfo(name = "last_name") private String lastName; // 중요: getter와 setter는 간략하게 작성하기 위해 여기서는 생략되었지만 Room과 함께 쓰이기 위해서는 반드시 포함하여야 합니다. } |
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); } |
AppDatabase.java
@Database(entities = {User.class}, version = 1) public abstract class AppDatabase extends RoomDatabase { public abstract UserDao userDao(); } |
위와 같은 클래스 파일을 만들고 난뒤에 아래의 코드를 통해 데이터 베이스를 생성할 수 있습니다.
AppDatabase db = Room.databaseBuilder(getApplicationContext(), AppDatabase.class, "database-name").build(); |
Note : RoomDatabase객체를 인스턴스화 하는 비용은 매우 크므로 개발자는 이 AppDatabase객체를 얻는 작업을 싱글톤패턴으로 만들기를 권장합니다. |
---|
Room Entity를 사용하여 데이터 구조 정의하기
Room 사용시 Entity를 사전에 정의해야하는데, 각가의 엔티티 별로 데이터베이스에서는 테이블이 생성되어 아이템들을 보관할 수 있게 됩니다.
기본적으로 Room은 Entity에 정의된 필드에맞춰 컬럼을 구성하게 됩니다. 만약 Entity를 위해 작성된 Data 클래스에서 어떠한 변수를 선언했고 이것이 데이터베이스에서 컬럼으로 생성되기를 원치 않는다면 @Ignore 애노테이션을 이용하면 됩니다.
샘플 코드는 다음과 같습니다.
@Entity public class User { @PrimaryKey public int id; public String firstName; public String lastName; @Ignore Bitmap picture; } |
Room이 필드에 접근하기 위해서 반드시 public 또는 getter setter를 만들어야 합니다. 만약 getter, setter 메소드를 사용한다면 명심해야할 것이 하나 있습니다. getter setter는 JavaBeans컨벤션을 기반으로 합니다.
Note:Entity들은 빈 생성자(해당 DAO 클래스가 각 필드에 액세스 할 수 있는경우) 또는 매개변수가 엔티티의 필드와 유형 및 이름이 일치하는 생성자를 포함 할 수 있습니다. Room은 또한 전체 또는 몇몇 필드만을 매개변수로 받는 생성자를 사용할 수 있습니다. |
---|
기본키 사용하기
각 엔티티는 반드시 한개의 필드를 기본키로 정의 해야합니다. 심지어 필드가 한개밖에 없더라도 반드시 @PrimaryKey 애노테이션을 붙여서 기본키로 정의를 해야 합니다. 또한 만약에 ID와 같은 기본키값을 자동으로 지정하고 싶다면, 예를 들면 Auto Increment같은.., @PrimaryKey의 속성값으로 autoGenerate를 true로 지정해주면 됩니다.
코드를 보도록하죠.
@Entity(primaryKeys = {"firstName", "lastName"}) public class User { public String firstName; public String lastName; @Ignore Bitmap picture; } |
기본적으로 Room은 클래스이름을 데이터베이스의 테이블명으로 사용합니다. 근데 만약에 다른 이름을 쓰고 싶다면 아래와 같이 tableName속성을 지정하면됩니다.
@Entity(tableName = "users") public class User { ... } |
Caution:SQLite에서는 대소문자를 구분하지 않습니다. |
---|
tableName과 비슷하게, Room 은 필드명 또한 다르게 지정할 수 있습니다. @ColumnInfo 애노테이션을 이용해서 말이죠. 아래와 같이 사용하면 됩니다.
@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 Bitmap picture; } |
인덱스와 고유성에 대해 주석달기(Annotate indices and uniqueness)
어떻게 data에 접근하냐에 따라, 쿼리의 속도를 높이기 위해 어떤필드를 인덱스하고 싶을지도 모른다. @Entity애노테이션의 속성으로 indices를 사용한다면 엔티티를 인덱스 할 수 있다. column이름의 목록을 적기만 하면된다. 샘플 코드를 확인해보자
@Entity(indices = {@Index("firstName"), @Index(value = {"last_name", "address"})}) public class User { @PrimaryKey public int id; public String firstName; public String address; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; } |
때로는 어떤 필드나 필드의 그룹을 고유하게 만들어야 할 때가 있다. 강제로 고유성을 부여할수도 있는데 바로 unique 속성이다. 인덱스 애노테이션과 같이 쓰이며 true로 지정하기만 하면된다. 다음 아래에 나오는 코드에서는 firstName과 lastName컬럼에 대하여 테이블이 두개 이상의 같은 행이 기록되지 않도록 방지 하고 있다.
@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)}) public class User { @PrimaryKey public int id; @ColumnInfo(name = "first_name") public String firstName; @ColumnInfo(name = "last_name") public String lastName; @Ignore Bitmap picture; } |
객체간의 관계 정의 하기
SQLite는 관계형 데이터베이스이기 때문에 객체간의 관계도 지정가능하다. 비록 대부분의 관계형 오브잭트 매핑 라이브러리들은 엔티티가 다른 것들을 참조하도록 하지만 Room 분명하게 이것을 금지하고 있다.
이에 대한 이유를 알고 싶다면 링크를 참조하도록 하자.
비록 직접적으로 관계를 맺을수는 없지만, Room은 여전히 엔티티간에 외부키를 정의하는것을 허용하고 있다.
예를들면 Book이라는 엔티티가 있고 User라는 엔티티와 관계를 맺고 싶다면 @ForeignKey 애노테이션을 통해 아래와같이 정의 할 수 있다.
@Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "id", childColumns = "user_id")) public class Book { @PrimaryKey public int bookId; public String title; @ColumnInfo(name = "user_id") public int userId; } |
외부키는 매우 강력하다. 참조한 엔티티가 업데이트 될때 어떤일이 발생했는지 명시하는것을 허용하고 있다.
예를들면 @ForeignKey(onDelete=CASCADE) 애노테이션을 가지고 있는 어떤 User객체가 삭제된다면 모든 해당 유져의 모든 책을 삭제해라 라는 명령을 SQLite에게 줄수도 있다.
Note:SQLite는 @Insert(onConflict = REPLACE)를 하나의 UPDATE 연산자 대신에 REMOVE 와 REPLACE 연산자들의 묶음으로 다룬다. 중복교체를 하는 이 메소드는 외부키 제약사항에 영향을 출수도 있다. 자세한 사항은 SQLite Documentation의 ON_CONFLICT 항목을 확인하도록 하자. |
---|
또 다른 엔티티를 내포하는 오브젝트 만들기(Create nested objects)
때로는 여러 필드를 포함하고 있는 어떠한 객체를 POJO나 Entity를 통해 표현하고 싶을때가 있다. 이런경우에는 @Embeded 애노테이션을 사용하여 테이블 내의 하위 필드로 분해 할 객체를 나타낼 수 있습니다. 그런 다음 다른 각가의 컬럼과 마찬가지로 포함된 필드를 쿼리 할 수 있다. 말이 어려운데 그냥 예제를 보는편이 더 쉬울때도 있다.
public class Address { public String street; public String state; public String city; @ColumnInfo(name = "post_code") public int postCode; } @Entity public class User { @PrimaryKey public int id; public String firstName; @Embedded public Address address; } |
결과적으로 User 테이블은 id, firstName, street, state, city, post_code를 다 포함하여서 표현한다.
Note:Embeded 필드는 또 다른 Embeded필드를 포함할 수 있다. |
---|
만약 엔티티가 같은자료형의 여러 임베디드 필드를 갖는다면, prefix속성을 통해 각 컬럼이 고유하도록 할 수 있다. 그런 다음 제공된 값을 포함 된 개체의 각 컬럼 이름 시작 부분에 추가합니다.
Room의 DAO들을 이용하여 data에 접근하기
Room을 통해 database에 있는 데이터를 사용하기 위해서는 접근하려면 DAO가 필요하다. DAO는 데이터베이스에 추상적인 접근을 제공하는 메소드들을 포함한다.
질의를 만드는 빌더나 직접적인 쿼리를 작성하는 것 대신, DAO클래스를 사용하여 데이터베이스에 접근하는것은 데이터베이스구조의 구성요소를 분리 한다. 더나아가 DAO는 쉽게 데이터베이스를 모킹(Mock)하여 애플리케이션을 테스트하기 쉽게 한다.
하나의 DAO는 interface나 abstract class가 되야 한다. 만약 abstract class로 만들었따면 선택적으로 RoomDatabase객체를 생성자의 매개변수로 가질수 있으며. Room 은 각 DAO의 구현을 컴파일 시간에 생성해낸다.
Note:Room은 메인쓰레드에서의 데이터베이스 접근을 허용하지 않는다. 허용하고 싶다면 데이터베이스를 생성하는 빌더에서 allowMainThreadQueries()를 호출해야한다. 허용하지 않는 이유는 데이터를 받아오는 작업이 길어질 경우 UI가 장시간 멈춰버릴수 있기 때문이다. 그래서 보통 비동기 쿼리를 하게 되는데 반환값으로는 LiveData 또는 RxJava의 Flowable이 될 수도 있다. |
---|
편의성을 위한 메소드 정의하기
DAO를 이용한 여러 편리한 쿼리들이 있는데 몇몇 가지 예만 다루어 보도록 하겠다.
Insert
DAO메소드를 만들때 @Insert를 달아줄수 있다. Room은 이와 관련된 코드를 생성해내고 모든 파라미터를 하나의 트랜잭션내에서 삽입(insert)하게 된다.
코드를 확인해보자
@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 메소드가 하나의 파라미터만 받는다면, 삽입한 데이터에 대한 long형 rowId를 리턴 받을 수 있다. 만약 파라미터가 여러개라면 long[] 또는 List<Long>으로 대신 리턴 받을 수 있다.
Update
Update를 통해 주어진 파라미터로부터 여러 엔티티들을 수정할 수 있다. 이는 각 엔티티의 기본키에 대해 일치하는 경우 사용된다.
@Dao public interface MyDao { @Update public void updateUsers(User... users); } |
보통 필요하지는 않지만, 이 메소드의 대한 반환형은 Int인데, 반환값은 수정된 행의 갯수를 알려준다.
Delete
주어진 파라미터로 부터 엔티티들을 지워주는 메소드. 엔티티를 찾아 삭제하기 위해서 기본키를 사용한다.
@Dao public interface MyDao { @Delete public void deleteUsers(User... users); } |
이 메소드 또한 몇개의 행이 지워졌는지 int형 반환값으로 알려준다.
Query해보기
@Query 는 DAO 에서 주요한 애노테이션이다. 읽기/쓰기를 이 애노테이션으로 모두 가능하다. 각 @Query 메소드는 컴파일 시간에 알맞은 쿼리 인지 입증하게 되고 문제가 있을시에는 컴파일 에러가 발생한다.
Room은 또한 쿼리에 대한 반환값을 확인한다. 반환되는 객체의 필드의 이름이 만약에 대응되는 컬럼이름이 질의응답에서 일치하지 않는다면 Room은 다음과 같이 두가지 방법중 하나로 알림을 줄것이다.
- 몇몇의 필드명만 일치하는 경우에는 경고 발생
- 일치하지 않는 필드명이 있을시 에러 발생
간단한 쿼리
@Dao public interface MyDao { @Query("SELECT * FROM user") public User[] loadAllUsers(); } |
매우 간단한 쿼리로 모든 사용자 목록을 불러 올수 있다. 컴파일시간에 Room은 User테이블에 있는 모든 컬럼을 쿼리하는 것을 알게 된다. 쿼리가 문법 오류를 포함하고 있거나 user 테이블이 존재하지 않는다면 Room은 적당한 에러메시지를 컴파일 시간에 알려줍니다.
쿼리에 파라미터 넘기기
대부분의 경우 쿼리에 파라미터를 넘겨 filter를 하고 싶을때가 있습니다. 예를들면 사용자를 쿼리 하는데 특정 숫자보다 나이가 많은 사람을 표현한다거나 할때 말이죠. 이러한 작업을 수행하기 위해서는 메소드에 인자값을 애노테이션에서 이용해야 합니다.
@Dao public interface MyDao { @Query("SELECT * FROM user WHERE age > :minAge") public User[] loadAllUsersOlderThan(int minAge); } |
쿼리가 컴파일 시간이 처리될때, Room은 바인드 인자값인 :minAge를 메소드의 매개변수인 minAge와 일치 시킵니다.
만약 일치 하지 않는다면 컴파일 시간에 에러를 발생시킵니다.
복수개의 파라미터를 사용할 수도 있습니다.
@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); } |
컬럼의 부분집합 반환하기
개발자는 대부분 몇몇 필드만 엔티티로 부터 얻으려고 할겁니다. 예를 들면 사용자의 모든정보를 다 보여주기보다는 성이나 이름같은 정보만 말이죠. 앱내의 UI에서 몇몇의 컬럼만 가져오는 것만으로 리소스사용을 줄일수 있습니다. 그리고 쿼리 하는 시간도 단축되겠죠.
Room은 Query할때 반환값이 컬럼들의 부분집합인 이상 어떠한 자바기반의 오브젝트도 리턴할 수 있도록 허용하고 있습니다. 예를들면 POJO를 만들고 사용자의 성과 이름만 받는 클래스를 만들수도 있습니다.
public class NameTuple { @ColumnInfo(name="first_name") public String firstName; @ColumnInfo(name="last_name") public String lastName; } |
위처럼 POJO를 만들고, 아래처럼 쿼리를 합니다.
@Dao public interface MyDao { @Query("SELECT first_name, last_name FROM user") public List<NameTuple> loadFullName(); } |
Room은 @ColumnInfo에 필드명만 적어줘도 알아서 척척 매핑이 됩니다.
Collection인자 넘기기
몇몇 쿼리들은 런타임까지는 정확한 파라미터갯수는 모르지만 다양한 갯수의 파라미터를 필요로 하는경우가 있습니다. 예를들면, 특정지역들의 부분집합으로부터 모든 사용자에 대한 정보를 필요로 하는 경우를 생각해봅시다. Room은 똑똑하게도 런타임시에 이러한 collection파라미터 사이즈에 맞추어 파라미터 갯수를 자동으로 확장시킵니다.
@Dao public interface MyDao { @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)") public List<NameTuple> loadUsersFromRegions(List<String> regions); } |
Observable 쿼리
쿼리를 요청할때 보통 데이터의 변경에 따라 APP의 UI도 같이 자동으로 갱신되길 원할겁니다. 이것을 하려면 LiveData를 리턴값으로 같는 쿼리를 메소드에 정의해줘야 합니다. Room은 database가 업데이트 됨에 따라 LiveData의 data도 변경될수 있도록 코드를 자동으로 생성할 것입니다.
@Dao public interface MyDao { @Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)") public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions); } |
RxJava와 함께 하는 반응형 쿼리
Room 은 RxJava2의 Publisher나 Flowable 타입으로 리턴값을 가질수 있습니다. 이 기능들을 사용하기 위해서는 android.arch.persistence.room:rxjava2 아티팩트를 Room Group에 의존성을 추가해줘야 합니다.
@Dao public interface MyDao { @Query("SELECT * from user where id = :id LIMIT 1") public Flowable<User> loadUserById(int id); } |
Cursor를 통한 직접적인 접근
만약 직접적인 접근이 필요하다면 Cursor객체를 사용할수 있습니다.
@Dao public interface MyDao { @Query("SELECT * FROM user WHERE age > :minAge LIMIT 5") public Cursor loadRawUsersOlderThan(int minAge); } |
Caution: Cursor API를 사용하는것은 별로 추천하지 않는 방법입니다. 행들이 존재하는지 어떤값이 행에 포함된것인지 보장하지 않습니다. 이미 cursor와 관련된 만들어진 코드가 있거나 리팩토링이 힘든 경우가 사용하시길 바랍니다. |
---|
다중 테이블 쿼리하기
몇몇 쿼리들은 여러 테이블들에 접근하여 계산된 결과를 필요로 한다. Room은 테이블을 join하여 쿼리 작성하는것을 허용하고 있다. Flowable이나 LiveData같은 Observable 데이터 타입으로 반환된다면 Room은 쿼리에서 무효성을 위해 연관된 모든 테이블을 감지합니다.
다음 보여주는 코드 스니펫은 어떻게 테이블이 join하여 통합되는지 보여줍니다. 두 테이블은 책을 빌리는 사용자를 포함한 테이블과 현재 대출중인 책을 포함하는 테이블을 보여주고 있습니다.
@Dao public interface MyDao { @Query("SELECT * FROM book " + "INNER JOIN loan ON loan.book_id = book.id " + "INNER JOIN user ON user.id = loan.user_id " + "WHERE user.name LIKE :userName") public List<Book> findBooksBorrowedByNameSync(String userName); } |
이러한 쿼리들로 부터 POJO를 반환할수도 있습니다. 예를들면 사용자와 애완동물의 이름을 불러오는 쿼리를 작성하는것은 다음과 같습니다.
@Dao public interface MyDao { @Query("SELECT user.name AS userName, pet.name AS petName " + "FROM user, pet " + "WHERE user.id = pet.user_id") public LiveData<List<UserPet>> loadUserAndPetNames(); // You can also define this class in a separate file, as long as you add the // "public" access modifier. static class UserPet { public String userName; public String petName; } } |
0개의 댓글