Room. Запит із кількох таблиць. Relation
У цьому уроці розглянемо, як отримувати дані з декількох таблиць. А також розберемося, як використовувати анотацію Relation.
Для прикладу будемо використовувати дві таблиці: співробітники та відділи. Кожен співробітник прикріплений до якогось відділу.
Entity об'єкт для відділів:
@Entity
public class Department {
@PrimaryKey
public int id;
public String name;
}
Entity об'єкт для співробітників:
@Entity
public class Employee {
@PrimaryKey
public long id;
public String name;
public int salary;
@ColumnInfo(name = "department_id")
public int departmentId;
}
У полі departmentId зберігається id відділу, до якого прикріплений співробітник.
Ми хочемо отримати список працівників, у якому буде така інформація: ім'я працівника, його зарплата, найменування його відділу. Для цього нам треба буде написати запит, який витягне дані з двох таблиць.
Описуємо метод у Dao об'єкті
@Dao
public interface EmployeeDao {
@Query("SELECT employee.name, employee.salary, department.name AS department_name " +
"FROM employee, department " +
"WHERE department.id == employee.department_id")
public List<EmployeeDepartment> getEmployeeWithDepartment();
// ...
}
Оскільки поле name є в обох таблицях, то для відділу перейменовуємо його в department_name
Зверніть увагу на тип об'єктів, який ми будемо отримувати від цього методу. Це EmployeeDepartment. Нам потрібно створити цей об'єкт і вказати в ньому всі поля, які ми очікуємо отримати від запиту.
public class EmployeeDepartment {
public String name;
public int salary;
@ColumnInfo(name = "department_name")
public String departmentName;
}
Це не Entity об'єкт, а звичайний клас. Поля цього класу мають збігатися з полями результату, який поверне запит. Room конвертує результати запиту в список цих об'єктів, і ми отримаємо те, що хотіли.
Relation
Анотація Relation також дає змогу робити запити з кількох таблиць, але структура результату буде трохи іншою. І нам самим не доведеться писати складні запити. Room усе зробить за нас.
Давайте уявимо, що нам треба отримати список відділів. І до кожного відділу має додаватися список співробітників.
Структура для цих даних матиме такий вигляд:
public class DepartmentWithEmployees {
public int id;
public String name;
@Relation(parentColumn = "id", entityColumn = "department_id")
public List<Employee> employees;
Це не Entity, а звичайний клас. У полях id і name будуть дані відділу.
У employees буде список співробітників цього відділу. Для цього ми позначаємо список анотацією Relation, і Room сам заповнить його для нас. Давайте розбиратися, як саме Room зрозуміє, що він має помістити в цей список. Звідки він братиме дані і за якою умовою?
Тип даних списку - це Employee. Це Entity об'єкт, для нього в базі даних створено таблицю. З цієї таблиці Room і буде читати дані по співробітниках. У параметрах parentColumn і entityColumn вказуємо назви полів, які беруть участь в умові вибірки даних. У результаті Room шукатиме співробітників, у яких entityColumn (тобто department_id) дорівнює parentColumn (тобто id) відділу. Усі знайдені співробітники опиняться в employees.
За вимогами Room, тип employees має бути List або Set.
Залишилося описати метод у Dao:
@Dao
public interface DepartmentDao {
@Query("SELECT id, name from department")
List<DepartmentWithEmployees> getDepartmentsWithEmployees();
// ...
}
Це простий запит, який витягне необхідні дані по відділу. А запит щодо співробітників для кожного відділу зробить за нас Room.
У класі DepartmentWithEmployees ми використовуємо поля id і name для даних по відділу. Але клас Department має точно таку саму структуру - id і name. Тому ми в DepartmentWithEmployees можемо замінити ці поля на одне поле з типом Department і анотацією Embedded:
public class DepartmentWithEmployees {
@Embedded
public Department department;
@Relation(parentColumn = "id", entityColumn = "department_id")
public List<Employee> employees;
}
Припустімо, що нам потрібні не всі дані по співробітниках, а тільки деякі поля. Наприклад, name і salary. Створюємо під них клас:
public class EmployeeNameAndSalary {
public String name;
public int salary;
}
І використовуємо його, як тип у Relation-списку
public class DepartmentWithEmployees {
public int id;
public String name;
@Relation(parentColumn = "id", entityColumn = "department_id", entity = Employee.class)
public List<EmployeeNameAndSalary> employees;
}
А щоб Room знав, звідки брати дані по співробітниках, вказуємо Entity клас Employee у параметрі entity.
Relation може бути вкладеним. Тобто в нашому прикладі клас EmployeeNameAndSalary також може містити в собі Relation, який буде для кожного співробітника збирати, наприклад, список техніки, записаної на нього.
Relation не може бути використаний в Entity класах, тільки у звичайних. Relation поле не може задаватися через конструктор. Воно має бути public або мати public set-метод.
Relation + Transaction
При використанні Relation, Room виконує кілька запитів, щоб зібрати всі дані. Має сенс виконувати всі ці запити в одній транзакції, щоб отримати коректні дані. Для цього можна використовувати анотацію Transaction
@Transaction
@Query("SELECT id, name from department")
List<DepartmentWithEmployees> getDepartmentsWithEmployees();
Use the multimap return types approach
У підході з типом повернення multimap вам не потрібно визначати жодних додаткових класів даних. Замість цього ви визначаєте тип повернення мультикарти для вашого методу на основі потрібної вам структури відображення і визначаєте зв'язок між вашими сутностями безпосередньо у вашому SQL-запиті.
Наприклад, наступний метод запиту повертає відображення екземплярів User і Book для представлення користувачів бібліотеки з певними книгами, які вони взяли:
@Query(
"SELECT * FROM user" +
"JOIN book ON user.id = book.user_id"
)
public Map<User, List<Book>> loadUserAndBookNames();
one-to-one relationships
Зв'язок «один-до-одного» між двома сутностями - це зв'язок, де кожному екземпляру батьківської сутності відповідає рівно один екземпляр дочірньої сутності, і навпаки.
Наприклад, розглянемо додаток для потокового прослуховування музики, де користувач має власну бібліотеку пісень. Кожен користувач має лише одну бібліотеку, і кожна бібліотека відповідає точно одному користувачеві. Отже, між сутністю User та сутністю Library існує зв'язок один-до-одного.
Визначте зв'язок
Щоб визначити зв'язок «один-до-одного», спочатку створіть клас для кожної з двох сутностей. Одна з сутностей повинна містити змінну, яка є посиланням на первинний ключ іншої сутності.
@Entity
public class User {
@PrimaryKey public long userId;
public String name;
public int age;
}
@Entity
public class Library {
@PrimaryKey public long libraryId;
public long userOwnerId;
}
Запит до сутностей
Щоб зробити запит до списку користувачів і відповідних бібліотек, ви повинні спочатку змоделювати зв'язок один-до-одного між цими двома сутностями.
Для цього створіть новий клас даних, кожен екземпляр якого містить екземпляр батьківської сутності і відповідний екземпляр дочірньої сутності. Додайте анотацію @Relation до екземпляра дочірньої сутності, встановивши parentColumn як назву стовпця первинного ключа батьківської сутності, а entityColumn - як назву стовпця дочірньої сутності, який посилається на первинний ключ батьківської сутності.
public class UserAndLibrary {
@Embedded public User user;
@Relation(
parentColumn = "userId",
entityColumn = "userOwnerId"
)
public Library library;
}
Нарешті, додайте до класу DAO метод, який повертає всі екземпляри класу даних, що зв'язує батьківську сутність і дочірню сутність. Цей метод вимагає від Room виконання двох запитів. Тому вам слід додати до цього методу анотацію @Transaction. Це гарантує, що вся операція виконується атомарно.
@Transaction
@Query("SELECT * FROM User")
public List<UserAndLibrary> getUsersAndLibraries();
one-to-many relationships
Зв'язок «один-до-багатьох» між двома сутностями - це зв'язок, де кожному екземпляру батьківської сутності відповідає нуль або більше екземплярів дочірньої сутності, але кожен екземпляр дочірньої сутності може відповідати лише одному екземпляру батьківської сутності.
У прикладі програми для потокового прослуховування музики, припустимо, що користувач має можливість організовувати свої пісні у плейлисти. Кожен користувач може створити стільки плейлистів, скільки захоче, але кожен плейлист створює лише один користувач. Отже, між сутністю User (Користувач) та сутністю Playlist (Список відтворення) існує зв'язок один-до-багатьох.
Визначення зв'язку
Щоб визначити зв'язок «один-до-багатьох», спочатку створіть клас для двох сутностей. Як і у випадку зв'язку «один-до-одного», дочірня сутність повинна містити змінну, яка є посиланням на первинний ключ батьківської сутності.
@Entity
public class User {
@PrimaryKey public long userId;
public String name;
public int age;
}
@Entity
public class Playlist {
@PrimaryKey public long playlistId;
public long userCreatorId;
public String playlistName;
}
Запит до сутностей
Щоб зробити запит до списку користувачів і відповідних плейлистів, спочатку потрібно змоделювати зв'язок «один-до-багатьох» між двома сутностями
Для цього створіть новий клас даних, кожен екземпляр якого містить екземпляр батьківської сутності та список усіх відповідних екземплярів дочірніх сутностей. Додайте анотацію @Relation до екземпляра дочірньої сутності, встановивши parentColumn як назву стовпця первинного ключа батьківської сутності, а entityColumn - як назву стовпця дочірньої сутності, який посилається на первинний ключ батьківської сутності.
@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylists> getUsersWithPlaylists();
many-to-many relationships
Зв'язок «багато-до-багатьох» між двома сутностями - це зв'язок, де кожному екземпляру батьківської сутності відповідає нуль або більше екземплярів дочірньої сутності, і навпаки.
У прикладі програми для потокового відтворення музики розглянемо пісні у визначених користувачем списках відтворення. Кожен плейлист може містити багато пісень, а кожна пісня може бути частиною багатьох різних плейлистів. Отже, між сутністю Playlist і сутністю Song існує зв'язок «багато до багатьох».
Визначення зв'язку
Щоб визначити зв'язок «багато до багатьох», спочатку створіть клас для кожної з двох сутностей. Зв'язок «багато-до-багатьох» відрізняється від інших типів зв'язків тим, що в дочірніх сутностях, як правило, немає посилань на батьківську сутність. Замість цього створіть третій клас для представлення асоціативної сутності, або таблиці перехресних посилань, між двома сутностями. Таблиця перехресних посилань повинна мати стовпці для первинного ключа кожної сутності у зв'язку «багато-до-багатьох», представленої в таблиці. У цьому прикладі кожен рядок у таблиці перехресних посилань відповідає парі екземплярів Playlist і Song, де пісня, на яку є посилання, входить до списку відтворення, на який є посилання.
@Entity
public class Playlist {
@PrimaryKey public long playlistId;
public String playlistName;
}
@Entity
public class Song {
@PrimaryKey public long songId;
public String songName;
public String artist;
}
@Entity(primaryKeys = {"playlistId", "songId"})
public class PlaylistSongCrossRef {
public long playlistId;
public long songId;
}
Запит до сутностей
Наступний крок залежить від того, як ви хочете запитувати ці пов'язані сутності.
- Якщо ви хочете запитувати списки відтворення і список відповідних пісень для кожного списку відтворення, створіть новий клас даних, який міститиме один об'єкт
Playlistі список усіх об'єктівSong, що входять до списку відтворення. - Якщо ви хочете запитувати пісні та список відповідних плейлистів для кожної з них, створіть новий клас даних, який містить один об'єкт
Songі список усіх об'єктівPlaylist, до яких входить пісня.
У будь-якому випадку змоделюйте зв'язок між об'єктами за допомогою властивості associateBy в анотації @Relation у кожному з цих класів, щоб визначити об'єкт перехресного посилання, який забезпечує зв'язок між об'єктом Playlist і об'єктом Song.
public class PlaylistWithSongs {
@Embedded public Playlist playlist;
@Relation(
parentColumn = "playlistId",
entityColumn = "songId",
associateBy = @Junction(PlaylistSongCrossref.class)
)
public List<Song> songs;
}
public class SongWithPlaylists {
@Embedded public Song song;
@Relation(
parentColumn = "songId",
entityColumn = "playlistId",
associateBy = @Junction(PlaylistSongCrossref.class)
)
public List<Playlist> playlists;
}
Нарешті, додайте метод до класу DAO, щоб розкрити функцію запиту, яка потрібна вашому додатку.
getPlaylistsWithSongs: цей метод робить запит до бази даних і повертає всі отримані об'єктиPlaylistWithSongs.getSongsWithPlaylists: цей метод робить запит до бази даних і повертає всі отримані об'єктиSongWithPlaylists.
Кожен з цих методів вимагає від Room виконання двох запитів, тому додайте до обох методів анотацію @Transaction, щоб вся операція виконувалася атомарно.
@Transaction
@Query("SELECT * FROM Playlist")
public List<PlaylistWithSongs> getPlaylistsWithSongs();
@Transaction
@Query("SELECT * FROM Song")
public List<SongWithPlaylists> getSongsWithPlaylists();
Вкладені запити
https://developer.android.com/training/data-storage/room/relationships/nested