מכיוון ש-SQLite הוא מסד נתונים רלציוני, אפשר להגדיר קשרים של ישויות. אבל רוב ספריות המיפוי שקשורות לאובייקטים מאפשרות ליצור ישויות אובייקטים מפנים זה לזה, 'חדר' אוסר על כך באופן מפורש. כדי ללמוד על הסיבות הטכניות להחלטה הזו, אפשר לעיין בקטע הסבר על הסיבה שבגללה אישור הפניות לאובייקטים.
שתי גישות אפשריות
ב'חדר' יש שתי דרכים להגדיר קשר גומלין בין ישויות ולשלוח שאילתות לגביו: באמצעות סיווג נתונים ברמת ביניים אובייקטים מוטמעים או שיטת שאילתה קשורה עם החזרה של ריבוי מיפויים מהסוג הזה.
סיווג נתונים ברמת ביניים
בגישת סיווג הנתונים ברמת הביניים, מגדירים סיווג נתונים שמדמה את המודל הקשר בין הישויות בחדר. סיווג הנתונים הזה מכיל את הצמדים בין מופעים של ישות אחת לבין מופעים של ישות אחרת כמוטמעת אובייקטים. לאחר מכן, שיטות השאילתה יכולות להחזיר מופעים סיווג נתונים לשימוש באפליקציה.
לדוגמה, אפשר להגדיר מחלקת נתונים UserBook
לייצוג משתמשים בספרייה
עם ספרים ספציפיים שעברו בדיקה, ולהגדיר שיטת שאילתה לאחזור רשימה של
UserBook
מופעים ממסד הנתונים:
Kotlin
@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?)
Java
@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
לייצוג משתמשי ספרייה שרכשו ספרים ספציפיים:
Kotlin
@Query( "SELECT * FROM user" + "JOIN book ON user.id = book.user_id" ) fun loadUserAndBookNames(): Map<User, List<Book>>
Java
@Query( "SELECT * FROM user" + "JOIN book ON user.id = book.user_id" ) public Map<User, List<Book>> loadUserAndBookNames();
בחירת גישה
אפליקציית Room תומכת בשתי הגישות האלה, איזו גישה הכי מתאימה לאפליקציה שלכם. החלק הזה דן למה כדאי להשתמש באף אחת מהן.
הגישה של סיווג נתוני הביניים מאפשרת להימנע מכתיבה של שאילתות SQL אבל היא יכולה גם להוביל למורכבות קוד גבוהה יותר עקב סיווגי הנתונים הנוספים שנדרשים לה. בקיצור, סוג החזרה של 'מיפוי מרובה' מחייבת ששאילתות ה-SQL שלכם יעשו יותר עבודה, ונתוני הביניים לגישה לכיתה מצריכה את הקוד שלכם כדי לבצע יותר עבודה.
אם אין לך סיבה ספציפית להשתמש בסיווגים של נתונים ברמת ביניים, אפשר מומלץ להשתמש בגישה של סוג ההחזרה בריבוי מיפוי. מידע נוסף על בגישה הזאת. תוכלו לקרוא החזרת מיפוי מרובה.
שאר המדריך הזה הדגמה איך להגדיר קשרים באמצעות וסיווג של נתוני ביניים.
יצירת אובייקטים מוטמעים
לפעמים, תרצו לבטא ישות או אובייקט נתונים בתור
שלם מאוחד בלוגיקת מסד הנתונים, גם אם האובייקט מכיל
. במצבים כאלה, אפשר להשתמש
@Embedded
שייצג את האובייקט שברצונכם לפרק
בתוך טבלה. לאחר מכן תוכלו לשלוח שאילתות על השדות המוטמעים בדיוק כמו
לעמודות בודדות אחרות.
למשל, המחלקה User
יכולה לכלול שדה מסוג Address
מייצג הרכב של שדות בשם street
, city
, state
ו
postCode
. כדי לאחסן את העמודות המורכבות בנפרד בטבלה, צריך לכלול
השדה Address
במחלקה User
שיש בו הערות עם הערות
@Embedded
, בתור
מוצגת בקטע הקוד הבא:
Kotlin
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? )
Java
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
לנכס. לאחר מכן, החדר מוסיף את הערך שצוין בתחילת כל עמודה.
באובייקט המוטמע.
הגדרת קשרי גומלין אחד לאחד
קשר אחד לאחד בין שתי ישויות הוא קשר שבו כל מופע אחד של ישות ההורה תואם בדיוק למופע אחד של הצאצא וההפך.
לדוגמה, נניח שיש לכם אפליקציה לסטרימינג של מוזיקה שבה יש למשתמש ספרייה של
שירים שבבעלותם. לכל משתמש יש רק ספרייה אחת, וכל ספרייה
תואם למשתמש אחד בדיוק. לכן יש מודל אחד לאחד
הקשר בין הישות User
לבין הישות Library
.
כדי להגדיר קשר של אחד לאחד, תחילה יש ליצור כיתה לכל אחד משני החשבונות של ישויות. אחת מהישויות חייבת כוללים משתנה שמפנה למפתח הראשי של הישות השנייה.
Kotlin
@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 )
Java
@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
.
מוגדר לשם העמודה של ישות הצאצא שמפנה אל ההורה
את המפתח הראשי של הישות.
Kotlin
data class UserAndLibrary( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userOwnerId" ) val library: Library )
Java
public class UserAndLibrary { @Embedded public User user; @Relation( parentColumn = "userId", entityColumn = "userOwnerId" ) public Library library; }
לסיום, מוסיפים שיטה למחלקת ה-DAO שמחזירה את כל המופעים של הנתונים.
שתואם בין ישות ההורה לישות הצאצא. השיטה הזו מחייבת
חדר כדי להריץ שתי שאילתות, לכן צריך להוסיף לו את ההערה @Transaction
כך שכל הפעולה תתבצע באופן אטומי.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersAndLibraries(): List<UserAndLibrary>
Java
@Transaction @Query("SELECT * FROM User") public List<UserAndLibrary> getUsersAndLibraries();
הגדרת קשרי גומלין מסוג אחד לרבים
קשר של אחד לרבים בין שתי ישויות הוא קשר שבו כל המופע של ישות ההורה תואם לאפס מופעים או יותר של הצאצא אבל כל מופע של ישות הצאצא יכול להתאים רק לאחד המופע של ישות ההורה.
בדוגמה של אפליקציית הסטרימינג של מוזיקה, נניח שלמשתמש יש אפשרות לארגן
את השירים שלהם לפלייליסטים. כל משתמש יכול ליצור כמה פלייליסטים שהוא רוצה,
אבל כל פלייליסט נוצר על ידי משתמש אחד בלבד. לכן, יש
קשר מסוג אחד לרבים בין הישות User
לבין הישות Playlist
.
כדי להגדיר קשר גומלין של אחד לרבים, תחילה צריך ליצור מחלקה לשתי הישויות. כמו בקשר מסוג אחד לאחד, ישות הצאצא חייבת לכלול משתנה הוא הפניה למפתח הראשי של ישות ההורה.
Kotlin
@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 )
Java
@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
.
מוגדר לשם העמודה של ישות הצאצא שמפנה אל ההורה
את המפתח הראשי של הישות.
Kotlin
data class UserWithPlaylists( @Embedded val user: User, @Relation( parentColumn = "userId", entityColumn = "userCreatorId" ) val playlists: List<Playlist> )
Java
public class UserWithPlaylists { @Embedded public User user; @Relation( parentColumn = "userId", entityColumn = "userCreatorId" ) public List<Playlist> playlists; }
לסיום, מוסיפים שיטה למחלקת ה-DAO שמחזירה את כל המופעים של הנתונים.
שתואם בין ישות ההורה לישות הצאצא. השיטה הזו מחייבת
חדר כדי להריץ שתי שאילתות, לכן צריך להוסיף לו את ההערה @Transaction
כך שכל הפעולה תתבצע באופן אטומי.
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylists(): List<UserWithPlaylists>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylists> getUsersWithPlaylists();
הגדרת קשרי גומלין רבים לרבים
קשר 'רבים לרבים' בין שתי ישויות הוא קשר שבו כל המופע של ישות ההורה תואם לאפס מופעים או יותר של הצאצא וההפך.
בדוגמה של אפליקציית הסטרימינג של המוזיקה, בודקים את השירים בפלייליסטים שהוגדרו על ידי המשתמש.
כל פלייליסט יכול לכלול הרבה שירים, וכל שיר יכול להיות חלק משירים רבים.
פלייליסטים שונים. לכן יש קשר של רבים לרבים
בין הישות Playlist
לישות Song
.
כדי להגדיר קשר של רבים לרבים, תחילה צריך ליצור מחלקה לכל אחד משני
של ישויות. קשרים בין רבים לרבים
שונים מסוגי קשרים אחרים, מכיוון שבדרך כלל אין
התייחסות לישות ההורה בישות הצאצא. במקום זאת, צרו
class כדי לייצג ישות אסוציאטיבית, או הפניה צולבת
בטבלה, בין שתי הישויות. טבלת ההצללה חייבת לכלול עמודות עבור
המפתח הראשי מכל ישות בקשר 'רבים לרבים' שמיוצג על ידי
את הטבלה. בדוגמה זו, כל שורה בטבלת ההפניות המקושרים תואמת
צמד של מכונה Playlist
ומופע Song
שבו ההפניה
השיר נכלל בפלייליסט המקושר.
Kotlin
@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 )
Java
@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
.
Kotlin
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> )
Java
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
אובייקטים.
בכל אחת מה-methods האלה נדרש 'חדר' כדי להריץ שתי שאילתות, לכן צריך להוסיף את
@Transaction
לשתי השיטות, כך שכל
מתבצעת באופן אטומי.
Kotlin
@Transaction @Query("SELECT * FROM Playlist") fun getPlaylistsWithSongs(): List<PlaylistWithSongs> @Transaction @Query("SELECT * FROM Song") fun getSongsWithPlaylists(): List<SongWithPlaylists>
Java
@Transaction @Query("SELECT * FROM Playlist") public List<PlaylistWithSongs> getPlaylistsWithSongs(); @Transaction @Query("SELECT * FROM Song") public List<SongWithPlaylists> getSongsWithPlaylists();
הגדרה של קשרי גומלין מקוננים
לפעמים ייתכן שתצטרכו להריץ שאילתה על קבוצה של שלוש טבלאות או יותר שקשורים זה לזה. במקרה כזה, מגדירים קשרי גומלין מוטמעים בין הטבלאות.
נניח שבדוגמה של אפליקציית סטרימינג של מוזיקה, אתם רוצים להריץ שאילתה על כל המשתמשים, כל הפלייליסטים של כל משתמש, וכל השירים בכל פלייליסט לכל משתמש. למשתמשים יש קשר של אחד לרבים עם לפלייליסטים ולפלייליסטים יש קשר של רבים לרבים עם שירים. בדוגמת הקוד הבאה אפשר לראות את המחלקות שמייצגות ישויות וגם את טבלת ההצללה עבור הקשר 'רבים לרבים' בין פלייליסטים ושירים:
Kotlin
@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 )
Java
@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
:
Kotlin
data class PlaylistWithSongs( @Embedded val playlist: Playlist, @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = Junction(PlaylistSongCrossRef::class) ) val songs: List<Song> )
Java
public class PlaylistWithSongs { @Embedded public Playlist playlist; @Relation( parentColumn = "playlistId", entityColumn = "songId", associateBy = Junction(PlaylistSongCrossRef.class) ) public List<Song> songs; }
אחרי שמגדירים סיווג נתונים שמייצג את הקשר הזה, יוצרים סיווג נוסף
סיווג נתונים שמשמש למודל את הקשר בין טבלה אחרת מהמערך שלכם
את מחלקת הקשרים הראשונה, את היחסים הקיימים במסגרת
אחת. בדוגמה הבאה מוצגת מחלקה UserWithPlaylistsAndSongs
שמשמשת לבניית מודלים
קשר של אחד לרבים בין סוג הישות User
PlaylistWithSongs
סוג קשר:
Kotlin
data class UserWithPlaylistsAndSongs( @Embedded val user: User @Relation( entity = Playlist::class, parentColumn = "userId", entityColumn = "userCreatorId" ) val playlists: List<PlaylistWithSongs> )
Java
public class UserWithPlaylistsAndSongs { @Embedded public User user; @Relation( entity = Playlist.class, parentColumn = "userId", entityColumn = "userCreatorId" ) public List<PlaylistWithSongs> playlists; }
הכיתה UserWithPlaylistsAndSongs
יוצרת מודלים באופן עקיף של הקשרים
בין כל שלושת סוגי הישויות: User
, Playlist
ו-Song
. הדבר
שמיוצגת באיור 1.
![מודלים של UserWithSongSongs לגבי הקשר בין משתמש לבין
פלייליסטWithSongs, שמדגים את הקשר בין
ו'שיר'.](https://developer.android.com/static/images/training/data-storage/room_nested_relationships.png?authuser=2&hl=he)
איור 1. תרשים של סוגי קשרים דוגמה לאפליקציית סטרימינג של מוזיקה.
אם יש עוד טבלאות בקבוצה, יוצרים מחלקה כדי לבנות את המודל הקשר בין כל הטבלה שנותרה לבין סיווג הקשרים שבמודל קשרי הגומלין בין כל הטבלאות הקודמות. היא יוצרת שרשרת של רכיבים בין כל הטבלאות שעליהן רוצים לשלוח שאילתות.
לבסוף, מוסיפים שיטה למחלקת ה-DAO כדי לחשוף את פונקציונליות השאילתה
שדרושה לאפליקציה. בשביל השיטה הזו נדרש 'חדר' כדי להריץ כמה שאילתות, לכן צריך להוסיף את
הערה אחת (@Transaction
)
כך שכל הפעולה תתבצע באופן אטומי:
Kotlin
@Transaction @Query("SELECT * FROM User") fun getUsersWithPlaylistsAndSongs(): List<UserWithPlaylistsAndSongs>
Java
@Transaction @Query("SELECT * FROM User") public List<UserWithPlaylistsAndSongs> getUsersWithPlaylistsAndSongs();
מקורות מידע נוספים
למידע נוסף על הגדרת קשרי גומלין בין ישויות בחדר, אפשר לעיין במאמר במקורות המידע הנוספים.
דוגמיות
סרטונים
- מה חדש בחדר (Android פיתוח כנס 2019)