הגדרת קשרים בין אובייקטים

מכיוון ש-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, שמדגים את הקשר בין
  ו&#39;שיר&#39;.

איור 1. תרשים של סוגי קשרים דוגמה לאפליקציית סטרימינג של מוזיקה.

אם יש עוד טבלאות בקבוצה, יוצרים מחלקה כדי לבנות את המודל הקשר בין כל הטבלה שנותרה לבין סיווג הקשרים שבמודל קשרי הגומלין בין כל הטבלאות הקודמות. היא יוצרת שרשרת של רכיבים בין כל הטבלאות שעליהן רוצים לשלוח שאילתות.

לבסוף, מוסיפים שיטה למחלקת ה-DAO כדי לחשוף את פונקציונליות השאילתה שדרושה לאפליקציה. בשביל השיטה הזו נדרש 'חדר' כדי להריץ כמה שאילתות, לכן צריך להוסיף את הערה אחת (@Transaction) כך שכל הפעולה תתבצע באופן אטומי:

Kotlin

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

Java

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

מקורות מידע נוספים

למידע נוסף על הגדרת קשרי גומלין בין ישויות בחדר, אפשר לעיין במאמר במקורות המידע הנוספים.

דוגמיות

סרטונים

בלוגים