שמירת נתונים במסד הנתונים אידיאלית לחזרה על נתונים או לנתונים מובְנים, כמו פרטים ליצירת קשר. בדף הזה אנו נניח שאתם מכירים את מסדי הנתונים של SQL באופן כללי, ונלמד אתכם איך להתחיל לעבוד עם מסדי נתונים של SQLite ב-Android. ממשקי ה-API הנדרשים לשימוש במסד נתונים ב-Android זמינים בחבילה android.database.sqlite
.
זהירות: למרות שממשקי ה-API האלה חזקים, הם ברמה נמוכה יחסית ונדרשים הרבה זמן ומאמצים כדי להשתמש בהם:
- אין אימות של שאילתות SQL גולמיות בזמן הידור. בהתאם לשינויים בגרף הנתונים, תצטרכו לעדכן באופן ידני את שאילתות ה-SQL המושפעות. התהליך הזה יכול להיות זמן רב ומועד לשגיאות.
- צריך להשתמש בהרבה קוד סטנדרטי כדי להמיר שאילתות SQL ואובייקטים של נתונים.
לכן, מומלץ מאוד להשתמש ב-Room Persistence Library בתור שכבת הפשטה של גישה למידע במסדי הנתונים של SQLite באפליקציה.
הגדרת הסכמה וחוזה
אחד מהעקרונות המרכזיים במסדי נתונים של SQL הוא הסכימה: הצהרה רשמית על האופן שבו מסד הנתונים מאורגן. הסכימה משתקפת בהצהרות ה-SQL שבהן משתמשים כדי ליצור את מסד הנתונים. כדאי ליצור מחלקה נלווית, שנקראת מחלקה, שמציינת במפורש את פריסת הסכימה באופן שיטתי עם תיעוד עצמי.
סוג חוזה הוא מאגר של קבועים שמגדירים שמות למזהי URI, לטבלאות ולעמודות. באמצעות כיתה החוזה אפשר להשתמש באותם ערכי קבועים בכל הכיתות האחרות באותה חבילה. כך תוכלו לשנות את שם העמודה במקום אחד ולהפיץ אותו בקוד.
דרך טובה לארגן את הכיתה של החוזה היא להציב הגדרות גלובאליות לכל מסד הנתונים ברמת הבסיס של הכיתה. לאחר מכן יוצרים סיווג פנימי לכל טבלה. כל מחלקה פנימית סופרת את העמודות המתאימות בטבלה.
הערה: כשמטמיעים את הממשק BaseColumns
, המחלקה הפנימית יכולה לרשת שדה מפתח ראשי שנקרא _ID
, שמחלקות Android מסוימות כמו CursorAdapter
מצפים לקבל. לא חובה לעשות זאת, אבל זה יכול לעזור למסד הנתונים לפעול בצורה הרמונית עם מסגרת Android.
לדוגמה, החוזה הבא מגדיר את שם הטבלה ואת שמות העמודות של טבלה אחת שמייצגת פיד RSS:
Kotlin
object FeedReaderContract { // Table contents are grouped together in an anonymous object. object FeedEntry : BaseColumns { const val TABLE_NAME = "entry" const val COLUMN_NAME_TITLE = "title" const val COLUMN_NAME_SUBTITLE = "subtitle" } }
Java
public final class FeedReaderContract { // To prevent someone from accidentally instantiating the contract class, // make the constructor private. private FeedReaderContract() {} /* Inner class that defines the table contents */ public static class FeedEntry implements BaseColumns { public static final String TABLE_NAME = "entry"; public static final String COLUMN_NAME_TITLE = "title"; public static final String COLUMN_NAME_SUBTITLE = "subtitle"; } }
יצירת מסד נתונים באמצעות כלי עזר של SQL
אחרי שקובעים את המראה של מסד הנתונים, צריך להטמיע שיטות ליצירה ולתחזוקה של מסד הנתונים והטבלאות. ריכזנו כאן כמה הצהרות אופייניות ליצירה ולמחיקה של טבלה:
Kotlin
private const val SQL_CREATE_ENTRIES = "CREATE TABLE ${FeedEntry.TABLE_NAME} (" + "${BaseColumns._ID} INTEGER PRIMARY KEY," + "${FeedEntry.COLUMN_NAME_TITLE} TEXT," + "${FeedEntry.COLUMN_NAME_SUBTITLE} TEXT)" private const val SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS ${FeedEntry.TABLE_NAME}"
Java
private static final String SQL_CREATE_ENTRIES = "CREATE TABLE " + FeedEntry.TABLE_NAME + " (" + FeedEntry._ID + " INTEGER PRIMARY KEY," + FeedEntry.COLUMN_NAME_TITLE + " TEXT," + FeedEntry.COLUMN_NAME_SUBTITLE + " TEXT)"; private static final String SQL_DELETE_ENTRIES = "DROP TABLE IF EXISTS " + FeedEntry.TABLE_NAME;
בדיוק כמו קבצים ששומרים באחסון הפנימי של המכשיר, Android מאחסן את מסד הנתונים בתיקייה הפרטית של האפליקציה. הנתונים שלכם מאובטחים כי כברירת מחדל לאפליקציות אחרות או למשתמש אין גישה לאזור הזה.
המחלקה SQLiteOpenHelper
מכילה קבוצה שימושית של ממשקי API לניהול מסד הנתונים.
כשמשתמשים במחלקה הזו כדי לקבל הפניות למסד הנתונים, המערכת מבצעת את הפעולות
הממושכות, של יצירה ועדכון של מסד הנתונים, רק כשיש צורך בכך ולא במהלך ההפעלה של האפליקציה. כל מה שצריך לעשות הוא להתקשר למספר getWritableDatabase()
או getReadableDatabase()
.
הערה: מכיוון שהן יכולות להיות ממושכות, חשוב להקפיד להפעיל את getWritableDatabase()
או getReadableDatabase()
בשרשור ברקע.
מידע נוסף זמין במאמר שימוש בשרשור ב-Android.
כדי להשתמש ב-SQLiteOpenHelper
, יוצרים Subclass שמחליף את שיטות ה-Callback onCreate()
ו-onUpgrade()
. מומלץ גם להטמיע את השיטות onDowngrade()
או onOpen()
, אבל אין חובה לעשות זאת.
לדוגמה, הנה הטמעה של SQLiteOpenHelper
שמשתמשת בחלק מהפקודות שלמעלה:
Kotlin
class FeedReaderDbHelper(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) { override fun onCreate(db: SQLiteDatabase) { db.execSQL(SQL_CREATE_ENTRIES) } override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { // This database is only a cache for online data, so its upgrade policy is // to simply to discard the data and start over db.execSQL(SQL_DELETE_ENTRIES) onCreate(db) } override fun onDowngrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { onUpgrade(db, oldVersion, newVersion) } companion object { // If you change the database schema, you must increment the database version. const val DATABASE_VERSION = 1 const val DATABASE_NAME = "FeedReader.db" } }
Java
public class FeedReaderDbHelper extends SQLiteOpenHelper { // If you change the database schema, you must increment the database version. public static final int DATABASE_VERSION = 1; public static final String DATABASE_NAME = "FeedReader.db"; public FeedReaderDbHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); } public void onCreate(SQLiteDatabase db) { db.execSQL(SQL_CREATE_ENTRIES); } public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // This database is only a cache for online data, so its upgrade policy is // to simply to discard the data and start over db.execSQL(SQL_DELETE_ENTRIES); onCreate(db); } public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { onUpgrade(db, oldVersion, newVersion); } }
כדי לגשת למסד הנתונים, יוצרים מופע של מחלקה משנית של SQLiteOpenHelper
:
Kotlin
val dbHelper = FeedReaderDbHelper(context)
Java
FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());
הכנסת מידע למסד נתונים
כדי להוסיף נתונים למסד הנתונים, מעבירים אובייקט ContentValues
לשיטה insert()
:
Kotlin
// Gets the data repository in write mode val db = dbHelper.writableDatabase // Create a new map of values, where column names are the keys val values = ContentValues().apply { put(FeedEntry.COLUMN_NAME_TITLE, title) put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle) } // Insert the new row, returning the primary key value of the new row val newRowId = db?.insert(FeedEntry.TABLE_NAME, null, values)
Java
// Gets the data repository in write mode SQLiteDatabase db = dbHelper.getWritableDatabase(); // Create a new map of values, where column names are the keys ContentValues values = new ContentValues(); values.put(FeedEntry.COLUMN_NAME_TITLE, title); values.put(FeedEntry.COLUMN_NAME_SUBTITLE, subtitle); // Insert the new row, returning the primary key value of the new row long newRowId = db.insert(FeedEntry.TABLE_NAME, null, values);
הארגומנט הראשון של insert()
הוא פשוט שם הטבלה.
הארגומנט השני מורה למסגרת מה לעשות במקרה ש-ContentValues
ריק (כלומר, לא הקצית ערכים ל-put
).
אם מציינים את שם העמודה, ה-framework יוסיף שורה ומגדיר את הערך של העמודה הזו כ-null. אם מציינים את null
, כמו בדוגמת הקוד הזו, ה-framework לא מוסיף שורה כשאין ערכים.
רכיב ה-methods insert()
יחזיר את המזהה של השורה החדשה, או את הערך 1 אם הייתה שגיאה בהוספת הנתונים. המצב הזה יכול לקרות אם יש לכם התנגשויות עם נתונים קיימים במסד הנתונים.
קריאת מידע ממסד נתונים
כדי לקרוא ממסד נתונים, משתמשים בשיטה query()
ומעבירים לה את קריטריוני הבחירה ואת העמודות הרצויות.
השיטה משלבת רכיבים של insert()
ו-update()
, אבל רשימת העמודות מגדירה את הנתונים שרוצים לאחזר ('ההיטל'), ולא את הנתונים שרוצים להוסיף. התוצאות של השאילתה מוחזר לכם באובייקט Cursor
.
Kotlin
val db = dbHelper.readableDatabase // Define a projection that specifies which columns from the database // you will actually use after this query. val projection = arrayOf(BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE) // Filter results WHERE "title" = 'My Title' val selection = "${FeedEntry.COLUMN_NAME_TITLE} = ?" val selectionArgs = arrayOf("My Title") // How you want the results sorted in the resulting Cursor val sortOrder = "${FeedEntry.COLUMN_NAME_SUBTITLE} DESC" val cursor = db.query( FeedEntry.TABLE_NAME, // The table to query projection, // The array of columns to return (pass null to get all) selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order )
Java
SQLiteDatabase db = dbHelper.getReadableDatabase(); // Define a projection that specifies which columns from the database // you will actually use after this query. String[] projection = { BaseColumns._ID, FeedEntry.COLUMN_NAME_TITLE, FeedEntry.COLUMN_NAME_SUBTITLE }; // Filter results WHERE "title" = 'My Title' String selection = FeedEntry.COLUMN_NAME_TITLE + " = ?"; String[] selectionArgs = { "My Title" }; // How you want the results sorted in the resulting Cursor String sortOrder = FeedEntry.COLUMN_NAME_SUBTITLE + " DESC"; Cursor cursor = db.query( FeedEntry.TABLE_NAME, // The table to query projection, // The array of columns to return (pass null to get all) selection, // The columns for the WHERE clause selectionArgs, // The values for the WHERE clause null, // don't group the rows null, // don't filter by row groups sortOrder // The sort order );
הארגומנט השלישי והרביעי (selection
ו-selectionArgs
) משולבים כדי ליצור משפט WHERE. מכיוון שהארגומנטים מסופקים בנפרד משאילתת הבחירה, המערכת תסמן אותם בתו בריחה (escape) לפני השילוב. כך הצהרות הבחירה שלכם חסנות להזרקת SQL. למידע נוסף על כל הארגומנטים, ראו query()
.
כדי להסתכל על שורה בסמן, משתמשים באחת משיטות ההעברה Cursor
, שתמיד צריך לקרוא להן לפני שמתחילים לקרוא ערכים. מכיוון שהסמן מתחיל במיקום 1-, קריאה לפונקציה moveToNext()
ממקמת את 'מיקום הקריאה' ברשומה הראשונה בתוצאות ומחזירה את הערך 'true' אם הסמן כבר עבר את הרשומה האחרונה בקבוצת התוצאות, או את הערך 'false' אם לא. בכל שורה אפשר לקרוא את הערך של עמודה על ידי קריאה לאחת מה-methods של Cursor
, כמו getString()
או getLong()
. בכל אחת משיטות ה-get, צריך להעביר את מיקום האינדקס של העמודה הרצויה. אפשר לקבל את המיקום באמצעות קריאה ל-getColumnIndex()
או ל-getColumnIndexOrThrow()
. בסיום החזרה על התוצאות, צריך להפעיל את close()
על הסמן כדי לשחרר את המשאבים שלו.
לדוגמה, הקוד הבא מראה איך לקבל את כל מזהי הפריטים שמאוחסנים בסמן ולהוסיף אותם לרשימה:
Kotlin
val itemIds = mutableListOf<Long>() with(cursor) { while (moveToNext()) { val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID)) itemIds.add(itemId) } } cursor.close()
Java
List itemIds = new ArrayList<>(); while(cursor.moveToNext()) { long itemId = cursor.getLong( cursor.getColumnIndexOrThrow(FeedEntry._ID)); itemIds.add(itemId); } cursor.close();
מחיקת מידע ממסד נתונים
כדי למחוק שורות מטבלה צריך לציין קריטריונים שמזהים את השורות באמצעות method delete()
. המנגנון פועל באותו אופן כמו הארגומנטים של הבחירה בשיטה query()
. הוא מחלק את מפרט הבחירה לסעיף בחירה ולארגומנטים של בחירה. התנאי מגדיר את העמודות שרוצים לבדוק, ומאפשר גם לשלב בדיקות של עמודות. הארגומנטים הם ערכים שצריך לבדוק והם מקושרים לסעיף.
מכיוון שהתוצאה לא מטופלת כמו הצהרת SQL רגילה, היא חסינה להזרקת SQL.
Kotlin
// Define 'where' part of query. val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?" // Specify arguments in placeholder order. val selectionArgs = arrayOf("MyTitle") // Issue SQL statement. val deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs)
Java
// Define 'where' part of query. String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?"; // Specify arguments in placeholder order. String[] selectionArgs = { "MyTitle" }; // Issue SQL statement. int deletedRows = db.delete(FeedEntry.TABLE_NAME, selection, selectionArgs);
הערך המוחזר ל-method delete()
מציין את מספר השורות שנמחקו ממסד הנתונים.
עדכון מסד נתונים
כדי לשנות קבוצת משנה של ערכי מסד הנתונים, אפשר להשתמש ב-method update()
.
עדכון הטבלה משלב את התחביר ContentValues
של insert()
עם התחביר WHERE
של delete()
.
Kotlin
val db = dbHelper.writableDatabase // New value for one column val title = "MyNewTitle" val values = ContentValues().apply { put(FeedEntry.COLUMN_NAME_TITLE, title) } // Which row to update, based on the title val selection = "${FeedEntry.COLUMN_NAME_TITLE} LIKE ?" val selectionArgs = arrayOf("MyOldTitle") val count = db.update( FeedEntry.TABLE_NAME, values, selection, selectionArgs)
Java
SQLiteDatabase db = dbHelper.getWritableDatabase(); // New value for one column String title = "MyNewTitle"; ContentValues values = new ContentValues(); values.put(FeedEntry.COLUMN_NAME_TITLE, title); // Which row to update, based on the title String selection = FeedEntry.COLUMN_NAME_TITLE + " LIKE ?"; String[] selectionArgs = { "MyOldTitle" }; int count = db.update( FeedReaderDbHelper.FeedEntry.TABLE_NAME, values, selection, selectionArgs);
ערך ההחזרה של השיטה update()
הוא מספר השורות שהושפעו במסד הנתונים.
חיבור מתמיד למסד נתונים
קל לקרוא ל-getWritableDatabase()
ול-getReadableDatabase()
כשמסד הנתונים סגור, ולכן צריך להשאיר את החיבור למסד הנתונים פתוח כל עוד אפשר לגשת אליו. בדרך כלל, הכי טוב לסגור את מסד הנתונים ב-onDestroy()
של הפעילות הקוראת.
Kotlin
override fun onDestroy() { dbHelper.close() super.onDestroy() }
Java
@Override protected void onDestroy() { dbHelper.close(); super.onDestroy(); }
ניפוי באגים במסד הנתונים
Android SDK כולל כלי מעטפת sqlite3
שמאפשר לעיין בתוכן של טבלאות, להריץ פקודות SQL ולבצע פונקציות שימושיות אחרות במסדי נתונים של SQLite. מידע נוסף זמין במאמר איך להנפיק פקודות מעטפת.