שמירת נתונים באמצעות SQLite

שמירת נתונים במסד נתונים אידיאלית לחזרה או לנתונים מובְנים, כמו פרטים ליצירת קשר. דף זה מניח שאתם להכיר את מסדי נתונים של SQL באופן כללי, ועוזר לך להתחיל מסדי נתונים של SQLite ב-Android. ממשקי ה-API הדרושים לשימוש במסד נתונים ב-Android זמינים בחבילה של android.database.sqlite.

זהירות: ממשקי ה-API האלה חזקים, אבל הם ברמה נמוכה למדי ונדרשים הרבה זמן ומאמצים כדי להשתמש בהם:

  • אין אימות בזמן הידור של שאילתות SQL גולמיות. לפי הנתונים שלך צריך לעדכן באופן ידני את שאילתות ה-SQL המושפעות. הזה התהליך עשוי להימשך זמן רב ולהביא לשגיאות.
  • צריך להשתמש בהרבה קוד סטנדרטי כדי להמיר שאילתות SQL ואובייקטים של נתונים.

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

הגדרת סכימה וחוזה

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

מחלקה של חוזה היא קונטיינר של קבועים שמגדירים שמות למזהי URI, הטבלאות והעמודות. מחלקה של החוזה מאפשרת להשתמש באותם קבועים בכל הכיתות האחרות באותה חבילה. כך אפשר לשנות עמודה במקום אחד, כדי שיופץ לאורך כל הקוד.

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

הערה: באמצעות הטמעת BaseColumns המחלקה הפנימית שלכם יכולה לרשת שדה מפתח שנקרא _ID, שחלק ממחלקות Android כמו CursorAdapter מצפים לקבל. לא חובה, אבל זה יכול לעזור לך לשפר את מסד הנתונים פועלים בהרמוניה עם framework של 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 helper

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

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, צריך ליצור מחלקה משנית מבטל את 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 ערכים). אם מציינים שם של עמודה, המסגרת מוסיפה שורה ומגדירה שהערך של העמודה הזו הוא 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 methods, שצריך תמיד לקרוא להן לפני שמתחילים לקרוא את הערכים. מאחר שהסמן מתחיל ב- מיקום 1-, קריאה אל moveToNext() מציבה את 'מיקום הקריאה' ב הכניסה הראשונה בתוצאות, ומחזירה אם הסמן כבר עבר את הערך האחרון והתוצאה מוגדרת. בכל שורה אפשר לקרוא את הערך של עמודה מסוימת באמצעות Cursor אחזור שיטות, כמו getString() או getLong(). בכל אחת משיטות השליפה עליך להעביר את מיקום האינדקס של העמודה הרצויה, ואפשר לקבל אותה באמצעות קריאה 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() מציין את מספר השורות שנמחקו ממסד הנתונים.

עדכון מסד נתונים

כשצריך לשנות קבוצת משנה של ערכי מסד הנתונים, אפשר להשתמש אמצעי תשלום אחד (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 מסדי נתונים. מידע נוסף זמין במאמר איך להנפיק פקודות מעטפת.