Room. Type converter

У цьому уроці розглянемо, як використовувати конвертери типів даних, щоб Room міг зберігати не тільки поля-примітиви.

Іноді ваші Entity об'єкти можуть містити поля, які не є примітивами, і не можуть бути збережені в БД.

Як приклад розглянемо клас працівника. У нього цілком може бути поле, в якому ми хочемо перерахувати його хобі. Використовуємо для цього поле hobbies з типом List<String>

@Entity()
public class Employee {
 
   @PrimaryKey
   public long id;
 
   public String name;
 
   public int salary;
 
   public List<String> hobbies;
 
}

Якщо ми спробуємо зараз скомпілювати проєкт, то отримаємо помилку: Cannot figure out how to save this field into database. You can consider adding a type converter for it.

Room справедливо зауважує, що й гадки не має, як йому таке поле зберегти в базу, і пропонує використовувати type converter.

Ок, давайте створимо конвертер. Він має вміти конвертувати List<String> у який-небудь простий тип, який може бути збережений у базу, наприклад, String. Також конвертер має вміти конвертувати у зворотний бік, тобто зі String у List<String>, щоб Room міг прочитати дані з бази в поле Entity об'єкта.

Створюємо конвертер:

public class HobbiesConverter {
 
   @TypeConverter
   public String fromHobbies(List<String> hobbies) {
       return hobbies.stream().collect(Collectors.joining(","));
   }
 
   @TypeConverter
   public List<String> toHobbies(String data) {
       return Arrays.asList(data.split(","));
   }
 
}

Перший метод перетворює List<String> на String. Другий - навпаки. Обидва методи позначаємо анотацією TypeConverter.

Залишилося вказати цей конвертер для поля hobbies. Це робиться анотацією TypeConverters із зазначенням класу конвертера.

@Entity()
public class Employee {
 
   @PrimaryKey
   public long id;
 
   public String name;
 
   public int salary;
 
   @TypeConverters({HobbiesConverter.class})
   public List<String> hobbies;
 
}

Тепер Room знатиме, що для поля hobbies він може використовувати конвертер HobbiesConverter.

Конвертер також можна вказати для всього Entity об'єкта. Це може бути корисно, якщо у вас в Entity кілька полів потребують конвертери. Ви створюєте один клас, там прописуєте всі необхідні методи перетворення полів, і вказуєте цей клас для всього Entity.

@Entity()
@TypeConverters({EmployeeConverter.class})
public class Employee {
 
   @PrimaryKey
   public long id;
 
   public String name;
 
   public int salary;
 
   public List<String> hobbies;
 
}

Бувають випадки, коли перетворення може бути необхідним не тільки для Entity об'єкта. Розглянемо приклад.

Є Entity клас

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

У працівника всі поля є простими, і Room без проблем може їх зберегти/прочитати. Цим полям не потрібні конвертери.

Але що якщо ми хочемо в Dao зробити так:

@Dao
public interface EmployeeDao {
 
   @Query("SELECT * FROM employee WHERE birthday = :birthdayDate")
   Employee getByDate(Date birthdayDate);
 
}

Тобто нам для пошуку за полем birthday (з типом long) зручніше використовувати об'єкт Date.

Під час спроби зібрати проєкт отримуємо помилку: Query method parameters should either be a type that can be converted into a database column or a List / Array that contains such type. Для цього можна розглянути можливість додавання Type Adapter.

Room повідомляє, що типи не збігаються і знову пропонує використовувати конвертери.

Створюємо конвертер:

public class DateConverter {
 
   @TypeConverter
   public Long dateToTimestamp(Date date) {
       if (date == null) {
           return null;
       } else {
           return date.getTime();
       }
   }
 
}

У нашому випадку необхідно Date конвертувати в long, щоб Room міг виконати query запит. Створюємо для цього метод dateToTimestamp.

Зворотна конвертація нам не потрібна. У Room немає необхідності конвертувати long у Date. Об'єкт Employee міститиме дату у форматі long.

Конвертер прописуємо в Dao, прямо для конкретного параметра конкретного методу

@Dao
public interface EmployeeDao {
 
   @Query("SELECT * FROM employee WHERE birthday = :birthday")
   Employee getByDate(@TypeConverters({DateConverter.class}) Date birthday);
}

Тепер Room конвертує Date в long і запит буде виконано.

Також конвертер можна прописати для всього методу, а не окремого параметра

@Dao
public interface EmployeeDao {
 
   @Query("SELECT * FROM employee WHERE birthday BETWEEN :birthdayFrom and :birthdayTo")
   @TypeConverters({DateConverter.class})
   Employee getByDate(Date birthdayFrom, Date birthdayTo);
}

У цьому разі Room зможе використовувати конвертер для перетворення всіх параметрів методу.

Якщо ж прописати конвертер для Dao, то він буде доступний усім методам цього Dao

@Dao
@TypeConverters({DateConverter.class})
public interface EmployeeDao {
 
   ...
}

Ну і найглобальніше рішення - прописати конвертер для Database

@Database(entities = {Employee.class}, version = 1)
@TypeConverters({DateConverter.class})
public abstract class AppDatabase extends RoomDatabase {
   public abstract EmployeeDao employeeDao();
}

У цьому разі Room зможе використовувати його у всіх Entity і Dao.

Якщо у вас кілька конвертерів, вказуйте їх через кому.