כשמשתמשים בספריית Room persistence כדי לאחסן את הנתונים של האפליקציה, מגדירים אובייקטים של גישה לנתונים (DAO) כדי ליצור אינטראקציה עם הנתונים המאוחסנים. כל DAO כולל פונקציות שמציעות גישה מופשטת למסד הנתונים של האפליקציה. בזמן ההידור, Room יוצר באופן אוטומטי הטמעות של אובייקטי ה-DAO שהגדרתם.
שימוש ב-DAO כדי לגשת למסד הנתונים של האפליקציה במקום בבוני שאילתות או בשאילתות ישירות מאפשר לשמור על הפרדה בין נושאים, עיקרון ארכיטקטוני קריטי. בנוסף, אפשר להשתמש ב-DAO כדי לדמות גישה למסד נתונים כשבודקים את האפליקציה.
המבנה של DAO
אפשר להגדיר כל DAO כממשק או כמחלקה מופשטת. במקרים בסיסיים, בדרך כלל משתמשים בממשק. בכל מקרה, תמיד צריך להוסיף את התג @Dao ל-DAO. ל-DAO אין מאפיינים, אבל הוא מגדיר פונקציה אחת או יותר לאינטראקציה עם הנתונים במסד הנתונים של האפליקציה.
הקוד הבא הוא דוגמה ל-DAO שמגדיר פונקציות להוספה, למחיקה ולבחירה של אובייקטים מסוג User במסד נתונים של Room:
@Dao interface UserDao { @Insert suspend fun insertAll(vararg users: User) @Delete suspend fun delete(user: User) @Query("SELECT * FROM user") suspend fun getAll(): List<User> }
יש שני סוגים של פונקציות DAO שמגדירות אינטראקציות עם מסד נתונים:
- פונקציות נוחות שמאפשרות להוסיף, לעדכן ולמחוק שורות במסד הנתונים בלי לכתוב קוד SQL.
- פונקציות שאילתה שמאפשרות לכם לכתוב שאילתת SQL משלכם כדי ליצור אינטראקציה עם מסד הנתונים.
בקטעים הבאים מוסבר איך להשתמש בשני סוגי הפונקציות של DAO כדי להגדיר את האינטראקציות עם מסד הנתונים שהאפליקציה צריכה.
פונקציות נוחות
Room מספקת הערות נוחות להגדרת פונקציות שמבצעות הוספות, עדכונים ומחיקות בלי שתצטרכו לכתוב הצהרת SQL.
אם אתם צריכים להגדיר הוספות, עדכונים או מחיקות מורכבים יותר, או אם אתם צריכים לשלוח שאילתות לנתונים במסד הנתונים, השתמשו במקום זאת בפונקציית שאילתה.
הוספה
ההערה @Insert מאפשרת להגדיר פונקציות שמוסיפות את הפרמטרים שלהן לטבלה המתאימה במסד הנתונים. בדוגמה הבאה מוצגות פונקציות @Insert תקינות שמכניסות אובייקט User אחד או יותר למסד הנתונים:
@Dao interface UserDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertUsers(vararg users: User) @Insert suspend fun insertBothUsers(user1: User, user2: User) @Insert suspend fun insertUsersAndFriends(user: User, friends: List<User>) }
כל פרמטר של פונקציה @Insert חייב להיות מופע של מחלקת ישויות נתונים של Room עם ההערה @Entity או אוסף של מופעים של מחלקת ישויות נתונים. כשקוראים לפונקציה @Insert, Room מוסיפה כל מופע של ישות שהועבר לטבלת מסד הנתונים המתאימה.
אם הפונקציה @Insert מקבלת פרמטר יחיד, היא יכולה להחזיר ערך Long
שהוא rowId חדש עבור הפריט שנוסף. אם הפרמטר הוא מערך או אוסף, הפונקציה צריכה להחזיר מערך או אוסף של ערכי Long, כאשר כל ערך הוא rowId של אחד מהפריטים שנוספו.
מידע נוסף על החזרת ערכים של rowId זמין במאמרי העזרה בנושא הערת @Insert ובמאמרי העזרה של SQLite בנושא טבלאות rowid.
עדכון
ההערה @Update מאפשרת להגדיר פונקציות שמעדכנות שורות ספציפיות בטבלת מסד נתונים. בדומה לפונקציות @Insert, פונקציות @Update מקבלות מופעים של ישויות נתונים כפרמטרים. בדוגמה הבאה אפשר לראות פונקציה @Update שמנסה לעדכן אובייקט User אחד או יותר במסד הנתונים:
@Dao interface UserDao { @Update suspend fun updateUsers(vararg users: User) }
Room משתמשת במפתח ראשי כדי להתאים מופעים של ישויות בארגומנטים לשורות במסד הנתונים. אם אין שורה עם אותו מפתח ראשי, לא מתבצעים שינויים ב-Room.
פונקציית @Update יכולה להחזיר ערך Int שמציין את מספר השורות שעודכנו בהצלחה.
מחיקה
ההערה @Delete מאפשרת להגדיר פונקציות שמוחקות שורות ספציפיות מטבלת מסד נתונים. בדומה לפונקציות @Insert, פונקציות @Delete מקבלות מופעים של ישויות נתונים כפרמטרים. בדוגמה הבאה מוצגת פונקציה @Delete שמנסה למחוק אובייקט User אחד או יותר מהמסד הנתונים:
@Dao interface UserDao { @Delete suspend fun deleteUsers(vararg users: User) }
Room משתמשת במפתח ראשי כדי להתאים מופעים של ישויות בארגומנטים לשורות במסד הנתונים. אם אין שורה עם אותו מפתח ראשי, לא מתבצעים שינויים ב-Room.
פונקציית @Delete יכולה להחזיר ערך Int שמציין את מספר השורות שנמחקו בהצלחה.
Upsert
ההערה @Upsert מאפשרת להגדיר פונקציות שמוסיפות מופעים של ישויות כשאין שורה תואמת, או מעדכנות אותם אם כבר קיימת שורה עם אותו מפתח ראשי.
בדומה לפונקציות @Insert ו-@Update, פונקציות @Upsert מקבלות מופעים של ישויות נתונים כפרמטרים. בדוגמה הבאה מוצגת פונקציה @Upsert שמנסה לבצע פעולת upsert באובייקט User אחד או יותר במסד הנתונים:
@Dao interface UserDao { @Upsert suspend fun upsertUsers(vararg users: User) }
אם הפונקציה @Upsert מקבלת פרמטר יחיד, היא יכולה להחזיר ערך Long. אם הפעולה גורמת להוספת שורה חדשה, הפונקציה מחזירה את rowId של השורה החדשה. אם הפעולה גורמת לעדכון של שורה קיימת, הפונקציה מחזירה את הערך
-1. אם הפרמטר הוא מערך או אוסף, הפונקציה צריכה להחזיר מערך או אוסף של ערכי Long במקום זאת.
פונקציות של שאילתות
ההערה @Query מאפשרת לכתוב הצהרות SQL ולחשוף אותן כפונקציות DAO. אפשר להשתמש בפונקציות השאילתה האלה כדי לשלוח שאילתות לנתונים ממסד הנתונים של האפליקציה, או כשצריך לבצע פעולות מורכבות יותר של הוספה, עדכון ומחיקה.
הספרייה Room מאמתת שאילתות SQL בזמן ההידור. המשמעות היא שאם יש בעיה בשאילתה, מתרחשת שגיאת קומפילציה במקום כשל בזמן הריצה.
שאילתות פשוטות
הקוד הבא מגדיר פונקציה שמשתמשת בשאילתת SELECT כדי להחזיר את כל אובייקטי User במסד הנתונים:
@Query("SELECT * FROM user") suspend fun loadAllUsers(): List<User>
בסעיפים הבאים מוסבר איך לשנות את הדוגמה הזו לתרחישי שימוש אופייניים.
החזרת קבוצת משנה של עמודות בטבלה
ברוב המקרים, צריך להחזיר רק קבוצת משנה של העמודות מהטבלה שמבצעים עליה שאילתה. לדוגמה, בממשק המשתמש יכול להיות שיוצגו רק השם הפרטי ושם המשפחה של המשתמש, במקום כל הפרטים שלו. כדי לחסוך במשאבים ולייעל את הביצוע של השאילתה, כדאי לשלוח שאילתות רק לגבי המאפיינים שאתם צריכים.
ספריית Room מאפשרת להחזיר אובייקט נתונים מכל אחת מהשאילתות שלכם, כל עוד אתם יכולים למפות את קבוצת עמודות התוצאות לאובייקט שמוחזר. לדוגמה, אפשר להגדיר את האובייקט הבא כדי להכיל את השם הפרטי ושם המשפחה של המשתמש:
data class NameTuple( @ColumnInfo(name = "first_name") val firstName: String, @ColumnInfo(name = "last_name") val lastName: String )
אחר כך, אפשר להחזיר את אובייקט הנתונים הזה מפונקציית השאילתה:
@Query("SELECT first_name, last_name FROM user") suspend fun loadFullName(): List<NameTuple>
מכיוון שהשאילתה מחזירה ערכים לעמודות first_name ו-last_name, Room ממפה את הערכים האלה למאפיינים במחלקה NameTuple. אם השאילתה מחזירה עמודה שלא ממופה למאפיין באובייקט שמוחזר, Room מציגה אזהרה.
בדוגמה הקודמת נעשה שימוש במחלקת נתונים בהתאמה אישית כדי לאחזר קבוצת משנה של עמודות, אבל Room תומכת גם בהחזרת kotlin.Pair ו-kotlin.Triple לנוחותכם, כששאילתה מחזירה בדיוק שתי עמודות או שלוש עמודות. כשמשתמשים בסוגים האלה, המיפוי של העמודות מתבצע לפי הסדר שבו הן מוגדרות בהצהרת השאילתה, ולכן הסדר של העמודות בהצהרת SELECT צריך להיות זהה לסדר של הסוגים בהצהרת Pair או Triple.
העברה של פרמטרים פשוטים לשאילתה
ברוב המקרים, הפונקציות של ה-DAO צריכות לקבל פרמטרים כדי לבצע פעולות סינון. ספריית Room תומכת בשימוש בפרמטרים של פונקציות כפרמטרים של bind בשאילתות.
לדוגמה, הקוד הבא מגדיר פונקציה שמחזירה את כל המשתמשים מעל גיל מסוים:
@Query("SELECT * FROM user WHERE age > :minAge") suspend fun loadAllUsersOlderThan(minAge: Int): Array<User>
אפשר גם להעביר כמה פרמטרים או להפנות לאותו פרמטר כמה פעמים בשאילתה, כמו בדוגמה הבאה:
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge") suspend fun loadAllUsersBetweenAges(minAge: Int, maxAge: Int): Array<User> @Query( """ SELECT * FROM user WHERE first_name LIKE :search OR last_name LIKE :search """ ) suspend fun findUserWithName(search: String): List<User>
העברת אוסף של פרמטרים לשאילתה
יכול להיות שחלק מהפונקציות של DAO ידרשו העברה של מספר משתנה של פרמטרים שלא ידוע עד זמן הריצה. אם פרמטר מייצג אוסף, הוא מורחב באופן אוטומטי בזמן הריצה על סמך מספר הערכים.
לדוגמה, הקוד הבא מגדיר פונקציה שמחזירה מידע על כל המשתמשים מקבוצת משנה של אזורים:
@Query("SELECT * FROM user WHERE region IN (:regions)") suspend fun loadUsersFromRegions(regions: List<String>): List<User>
הרצת שאילתות על יותר מטבלה אחת
יכול להיות שחלק מהשאילתות שלכם ידרשו גישה לכמה טבלאות כדי לחשב את התוצאה. אפשר להשתמש בסעיפי JOIN בשאילתות SQL כדי להפנות ליותר מטבלה אחת.
הקוד הבא מגדיר פונקציה שמצטרפת לשלוש טבלאות כדי להחזיר את הספרים שמושאלים כרגע למשתמש ספציפי:
@Query( """ SELECT * FROM book INNER JOIN loan ON loan.book_id = book.id INNER JOIN user ON user.id = loan.user_id WHERE user.name LIKE :userName """ ) suspend fun findBooksBorrowedByName(userName: String): List<Book>
אפשר גם להגדיר אובייקטים של נתונים כדי להחזיר קבוצת משנה של עמודות מכמה טבלאות שמצורפות. מידע נוסף זמין במאמר החזרת קבוצת משנה של עמודות בטבלה. הקוד הבא מגדיר DAO עם פונקציה שמחזירה את שמות המשתמשים ואת שמות הספרים שהם השאילו:
interface UserBookDao { @Query( """ SELECT user.name AS userName, book.name AS bookName FROM user, book WHERE user.id = book.user_id """ ) fun loadUserAndBookNames(): Flow<List<UserBook>> } data class UserBook(val userName: String, val bookName: String)
החזרת מפה מרובת שכבות
בפעולות של צירוף, אפשר גם לשלוח שאילתות לגבי עמודות מכמה טבלאות בלי להגדיר מחלקה נוספת של נתונים, על ידי כתיבת פונקציות של שאילתות שמחזירות multimap.
נשתמש בדוגמה מתוך הרצת שאילתות על יותר מטבלה אחת. במקום להחזיר רשימה של מופעים של מחלקת נתונים מותאמת אישית שמכילה זוגות של מופעים של User ושל Book, אפשר להחזיר מיפוי של User ושל Book ישירות מפונקציית השאילתה:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNames(): Map<User, List<Book>>
כשפונקציית השאילתה מחזירה multimap, אפשר לכתוב שאילתות שמשתמשות בתנאי GROUP BY, וכך לנצל את היכולות של SQL לחישובים מתקדמים ולסינון. לדוגמה, אפשר לשנות את הפונקציה loadUserAndBookNames כך שתחזיר רק משתמשים עם שלוש ספרים או יותר בהשאלה:
@Query( """ SELECT * FROM user JOIN book ON user.id = book.user_id GROUP BY user.name HAVING COUNT(book.id) >= 3 """ ) suspend fun loadUserAndBookNamesGrouped(): Map<User, List<Book>>
אם לא צריך למפות אובייקטים שלמים, אפשר גם להחזיר מיפויים בין עמודות ספציפיות בשאילתה באמצעות ההערה @MapColumn בפרמטרים הגנריים של סוג ההחזרה.
@Query( """ SELECT user.name AS username, book.name AS bookname FROM user JOIN book ON user.id = book.user_id """ ) suspend fun loadUserAndBookNamesColumns(): Map< @MapColumn(columnName = "username") String, List<@MapColumn(columnName = "bookname") String> >
סוגים מיוחדים של החזרות
Room מספק כמה סוגים מיוחדים של ערכי החזרה לשילוב עם ספריות API אחרות.
שאילתות עם חלוקה לדפים באמצעות ספריית ה-Paging
Room תומכת בשאילתות עם חלוקה לדפים באמצעות שילוב עם ספריית החלוקה לדפים. כדי להשתמש בסוגי ההחזרה של Paging 3, צריך לרשום את הממירים של סוג ההחזרה Paging במסד הנתונים או ב-DAO:
- כוללים את ארטיפקט
androidx.room3:room3-pagingבהגדרות של הבנייה. - מוסיפים הערות להצהרה לגבי
@Databaseאו@Daoבאמצעות@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class).
אחרי הרישום, אובייקטים של PagingSource יכולים לחזור מ-DAO לשימוש עם Paging 3:
@Dao @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) interface UserDao { @Query("SELECT * FROM users WHERE label LIKE :query") fun pagingSource(query: String): PagingSource<Int, User> }
מידע נוסף על בחירת פרמטרים של סוגים עבור PagingSource זמין במאמר בחירת סוגי מפתח וערך.
גישה ישירה לחיבור למסד נתונים
אם הלוגיקה של האפליקציה דורשת גישה ישירה ברמה נמוכה לחיבור למסד הנתונים, אפשר להשתמש במקום זאת בממשקי ה-API של Room לחיבור. אפשר לקבל חיבור באמצעות useReaderConnection לפעולות קריאה בלבד או באמצעות useWriterConnection לפעולות כתיבה במופע RoomDatabase, ולהשתמש ב-usePrepared כדי להריץ הצהרות:
val result: List<Pair<Long, String>> = roomDatabase.useReaderConnection { connection -> connection.usePrepared( "SELECT * FROM user WHERE age > :minAge LIMIT 5" ) { stmt -> // Bind arguments if needed stmt.bindLong(1, minAge.toLong()) buildList { // Step through the results while (stmt.step()) { add(stmt.getLong(0) to stmt.getText(1)) } } } }
אם אתם צריכים לבצע עסקאות במסד נתונים ברמה נמוכה ישירות בחיבור, אתם יכולים להשתמש בפונקציות העזר immediateTransaction, deferredTransaction או exclusiveTransaction במופע Transactor בתוך בלוק useWriterConnection:
roomDatabase.useWriterConnection { transactor -> transactor.immediateTransaction { // Perform transactional database operations using transactor } }
לחלופין, אם אתם צריכים לבצע רק פעולות DAO ברמה גבוהה בטרנזקציה, אתם יכולים להשתמש בפונקציות העזר withReadTransaction או withWriteTransaction במופע RoomDatabase:
// Perform transactional read operations (DEFERRED transaction) val userCount = roomDatabase.withReadTransaction { userDao.countUsers() } // Perform transactional write operations (IMMEDIATE transaction) roomDatabase.withWriteTransaction { userDao.insert(newUser) userDao.update(existingUser) }