การบันทึกข้อมูลลงในฐานข้อมูลเหมาะสำหรับการทำซ้ำหรือข้อมูลที่มีโครงสร้าง เช่น ข้อมูลติดต่อ หน้านี้ถือว่าคุณคุ้นเคยกับฐานข้อมูล SQL โดยทั่วไปและจะช่วยคุณเริ่มต้นใช้งานฐานข้อมูล SQLite ใน Android API ที่คุณจะต้องใช้ฐานข้อมูลใน Android จะมีให้ในแพ็กเกจ android.database.sqlite
ข้อควรระวัง: แม้ว่า API เหล่านี้จะมีประสิทธิภาพ แต่ก็อยู่ในระดับที่ค่อนข้างต่ำ รวมถึงต้องใช้เวลาและความพยายามอย่างมากในการใช้งาน
- ไม่มีการยืนยันเวลาคอมไพล์สําหรับการค้นหา SQL ดิบ เมื่อกราฟข้อมูลมีการเปลี่ยนแปลง คุณต้องอัปเดตการค้นหา SQL ที่ได้รับผลกระทบด้วยตนเอง กระบวนการนี้อาจใช้เวลานานและเกิดข้อผิดพลาดได้ง่าย
- คุณต้องใช้โค้ดต้นแบบจำนวนมากเพื่อแปลงการค้นหา SQL และออบเจ็กต์ข้อมูล
ด้วยเหตุนี้ เราจึงแนะนําอย่างยิ่งให้ใช้ไลบรารีความต่อเนื่องของห้องเป็นเลเยอร์ Abstraction ในการเข้าถึงข้อมูลในฐานข้อมูล SQLite ของแอป
กำหนดสคีมาและสัญญา
หลักการหลักอย่างหนึ่งของฐานข้อมูล SQL คือสคีมา ซึ่งเป็นประกาศอย่างเป็นทางการเกี่ยวกับวิธีจัดระเบียบฐานข้อมูล สคีมาจะแสดงในคำสั่ง SQL ที่คุณใช้สร้างฐานข้อมูล คุณอาจพบว่าการสร้างคลาสที่แสดงร่วมกันที่เรียกว่าคลาส contract ซึ่งระบุเลย์เอาต์ของสคีมาอย่างชัดเจนในแบบที่เป็นระบบและมีการจัดทำเอกสารด้วยตนเอง
คลาสสัญญาคือคอนเทนเนอร์สำหรับค่าคงที่ที่กำหนดชื่อสำหรับ URI, ตาราง และคอลัมน์ คลาสสัญญาจะช่วยให้คุณใช้ค่าคงที่เดียวกันในคลาสอื่นๆ ทั้งหมดในแพ็กเกจเดียวกันได้ ซึ่งจะช่วยให้คุณเปลี่ยนชื่อคอลัมน์ได้ในที่เดียวและเผยแพร่การเปลี่ยนแปลงนั้นทั่วทั้งโค้ด
วิธีที่ดีในการจัดระเบียบคลาสสัญญาคือการใส่คำจำกัดความส่วนกลางของฐานข้อมูลทั้งหมดไว้ที่ระดับรากของคลาส จากนั้นสร้างคลาสภายในสำหรับแต่ละตาราง คลาสภายในแต่ละคลาสจะแจกแจงคอลัมน์ของตารางที่เกี่ยวข้อง
หมายเหตุ: การใช้อินเทอร์เฟซ BaseColumns
จะทำให้คลาสภายในของคุณรับค่าช่องคีย์หลักชื่อ _ID
ที่คลาส Android บางคลาส เช่น CursorAdapter
คาดว่าจะมี ไม่จำเป็น แต่วิธีนี้จะช่วยให้ฐานข้อมูล
ทำงานร่วมกับเฟรมเวิร์ก Android ได้อย่างกลมกลืน
เช่น สัญญาต่อไปนี้จะกำหนดชื่อตารางและชื่อคอลัมน์สำหรับตารางเดียวที่แสดงฟีด RSS
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"
}
}
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
เมื่อกําหนดลักษณะของฐานข้อมูลแล้ว คุณควรใช้เมธอดที่สร้างและดูแลรักษาฐานข้อมูลและตาราง ตัวอย่างคำสั่งทั่วไปที่สร้างและลบตารางมีดังนี้
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}"
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
ที่ใช้คำสั่งบางส่วนที่แสดงด้านบน
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"
}
}
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
val dbHelper = FeedReaderDbHelper(context)
FeedReaderDbHelper dbHelper = new FeedReaderDbHelper(getContext());
ใส่ข้อมูลลงในฐานข้อมูล
แทรกข้อมูลลงในฐานข้อมูลโดยการส่งออบเจ็กต์ ContentValues
ไปยังเมธอด insert()
ดังนี้
// 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)
// 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()
คือชื่อตาราง
อาร์กิวเมนต์ที่ 2 บอกเฟรมเวิร์กว่าต้องทำอะไรในกรณีที่ ContentValues
ว่างเปล่า (กล่าวคือ คุณไม่ได้put
ค่าใดๆ)
หากคุณระบุชื่อคอลัมน์ เฟรมเวิร์กจะแทรกแถวและตั้งค่าของคอลัมน์นั้นเป็น Null หากคุณระบุ null
อย่างเช่นในตัวอย่างโค้ดนี้ เฟรมเวิร์กจะไม่แทรกแถวเมื่อไม่มีค่า
เมธอด insert()
จะแสดงผลรหัสของแถวที่สร้างขึ้นใหม่ หรือจะแสดงผล -1 หากเกิดข้อผิดพลาดในการแทรกข้อมูล ปัญหานี้อาจเกิดขึ้นได้หากมีข้อขัดแย้งกับข้อมูลที่มีอยู่แล้วในฐานข้อมูล
อ่านข้อมูลจากฐานข้อมูล
หากต้องการอ่านจากฐานข้อมูล ให้ใช้เมธอด query()
โดยส่งเกณฑ์การเลือกและคอลัมน์ที่ต้องการ
วิธีการนี้รวมองค์ประกอบของ insert()
และ update()
ยกเว้นรายการคอลัมน์ ซึ่งจะกำหนดข้อมูลที่ต้องการดึงข้อมูล ("การโปรเจ็กต์") ไม่ใช่ข้อมูลที่จะแทรก ระบบจะแสดงผลการค้นหาให้คุณเห็นในออบเจ็กต์ Cursor
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
)
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
);
อาร์กิวเมนต์ที่ 3 และ 4 (selection
และ selectionArgs
) จะรวมเข้าด้วยกันเพื่อสร้างคำสั่ง WHERE เนื่องจากอาร์กิวเมนต์มีให้แยกต่างหากจากคำค้นหาการเลือก อาร์กิวเมนต์จึงถูก Escape ก่อนที่จะรวมเข้าด้วยกัน ซึ่งจะทำให้คำสั่งการเลือกของคุณไม่ได้รับผลกระทบจากการแทรก SQL ดูรายละเอียดเพิ่มเติมเกี่ยวกับอาร์กิวเมนต์ทั้งหมดได้ที่ข้อมูลอ้างอิงquery()
หากต้องการดูแถวในเคอร์เซอร์ ให้ใช้วิธีย้าย Cursor
วิธีใดวิธีหนึ่ง ซึ่งต้องเรียกใช้ทุกครั้งก่อนที่จะเริ่มอ่านค่า เนื่องจากเคอร์เซอร์เริ่มต้นที่ตำแหน่ง -1 การเรียกใช้ moveToNext()
จึงจะวาง "ตำแหน่งการอ่าน" บนรายการแรกในผลลัพธ์ และแสดงผลว่าเคอร์เซอร์เลยรายการสุดท้ายในชุดผลลัพธ์ไปแล้วหรือไม่ สําหรับแต่ละแถว คุณสามารถอ่านค่าของคอลัมน์ได้โดยเรียกใช้Cursor
เมธอด get อย่างใดอย่างหนึ่ง เช่น getString()
หรือ getLong()
สําหรับเมธอด get แต่ละรายการ คุณจะต้องส่งตําแหน่งอินเด็กซ์ของคอลัมน์ที่ต้องการ ซึ่งคุณรับได้โดยเรียกใช้ getColumnIndex()
หรือ getColumnIndexOrThrow()
เมื่อเรียกใช้ผลลัพธ์จนเสร็จแล้ว ให้เรียกใช้ close()
บนเคอร์เซอร์เพื่อปล่อยทรัพยากร
ตัวอย่างต่อไปนี้แสดงวิธีรับรหัสสินค้าทั้งหมดที่จัดเก็บไว้ในเคอร์เซอร์ และเพิ่มรหัสดังกล่าวลงในรายการ
val itemIds = mutableListOf<Long>()
with(cursor) {
while (moveToNext()) {
val itemId = getLong(getColumnIndexOrThrow(BaseColumns._ID))
itemIds.add(itemId)
}
}
cursor.close()
List itemIds = new ArrayList<>();
while(cursor.moveToNext()) {
long itemId = cursor.getLong(
cursor.getColumnIndexOrThrow(FeedEntry._ID));
itemIds.add(itemId);
}
cursor.close();
ลบข้อมูลออกจากฐานข้อมูล
หากต้องการลบแถวออกจากตาราง คุณต้องระบุเกณฑ์การเลือกที่จะระบุแถวให้กับเมธอด delete()
กลไกนี้ทำงานเหมือนกับอาร์กิวเมนต์การเลือกของเมธอด query()
โดยจะแบ่งข้อกําหนดการเลือกออกเป็นคำสั่งการเลือกและอาร์กิวเมนต์การเลือก ข้อกำหนดเหล่านี้จะกำหนดคอลัมน์ที่จะดูข้อมูลและยังช่วยให้คุณรวมการทดสอบคอลัมน์ได้ด้วย อาร์กิวเมนต์คือค่าที่จะทดสอบซึ่งเชื่อมโยงกับประโยค
เนื่องจากผลลัพธ์ไม่ได้รับการจัดการเหมือนกับคำสั่ง SQL ปกติ จึงมีภูมิคุ้มกันต่อการแทรก SQL
// 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)
// 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);
ค่าที่ส่งกลับสำหรับเมธอด delete()
จะแสดงจำนวนแถวที่ถูกลบออกจากฐานข้อมูล
อัปเดตฐานข้อมูล
เมื่อต้องการแก้ไขค่าฐานข้อมูลชุดย่อย ให้ใช้เมธอด update()
การอัปเดตตารางจะรวมไวยากรณ์ ContentValues
ของ insert()
เข้ากับไวยากรณ์ WHERE
ของ delete()
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)
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()
ของกิจกรรมการเรียกใช้จะเป็นการดีที่สุด
override fun onDestroy() {
dbHelper.close()
super.onDestroy()
}
@Override
protected void onDestroy() {
dbHelper.close();
super.onDestroy();
}
แก้ไขข้อบกพร่องของฐานข้อมูล
Android SDK มีเครื่องมือ Shell ของ sqlite3
ที่ช่วยให้คุณเรียกดูเนื้อหาตาราง, เรียกใช้คำสั่ง SQL และใช้ฟังก์ชันอื่นๆ ที่เป็นประโยชน์ในฐานข้อมูล SQLite ดูข้อมูลเพิ่มเติมได้ที่วิธีออกคำสั่ง Shell