Определение отношений между объектами

Поскольку SQLite — это реляционная база данных, вы можете определять отношения между сущностями. Но хотя большинство библиотек объектно-реляционного отображения позволяют объектам сущностей ссылаться друг на друга, Room явно запрещает это. Чтобы узнать о технических причинах этого решения, см. раздел «Понимание того, почему Room не разрешает ссылки на объекты» .

Два возможных подхода

В Room есть два способа определить и запросить связь между сущностями: использовать либо промежуточный класс данных со встроенными объектами, либо метод реляционного запроса с возвращаемым типом multimap.

Промежуточный класс данных

При подходе с промежуточным классом данных вы определяете класс данных, который моделирует отношения между объектами вашей комнаты. Этот класс данных содержит пары между экземплярами одного объекта и экземплярами другого объекта в виде встроенных объектов . Ваши методы запроса могут затем возвращать экземпляры этого класса данных для использования в вашем приложении.

Например, вы можете определить класс данных UserBook для представления пользователей библиотеки с определенными извлеченными книгами, а также определить метод запроса для получения списка экземпляров UserBook из базы данных:

Котлин

@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>>
}

data class UserBook(val userName: String?, val bookName: String?)

Ява

@Dao
public interface UserBookDao {
   @Query("SELECT user.name AS userName, book.name AS bookName " +
          "FROM user, book " +
          "WHERE user.id = book.user_id")
   public LiveData<List<UserBook>> loadUserAndBookNames();
}

public class UserBook {
    public String userName;
    public String bookName;
}

Типы возврата мультикарты

В подходе типа возвращаемого значения с несколькими отображениями вам не нужно определять какие-либо дополнительные классы данных. Вместо этого вы определяете тип возвращаемого значения multimap для своего метода на основе желаемой структуры карты и определяете связь между вашими сущностями непосредственно в SQL-запросе.

Например, следующий метод запроса возвращает сопоставление экземпляров User и Book для представления пользователей библиотеки с определенными извлеченными книгами:

Котлин

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
fun loadUserAndBookNames(): Map<User, List<Book>>

Ява

@Query(
    "SELECT * FROM user" +
    "JOIN book ON user.id = book.user_id"
)
public Map<User, List<Book>> loadUserAndBookNames();

Выберите подход

Room поддерживает оба этих подхода, поэтому вы можете использовать тот, который лучше всего подходит для вашего приложения. В этом разделе обсуждаются некоторые причины, по которым вы можете предпочесть тот или иной вариант.

Подход с использованием промежуточных классов данных позволяет избежать написания сложных SQL-запросов, но он также может привести к увеличению сложности кода из-за необходимости использования дополнительных классов данных. Короче говоря, подход типа возвращаемого значения с несколькими отображениями требует, чтобы ваши SQL-запросы выполняли больше работы, а подход с промежуточным классом данных требует, чтобы ваш код выполнял больше работы.

Если у вас нет конкретной причины использовать промежуточные классы данных, мы рекомендуем использовать подход типа возвращаемого значения multimap. Дополнительные сведения об этом подходе см. в разделе Возврат мультикарты .

Оставшаяся часть этого руководства демонстрирует, как определять отношения, используя подход промежуточного класса данных.

Создание встроенных объектов

Иногда вам нужно выразить сущность или объект данных как единое целое в логике вашей базы данных, даже если объект содержит несколько полей. В таких ситуациях вы можете использовать аннотацию @Embedded для представления объекта, который вы хотите разложить на подполя в таблице. Затем вы можете запросить встроенные поля так же, как и для других отдельных столбцов.

Например, ваш класс User может включать поле типа Address , которое представляет собой комбинацию полей с именами street , city , state и postCode . Чтобы хранить составные столбцы в таблице отдельно, включите поле Address в класс User , помеченное @Embedded , как показано в следующем фрагменте кода:

Котлин

data class Address(
    val street: String?,
    val state: String?,
    val city: String?,
    @ColumnInfo(name = "post_code") val postCode: Int
)

@Entity
data class User(
    @PrimaryKey val id: Int,
    val firstName: String?,
    @Embedded val address: Address?
)

Ява

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 .

Если сущность имеет несколько встроенных полей одного типа, вы можете сохранить уникальность каждого столбца, задав свойство prefix . Затем Room добавляет предоставленное значение в начало имени каждого столбца во внедренном объекте.

Определите отношения один к одному

Отношение «один к одному» между двумя объектами — это отношения, в которых каждый экземпляр родительского объекта соответствует ровно одному экземпляру дочернего объекта, и обратное также верно.

Например, рассмотрим приложение потоковой передачи музыки, в котором у пользователя есть собственная библиотека песен. У каждого пользователя есть только одна библиотека, и каждая библиотека соответствует ровно одному пользователю. Следовательно, между сущностью User и сущностью Library существует связь «один к одному».

Чтобы определить связь «один к одному», сначала создайте класс для каждой из двух сущностей. Одна из сущностей должна включать переменную, которая является ссылкой на первичный ключ другой сущности.

Котлин

@Entity
data class User(
    @PrimaryKey val userId: Long,
    val name: String,
    val age: Int
)

@Entity
data class Library(
    @PrimaryKey val libraryId: Long,
    val userOwnerId: Long
)

Ява

@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 задано имя столбца дочерней сущности, который ссылается на первичный ключ родительской сущности.

Котлин

data class UserAndLibrary(
    @Embedded val user: User,
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    val library: Library
)

Ява

public class UserAndLibrary {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userOwnerId"
    )
    public Library library;
}

Наконец, добавьте в класс DAO метод, который возвращает все экземпляры класса данных, объединяющего родительский объект и дочерний объект. Этот метод требует, чтобы Room выполнил два запроса, поэтому добавьте к этому методу аннотацию @Transaction , чтобы вся операция выполнялась атомарно.

Котлин

@Transaction
@Query("SELECT * FROM User")
fun getUsersAndLibraries(): List<UserAndLibrary>

Ява

@Transaction
@Query("SELECT * FROM User")
public List<UserAndLibrary> getUsersAndLibraries();

Определить отношения «один ко многим»

Связь «один ко многим» между двумя объектами — это связь, при которой каждый экземпляр родительской сущности соответствует нулю или более экземплярам дочерней сущности, но каждый экземпляр дочерней сущности может соответствовать только одному экземпляру родительской сущности.

Предположим, что в примере приложения для потоковой передачи музыки у пользователя есть возможность организовывать свои песни в списки воспроизведения. Каждый пользователь может создать столько списков воспроизведения, сколько пожелает, но каждый список воспроизведения создается только одним пользователем. Следовательно, между объектом User и объектом Playlist существует связь «один ко многим».

Чтобы определить связь «один ко многим», сначала создайте класс для двух сущностей. Как и в отношениях «один к одному», дочерняя сущность должна включать переменную, которая является ссылкой на первичный ключ родительской сущности.

Котлин

@Entity
data class User(
    @PrimaryKey val userId: Long,
    val name: String,
    val age: Int
)

@Entity
data class Playlist(
    @PrimaryKey val playlistId: Long,
    val userCreatorId: Long,
    val playlistName: String
)

Ява

@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 задано имя столбца дочерней сущности, который ссылается на первичный ключ родительской сущности.

Котлин

data class UserWithPlaylists(
    @Embedded val user: User,
    @Relation(
          parentColumn = "userId",
          entityColumn = "userCreatorId"
    )
    val playlists: List<Playlist>
)

Ява

public class UserWithPlaylists {
    @Embedded public User user;
    @Relation(
         parentColumn = "userId",
         entityColumn = "userCreatorId"
    )
    public List<Playlist> playlists;
}

Наконец, добавьте в класс DAO метод, который возвращает все экземпляры класса данных, объединяющего родительский объект и дочерний объект. Этот метод требует, чтобы Room выполнил два запроса, поэтому добавьте к этому методу аннотацию @Transaction , чтобы вся операция выполнялась атомарно.

Котлин

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylists(): List<UserWithPlaylists>

Ява

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylists> getUsersWithPlaylists();

Определите отношения «многие ко многим»

Связь «многие ко многим» между двумя объектами — это связь, в которой каждый экземпляр родительской сущности соответствует нулю или более экземплярам дочерней сущности, и обратное также верно.

В примере приложения для потоковой передачи музыки рассмотрим песни в пользовательских плейлистах. Каждый список воспроизведения может включать множество песен, и каждая песня может быть частью множества разных списков воспроизведения. Таким образом, между объектом Playlist и объектом Song существует связь «многие ко многим».

Чтобы определить связь «многие ко многим», сначала создайте класс для каждой из двух сущностей. Отношения «многие-ко-многим» отличаются от других типов отношений, поскольку в дочерней сущности обычно нет ссылки на родительскую сущность. Вместо этого создайте третий класс для представления ассоциативной сущности или таблицы перекрестных ссылок между двумя сущностями. Таблица перекрестных ссылок должна содержать столбцы для первичного ключа от каждой сущности в отношении «многие ко многим», представленном в таблице. В этом примере каждая строка в таблице перекрестных ссылок соответствует паре экземпляра Playlist и экземпляра Song , где указанная песня включена в указанный список воспроизведения.

Котлин

@Entity
data class Playlist(
    @PrimaryKey val playlistId: Long,
    val playlistName: String
)

@Entity
data class Song(
    @PrimaryKey val songId: Long,
    val songName: String,
    val artist: String
)

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
    val playlistId: Long,
    val songId: Long
)

Ява

@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 .

Котлин

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)

data class SongWithPlaylists(
    @Embedded val song: Song,
    @Relation(
         parentColumn = "songId",
         entityColumn = "playlistId",
         associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val playlists: List<Playlist>
)

Ява

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")
fun getPlaylistsWithSongs(): List<PlaylistWithSongs>

@Transaction
@Query("SELECT * FROM Song")
fun getSongsWithPlaylists(): List<SongWithPlaylists>

Ява

@Transaction
@Query("SELECT * FROM Playlist")
public List<PlaylistWithSongs> getPlaylistsWithSongs();

@Transaction
@Query("SELECT * FROM Song")
public List<SongWithPlaylists> getSongsWithPlaylists();

Определение вложенных отношений

Иногда вам может потребоваться запросить набор из трех или более таблиц, связанных друг с другом. В этом случае вы определяете вложенные связи между таблицами.

Предположим, что в примере приложения потоковой передачи музыки вы хотите запросить всех пользователей, все плейлисты для каждого пользователя и все песни в каждом плейлисте для каждого пользователя. Пользователи имеют отношение «один ко многим» со списками воспроизведения, а плейлисты имеют отношение «многие ко многим» с песнями. В следующем примере кода показаны классы, представляющие эти сущности, а также таблица перекрестных ссылок для связи «многие ко многим» между плейлистами и песнями:

Котлин

@Entity
data class User(
    @PrimaryKey val userId: Long,
    val name: String,
    val age: Int
)

@Entity
data class Playlist(
    @PrimaryKey val playlistId: Long,
    val userCreatorId: Long,
    val playlistName: String
)

@Entity
data class Song(
    @PrimaryKey val songId: Long,
    val songName: String,
    val artist: String
)

@Entity(primaryKeys = ["playlistId", "songId"])
data class PlaylistSongCrossRef(
    val playlistId: Long,
    val songId: Long
)

Ява

@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;
}
@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;
}

Сначала смоделируйте отношения между двумя таблицами в вашем наборе, как обычно, используя класс данных и аннотацию @Relation . В следующем примере показан класс PlaylistWithSongs , который моделирует связь «многие ко многим» между классом сущности Playlist и классом сущности Song :

Котлин

data class PlaylistWithSongs(
    @Embedded val playlist: Playlist,
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef::class)
    )
    val songs: List<Song>
)

Ява

public class PlaylistWithSongs {
    @Embedded public Playlist playlist;
    @Relation(
         parentColumn = "playlistId",
         entityColumn = "songId",
         associateBy = Junction(PlaylistSongCrossRef.class)
    )
    public List<Song> songs;
}

После того как вы определите класс данных, представляющий эту связь, создайте другой класс данных, который моделирует связь между другой таблицей из вашего набора и первым классом отношений, «вкладывая» существующую связь в новую. В следующем примере показан класс UserWithPlaylistsAndSongs , который моделирует связь «один ко многим» между классом сущности User и классом отношений PlaylistWithSongs :

Котлин

data class UserWithPlaylistsAndSongs(
    @Embedded val user: User
    @Relation(
        entity = Playlist::class,
        parentColumn = "userId",
        entityColumn = "userCreatorId"
    )
    val playlists: List<PlaylistWithSongs>
)

Ява

public class UserWithPlaylistsAndSongs {
    @Embedded public User user;
    @Relation(
        entity = Playlist.class,
        parentColumn = "userId",
        entityColumn = "userCreatorId"
    )
    public List<PlaylistWithSongs> playlists;
}

Класс UserWithPlaylistsAndSongs косвенно моделирует отношения между всеми тремя классами сущностей: User , Playlist и Song . Это показано на рисунке 1.

UserWithPlaylistsAndSongs моделирует связь между User и PlaylistWithSongs, которая, в свою очередь, моделирует связь между Playlist и Song.

Рисунок 1. Диаграмма классов отношений в примере приложения потоковой передачи музыки.

Если в вашем наборе есть еще таблицы, создайте класс для моделирования отношений между каждой оставшейся таблицей и класс отношений, который моделирует отношения между всеми предыдущими таблицами. Это создает цепочку вложенных связей между всеми таблицами, которые вы хотите запросить.

Наконец, добавьте метод в класс DAO, чтобы предоставить функции запросов, необходимые вашему приложению. Этот метод требует, чтобы Room выполнял несколько запросов, поэтому добавьте аннотацию @Transaction , чтобы вся операция выполнялась атомарно:

Котлин

@Transaction
@Query("SELECT * FROM User")
fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>

Ява

@Transaction
@Query("SELECT * FROM User")
public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();

Дополнительные ресурсы

Чтобы узнать больше об определении связей между сущностями в Room, см. следующие дополнительные ресурсы.

Образцы

Видео

Блоги