Room 라이브러리를 사용하여 앱 데이터를 저장할 때, 저장하려는 객체를 나타내기 위해 엔티티를 정의함.
각 엔티티는 연결된 Room 데이터베이스의 테이블에 해당하며
엔티티의 각 인스턴스는 해당 테이블의 데이터 행을 나타냄.
= Room 엔티티를 사용하면 SQL 코드를 작성하지 않고 데이터베이스 스키마를 정의할 수 있음.
@Entity
@Entity
어노테이션을 사용하여 클래스로 정의함.//예시 1
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val lastName: String?
)
//예시 2
@Entity //User 클래스가 데이터베이스 테이블과 관련이 있음을 나타냄
public class User {
@PrimaryKey(autoGenerate = true) // id 필드를 기본 키로 지정하며, autoGenerate 속성으로 값을 자동으로 증가시킴.
public int id;
@ColumnInfo(name = "first_name") //필드의 열 이름 저장
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
}
기본적으로 Room은 클래스 이름을 데이터베이스 테이블 이름으로 사용함.
테이블에 다른 이름을 지정하려면 @Entity
어노테이션의 "tableName" 속성을 설정하면 됨.
또한 기본적으로 필드 이름을 데이터베이스의 열 이름으로 사용하며 열에 다른 이름을 지정하려면
@ColumnInfo
어노테이션을 필드에 추가하고 "name" 속성을 설정하면 됨.
@Entity(tableName = "users")
data class User (
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
★ SQLite는 테이블 및 열 이름에 대소문자를 구분하지 않음.
즉, "User"와 "user"는 SQLite에서 동일한 테이블 또는 열을 나타냄.
따라서 데이터베이스 스키마를 정의할 때 대소문자 구분에 신경 쓸 필요가 없음.
Room 엔티티는 해당 데이터베이스 테이블의 각 행을 고유하게 식별하는 기본 키를 정의해야 함.
@PrimaryKey val id: Int
여러 열의 조합으로 엔티티의 인스턴스를 고유하게 식별해야 하는 경우 @Entity
의 "primaryKeys" 속성에 해당 열을 나열하여 복합 키 정의 가능.
@Entity(primaryKeys = ["firstName", "lastName"])
data class User(
val firstName: String?,
val lastName: String?
)
Room은 엔티티에서 정의된 각 필드에 대해 열을 생성. 엔티티에 저장하고 싶지 않은 필드가 있을 경우 @Ignore
어노테이션을 사용하여 해당 필드를 표시할 수 있음.
@Entity
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val lastName: String?,
@Ignore val picture: Bitmap?
)
엔티티가 상위 엔티티로부터 필드를 상속하는 경우 @Entity
어노테이션의 "ignoredColumns" 속성을 사용하는 것이 더 간단.
open class User {
var picture: Bitmap? = null
}
@Entity(ignoredColumns = ["picture"])
data class RemoteUser(
@PrimaryKey val id: Int,
val hasVpn: Boolean
) : User()
Room은 데이터베이스 테이블에서 정보를 검색하기 쉽게 하는 여러 유형의 어노테이션을 지원함.
minSdkVersion이 16 미만인 경우를 제외하고 전문 검색(full-text search)을 사용.
// Use `@Fts3` only if your app has strict disk space requirements or if you
// require compatibility with an older SQLite version.
@Fts4
@Entity(tableName = "users")
data class User(
/* Specifying a primary key for an FTS-table-backed entity is optional, but
if you include one, it must use this type and column name. */
@PrimaryKey @ColumnInfo(name = "rowid") val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?
)
앱이 전문 검색(FTS)를 이용해 매우 빠른 데이터베이스 액세스를 필요로 할 경우,
해당 엔티티를 FTS3 또는 FTS4SQLite 확장 모듈을 사용하는 가상 테이블로 백업.
@Fts3
혹은 @Fts4
어노테이션을 추가하면 됨.
💡★ FTS란 긴 문장에서 특정 단어를 찾을 때 개별단어를 색인화하여 빠르게 찾을 수 있도록 하는 기능. (LIKE와 비슷하지만 LIKE는 색인화를 하지 않음.)
💡 FTS 테이블을 지원하는 엔티티에서 기본 키를 정의하는 경우, 기본 키는 반드시 INTEGER 유형이어야 하며, 이름은 rowid여야 함.
테이블이 여러 언어로 컨텐츠를 지원할 경우, 각 행의 언어 정보를 저장하는 열을 지정할 때는
languageId 옵션을 사용.
@Fts4(languageId = "lid")
@Entity(tableName = "users")
data class User(
// ...
@ColumnInfo(name = "lid") val languageId: Int
)
앱이 FTS3 또는 FTS4 테이블을 지원하지 않는 SDK 버전을 지원해야 한다면, 쿼리를 가속하기 위해 데이터베이스의 특정 열에 인덱스를 추가할 수 있음.
@Entity(indices = [Index(value = ["last_name", "address"])])
data class User(
@PrimaryKey val id: Int,
val firstName: String?,
val address: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore val picture: Bitmap?
)
때로는 데이터베이스의 특정 필드 또는 필드 그룹이 고유해야 함.
@Index
어노테이션의 unique 속성을 사용하여 고유성을 설정할 수 있음.
//first_name과 last_name 열의 값 집합이 동일한 두 개의 행이 테이블에 존재하지 않도록 방지
@Entity(indices = [Index(value = ["first_name", "last_name"],
unique = true)])
data class User(
@PrimaryKey val id: Int,
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?,
@Ignore var picture: Bitmap?
)
💡 AutoValue
자바 라이브러리로 자바 개발자들이 Immutable 데이터 클래스를 쉽게 생성하고 유지하는데 도움을 주는 도구. 반복적으로 작성해야 하는 코드를 자동으로 생성 가능.
주요 특징
1. Immutable 클래스를 간단히 생성 가능
2. 자동으로 Getter 메서드를 생성. 따라서 접근 메서드를 직접 생성하지 않아도 됨.
3. equals() 및 hashCode() 메서드를 자동으로 생성해줌.
4. Builder 패턴을 사용하여 객체를 생성할 수 있는 기능도 제공해줌.
5. 자동으로 toString() 메서드를 생성하여 디버깅 및 로깅 작업을 단순화.
import com.google.auto.value.AutoValue;
// Annotate the class with @AutoValue
@AutoValue
public abstract class Person {
// Define properties (fields) of the class
public abstract String name();
public abstract int age();
// Create a static factory method to construct instances
public static Person create(String name, int age) {
return new AutoValue_Person(name, age);
}
}
자동으로 생성된 클래스 코드
final class AutoValue_Person extends Person {
private final String name;
private final int age;
AutoValue_Person(String name, int age) {
if (name == null) {
throw new NullPointerException("Null name");
}
this.name = name;
this.age = age;
}
@Override
public String name() {
return name;
}
@Override
public int age() {
return age;
}
@Override
public boolean equals(Object o) {
if (o == this) {
return true;
}
if (o instanceof Person) {
Person that = (Person) o;
return (this.name.equals(that.name()))
&& (this.age == that.age());
}
return false;
}
@Override
public int hashCode() {
int h = 1;
h *= 1000003;
h ^= this.name.hashCode();
h *= 1000003;
h ^= this.age;
return h;
}
}
public class Main {
public static void main(String[] args) {
Person person = Person.create("John", 30);
System.out.println("Name: " + person.name());
System.out.println("Age: " + person.age());
}
}
Room 2.1.0 이상에서는 앱의 데이터베이스에 엔티티로 사용하기 위해 @AutoValue
어노테이션을 추가한 Java 기반 Immutable 클래스를 사용할 수 있음.
@AutoValue
@Entity
public abstract class User {
// Supported annotations must include `@CopyAnnotations`.
@CopyAnnotations
@PrimaryKey
public abstract long getId();
public abstract String getFirstName();
public abstract String getLastName();
// Room uses this factory method to create User objects.
public static User create(long id, String firstName, String lastName) {
return new AutoValue_User(id, firstName, lastName);
}
}
클래스의 추상메서드에 @PrimaryKey
, @ColumnInfo
, @Embedded
, @Relation
어노테이션을 사용할 수 있음.
이때 Room이 메서드의 자동 생성된 코드를 올바르게 해석할 수 있도록 각 메서드에 @CopyAnnotations
어노테이션을 포함해야 함.
Room 라이브러리를 사용하여 앱 데이터를 저장할 때, 저장된 데이터와 상호 작용하기 위해 데이터 액세스 객체(DAO)를 정의함. 컴파일 시에 Room은 정의한 DAO의 구현을 자동으로 생성함.
쿼리 빌더나 직접 쿼리를 하는 대신 DAO를 사용하여 앱 데이터베이스에 액세스하면 중요한 아키텍처 원칙인 관심사 분리를 유지할 수 있음.
앱을 테스트할 때 데이터베이스 액세스를 모방하기가 더 쉬워짐.
DAO는 인터페이스 또는 추상 클래스로 정의 가능. 일반적으로 인터페이스를 사용.
어느 경우에든 @Dao
를 써줘야 함.
DAO는 속성이 없지만 데이터베이스의 데이터와 상호 작용하기 위한 하나 이상의 메서드를 정의.
@Dao
interface UserDao {
@Insert
fun insertAll(vararg users: User)
@Delete
fun delete(user: User)
@Query("SELECT * FROM user")
fun getAll(): List<User>
}
SQL문을 작성하지 않고도 간단한 삽입, 업데이트 및 삭제 작업을 수행하는 메서드를 정의하기 위한 어노테이션 제공.
더 복잡한 삽입, 업데이트, 삭제 작업을 정의하거나 데이터베이스에서 데이터를 쿼리해야할 경우 쿼리 메서드를 사용해야 함.
@Insert
어노테이션을 사용하여 메서드를 정의하면 해당 메서드가 매개변수를 데이터베이스의 적절한 테이블에 삽입할 수 있음.
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) //기본 키 충돌시 처리 방식 지정
fun insertUsers(vararg users: User)
@Insert
fun insertBothUsers(user1: User, user2: User)
@Insert
fun insertUsersAndFriends(user: User, friends: List<User>)
}
insert 시 메서드는 rowId로 long 값을 반환할 수 있음. (insert하면 rowId 값을 받을 수 있음.)
@Update
어노테이션을 사용하여 데이터베이스 테이블에서 특정 행을 업데이트하는 메서드를 정의할 수 있음. @Insert
와 마찬가지로 데이터 엔티티 인스턴스의 매개변수를 메서드의 각 매개변수로 받음.
@Dao
interface UserDao {
@Update
fun updateUsers(vararg users: User)
}
Room은 전달된 엔티티 인스턴스를 갱신하는데, 데이터베이스에서 매칭시키기 위해 기본키를 사용.
동일한 기본 키를 가진 행이 없을 경우 Room은 아무 변경 작업도 하지 않음.
update 시 메서드는 성공적으로 업데이트된 행 수를 나타내는 int값을 반환할 수 있음.
@Delete
어노테이션을 사용하여 데이터베이스 테이블에서 특정 행을 삭제하는 메서드를 정의할 수 있음. @Delete
또한 매개변수로 데이터 엔티티 인스턴스를 받음.
@Dao
interface UserDao {
@Delete
fun deleteUsers(vararg users: User)
}
Delete도 Update와 마찬가지로 삭제작업 수행 시, 엔티티 인스턴스와 데이터베이스 행을 매칭시키기 위해 기본키를 사용.
동일한 기본 키를 가진 행이 없을 경우 Room은 아무 변경 작업도 하지 않음.
delete 시 메서드는 성공적으로 삭제된 행 수를 나타내는 int값을 반환할 수 있음.
@Query
어노테이션을 사용하여 SQL문을 작성하고 DAO 메서드로 나타낼 수 있음.
앱 데이터베이스에서 데이터를 조회하거나 더 복잡한 삽입, 업데이트 및 삭제를 수행해야할 때 사용함.
☆ 간단한 코드 예시
@Query("SELECT * FROM user")
fun loadAllUsers(): Array<User>
대부분의 경우 쿼리하는 테이블에서 열의 서브셋만 반환해야 함.
예를 들어 UI에서 사용자의 세부정보가 아닌 이름과 성만 표시하는 경우.
리소스를 절약하고 쿼리 실행을 간소화 하기 위해 필요한 필드만 쿼리.
data class NameTuple(
@ColumnInfo(name = "first_name") val firstName: String?,
@ColumnInfo(name = "last_name") val lastName: String?
)
@Query("SELECT first_name, last_name FROM user")
fun loadFullName(): List<NameTuple>
대부분 DAO 메서드는 필터링 작업을 수행할 수 있도록 매개변수를 받아야 함.
Room은 메서드 매개변수를 쿼리에서 사용하는 것을 지원.
@Query("SELECT * FROM user WHERE age > :minAge")
fun loadAllUsersOlderThan(minAge: Int): Array<User>
여러개의 매개변수도 사용 가능
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User>
@Query("SELECT * FROM user WHERE first_name LIKE :search " +
"OR last_name LIKE :search")
fun findUserWithName(search: String): List<User>
런타임 시점에 변수의 수를 알 수 없는 매개변수를 전달해야 하는 DAO 메서드가 있을 수 있음.
@Query("SELECT * FROM user WHERE region IN (:regions)")
fun loadUsersFromRegions(regions: List<String>): List<User>
결과 계산을 위해 여러 테이블에 액세스해야 할 경우 SQL쿼리에서 JOIN절을 사용하여 하나 이상의 테이블 참조.
@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"
)
fun findBooksBorrowedByNameSync(userName: String): List<Book>
조인된 테이블에서 열의 서브셋 집합을 반환할 수 있음.
//사용자 이름과 대출한 책을 반환하는 메서드를 가진 DAO
interface UserBookDao {
@Query(
"SELECT user.name AS userName, book.name AS bookName " +
"FROM user, book " +
"WHERE user.id = book.user_id"
)
fun loadUserAndBookNames(): LiveData<List<UserBook>>
// You can also define this class in a separate file.
data class UserBook(val userName: String?, val bookName: String?)
}
Room 2.4 이상에서 멀티맵을 반환하는 쿼리 메서드를 작성하여 여러 테이블에서 열을 조회할 수 있음.
@Query(
"SELECT * FROM user" +
"JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<User, List<Book>>
쿼리 메서드가 멀티맵을 반환하는 경우, GROUP BY절을 사용하여 필터링할 수 있음.
@Query(
"SELECT * FROM user" +
"JOIN book ON user.id = book.user_id" +
"GROUP BY user.name WHERE COUNT(book.id) >= 3"
)
fun loadUserAndBookNames(): Map<User, List<Book>>
전체 객체를 맵핑할 필요가 없는 경우, @MapInfo
어노테이션에서 keyColumn과 valueColumn을 설정하여 특정 열 사이의 맵핑을 반환할 수 있음.
@MapInfo(keyColumn = "userName", valueColumn = "bookName")
@Query(
"SELECT user.name AS username, book.name AS bookname FROM user" +
"JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<String, List<String>>
Room은 페이징 라이브러리와 통합을 통해 페이지별 쿼리를 지원.
Room 2.3.0-alpha01 이상에서는 DAO가 Paging3과 함께 사용하기 위한 PagingSource 객체를 반환할 수 있음.
@Dao
interface UserDao {
@Query("SELECT * FROM users WHERE label LIKE :query")
fun pagingSource(query: String): PagingSource<Int, User>
}
앱의 로직이 반환된 행에 직접 액세스해야 할 경우, 커서 객체를 반환하는 DAO 메서드를 작성할 수 있음.
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
fun loadRawUsersOlderThan(minAge: Int): Cursor
}
❗️ Column이 존재하거나 어떤 값을 포함하는지를 보장하지 않으므로 Cursor API를 사용하는 것을 권장하지 않음. 쉽게 리팩토링할 수 없을 경우에만 사용.