Room. Entity

У цьому уроці детальніше розглянемо можливості Entity. Як задати ім'я таблиці. Як задати ім'я або тип поля. Як створити складений або зовнішній ключ. Як створити індекс. Як використовувати вкладені об'єкти.

Ім'я таблиці

Entity клас використовується для створення таблиці. За замовчуванням як ім'я таблиці використовується ім'я цього класу. Але ми можемо вказати своє ім'я, використовуючи параметр tableName.

@Entity(tableName = "employees")
public class Employee {
 
   // ...
 
}

Для зберігання об'єктів Employee буде створено таблицю з іменем employees.

Ім'я поля

За замовчуванням як ім'я полів у таблиці використовуються імена полів Entity класу. Але ми можемо вказати своє ім'я, використовуючи параметр name в анотації ColumnInfo.

@Entity()
public class Employee {
 
   @PrimaryKey()
   public long id;
 
   @ColumnInfo(name = "full_name")
   public String fullName;
 
   public int salary;
 
}

Для fullName у таблиці буде створено поле з ім'ям full_name.

Тип поля

За замовчуванням Room визначає тип даних для поля в таблиці за типом даних поля в Entity класі. Але ми можемо явно вказати свій тип.

@Entity()
public class Employee {
 
   @PrimaryKey(autoGenerate = true)
   public long id;
 
   public String name;
 
   @ColumnInfo(typeAffinity = TEXT)
   public int salary;
 
}

У таблиці поле salary буде з типом TEXT.

Модифікатори доступу

Щоб Room міг дістатися до полів класу Entity, ми робимо їх public.

@Entity
public class Employee {
 
   @PrimaryKey
   public long id;
 
   public String name;
 
   public int salary;
 
}

Але є можливість використовувати private поля. Для цього треба додати set/get методи.

@Entity
public class Employee {
 
   @PrimaryKey
   private long id;
   private String name;
   private int salary;
 
   public long getId() {
       return id;
   }
 
   public void setId(long id) {
       this.id = id;
   }
 
   public String getName() {
       return name;
   }
 
   public void setName(String name) {
       this.name = name;
   }
 
   public int getSalary() {
       return salary;
   }
 
   public void setSalary(int salary) {
       this.salary = salary;
   }
}

Усі поля - private. Але кожне має set/get методи.

В Android Studio ці методи додаються парою кліків. Тиснете в коді ALT+INSERT, вибираєте пункт Getter and Setter, потім вибираєте поля, для яких треба створити методи.

Замість set-методів ми також можемо використовувати конструктор.

@Entity
public class Employee {
 
   @PrimaryKey
   private long id;
 
   public String name;
 
   public int salary;
 
    public Employee(long id) {
       this.id = id;
   }
 
   public long getId() {
       return id;
   }
}

Поле id тут - private і має get-метод. А замість set-методу, Room буде використовувати конструктор.

Параметр конструктора повинен мати той самий тип та ім'я, що й поле Entity класу. Ви можете використовувати конструктор для всіх полів або тільки для деяких, як у прикладі вище.

Я для спрощення прикладів скрізь буду використовувати public поля.

Первинний ключ

Ми вже знаємо, як за допомогою @PrimaryKey призначити будь-яке поле ключем. Кожен Entity клас повинен містити хоча б одне таке поле. Навіть якщо в класі всього одне поле.

У PrimaryKey є параметр autoGenerate. Він дає змогу ввімкнути для поля режим autoincrement, у якому база даних сама генеруватиме значення, якщо ви його не вкажете.

@PrimaryKey(autoGenerate = true)
public long id;

Тепер під час створення Entity об'єкта ви можете не заповнювати поле id. База сама знайде найближче вільне значення і використає його.

Щоб створити складений ключ, використовуйте параметр primaryKeys.

@Entity(primaryKeys = {"key1", "key2"})
public class Item {
 
   public long key1;
   public long key2;
 
   // ...
 
}

Зовнішній ключ Зовнішні ключі дають змогу пов'язувати таблиці між собою.

У вище розглянутих прикладах у нас є клас Employee для зберігання даних по співробітниках. Давайте створимо клас Car для зберігання даних по машинах. І кожна машина має бути прикріплена до якогось співробітника.

@Entity(foreignKeys = @ForeignKey(entity = Employee.class, parentColumns = "id", childColumns = "employee_id"))
public class Car {
 
   @PrimaryKey(autoGenerate = true)
   public long id;
 
   public String model;
 
   public int year;
 
   @ColumnInfo(name = "employee_id")
   public long employeeId;
 
}

У полі employee_id зберігатиметься id співробітника, до якого прикріплена ця машина.

Використовуємо параметр foreignKeys для створення зовнішнього ключа. Вказуємо, що значення поля employee_id (параметр childColumns) повинно обов'язково дорівнювати будь-якому значенню поля id (параметр parentColumns) у таблиці співробітників Employee (параметр entity).

Тобто якщо у нас є три співробітники з id 1,2 і 3, ми не зможемо додати в базу даних машину з employee_id = 4. Тому що в базі немає такого батьківського ключа, тобто співробітника з id = 4.

Або, якщо ви спробуєте видалити батьківський ключ, тобто співробітника, до якого прикріплена будь-яка машина, то база видасть вам помилку. Тому що після видалення співробітника, у машини в полі employee_id буде знаходитися значення, якого немає в полі id таблиці співробітників.

Для подібних випадків видалення або зміни батьківського ключа, ви можете налаштувати поведінку бази даних. За замовчуванням вона повертає помилку, але це можна змінити за допомогою параметрів onDelete і onUpdate в анотації ForeignKey.

Додамо параметр onDelete

@Entity(foreignKeys = @ForeignKey(entity = Employee.class, parentColumns = "id", childColumns = "employee_id", onDelete = CASCADE))

Його значення = CASCADE. Це означає, що при видаленні батьківського ключа, будуть видалені пов'язані з ним дочірні ключі. Тобто при видаленні співробітника, видалиться і його машина.

Список можливих значень для параметра onDelete можна подивитися тут.

Ще один параметр анотації ForeignKey - це deferred, що має за замовчуванням значення false. Якщо задати цьому параметру значення true, то зовнішній ключ стане відкладеним. Це може бути корисно під час вставки даних у різні таблиці в рамках однієї транзакції. Ви зможете внести всі необхідні зміни, і перевірку на коректність зовнішніх ключів буде виконано наприкінці, під час виконання commit.

Індекс

Індекси можуть підвищити продуктивність вашої таблиці.

В анотації Entity є параметр indicies, який дає змогу задавати індекси.

@Entity(indices = {
               @Index("salary"),
               @Index(value = {"first_name", "last_name"})
           }
       )
public class Employee {
 
   @PrimaryKey(autoGenerate = true)
   public long id;
 
   @ColumnInfo(name = "first_name")
   public String firstName;
 
   @ColumnInfo(name = "last_name")
   public String lastName;
 
   public int salary;
 
}

Створюємо два індекси: один за полем salary, а інший за двома полями first_name і last_name.

Індекс дає можливість встановити для його полів перевірку на унікальність. Це робиться параметром unique = true.

@Entity(indices = {@Index(value = {"first_name", "last_name"}, unique = true)})

У цьому разі база стежитиме, щоб у цій таблиці не було запису з повторюваною парою значень first_name і last_name.

Індекс для одного поля також може бути налаштований через параметр index анотації ColumnInfo

@Entity()
public class Employee {
 
   @PrimaryKey(autoGenerate = true)
   public long id;
 
   public String name;
 
   @ColumnInfo(index = true)
   public int salary;
 
}

Буде створено індекс для поля salary.

Вкладені об'єкти

Нехай у нас є клас Address, з даними про адресу. Це звичайний клас, не Entity.

public class Address {
 
   public String city;
   public String street;
   public int number;
 
}

І ми хочемо використовувати його в Entity класі Employee

@Entity()
public class Employee {
 
   @PrimaryKey(autoGenerate = true)
   public long id;
 
   public String name;
 
   public int salary;
 
   public Address address;
 
}

Якщо ми зробимо так, то Room буде лаятися, оскільки він не знає, як зберегти такий об'єкт у базу: Cannot figure out how to save this field into database. Можна розглянути питання про додавання для нього конвертера типів.

Але є просте рішення - використовувати анотацію Embedded.

@Entity()
public class Employee {
 
   @PrimaryKey(autoGenerate = true)
   public long id;
 
   public String name;
 
   public int salary;
 
   @Embedded
   public Address address;
 
}

Embedded підкаже Room, що треба просто взяти поля з Address і вважати їх полями таблиці Employee.

Тобто в базі буде створено таблицю Employee з полями id, name, salary, city, street, number.

Додавання нового запису матиме такий вигляд:

Employee employee = new Employee();
employee.id = 1;
employee.name = "John Smith";
employee.salary = 10000;
Address address = new Address();
address.city = "London";
address.street = "Baker Street";
address.number = 221;
employee.address = address;
 
db.employeeDao().insert(employee);

Ми створюємо вкладений об'єкт Address, але Room розбереться, і запише все в таблицю, як плоску структуру.

Embedded об'єкти можуть містити в собі інші Embedded об'єкти.

Якщо у вас виходить так, що збігаються імена якихось полів в основному об'єкті і в Embedded об'єкті, то використовуйте префікс для Embedded об'єкта.

@Embedded(prefix = "address")
public Address address;

У цьому разі до імен полів Embedded об'єкта в таблиці буде додано зазначений префікс.

Ignore

Анотація Ignore дає змогу підказати Room, що це поле не повинно записуватися в базу або читатися з неї.

@Entity
public class Employee {
 
   @PrimaryKey
   public long id;
 
   public String name;
 
   public int salary;
 
   @Ignore
   public Bitmap avatar;
 
}

Нам не потрібно зберігати Bitmap у базі, тому додаємо Ignore до цього поля.