Android資料庫存儲之一SQLite的基本操作

引言

軟體開發始終無法離開數據存儲,作為開發者,一定不會對資料庫感到陌生。對於性能較弱的移動設備,通常採用一種輕量級的資料庫SQLite,作為解決方案。我們都知道,Android對SQLite提供了支持。但我們在實際的編碼過程中,時常會感到困惑,比如訪問資料庫的時候是否要在子線程中進行;直接拼接SQL語句對資料庫進行操作,與通過ContentResolver對ContentProvider進行訪問有什麼區別,等等。可以這樣說,在Android中對資料庫的操作並沒有統一的標準,實際的操作太靈活,不夠友好,以致於經常給開發者帶來困惑。

帶著我在工作中遇到的這些困惑,加上自己對這些問題的理解,我打算將Android中對SQLite的使用,分為3篇文章來續寫。

  1. SQLite的基本操作
  2. ContentResolver和ContentProvider的使用
  3. AsyncQueryHandler的使用

希望通過這3篇文章,讀者能對Android中資料庫的使用有一個清晰細緻的了解。


SQLite的基本操作

本文將通過一個完整的Demo,告訴讀者,如何在Android中,創建資料庫,完成增刪改查,並討論如何搭建更為合理的項目結構。

首先我們看下效果。我們在一個Activity種實現了數據的CRUD。

接著,我們再來看看項目的基本結構。

我們重點關注database包目錄下的類文件。在本例中,我們創建了一個Student表。我們將Student表相關的代碼,統一放置在database/student包目錄下。假設以後需要創建其它表,例如課程Course表,那麼,我們可以將Course相關的代碼,創建並放置在database/course包目錄下。

了解了項目的基本結構,接下來,我們結合相關的代碼,進一步探討。

首先是資料庫的創建:

database/DBHelper.java

public class DBHelper extends SQLiteOpenHelpern{n /**n * 資料庫的名字n * */n private static final String DATABASE_NAME = "music.db";n /**n * 版本號n * */n private static int DATABASE_VERSION = 1;nn public DBHelper(Context context)n {n super(context, DATABASE_NAME, null, DATABASE_VERSION);n }nn /**n * 用戶首次安裝應用程序,會調用該方法n * */n @Overriden public void onCreate(SQLiteDatabase db)n {n db.execSQL(StudentContact.CREATE_TABLE);nn }nn /**n * 版本號發生變化,會調用該方法n * */n @Overriden public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)n {n //通過版本號來決定如何升級資料庫n }n}n

當資料庫被創建後默認會放置在data/data目錄下,如果你使用的是模擬器,或者你的設備root過,那麼你可以通過Android Studio的DDMS查看該文件。當然,資料庫也可以被創建在SD卡上,但是我們不推薦這樣做,因為SD卡可以被其它應用程序輕鬆訪問到,相對來說,不夠安全。

以上創建資料庫的代碼,放在哪裡比較合適呢?我採用單例DBManager的方式來創建並持有資料庫對象。

database/DBManager.java

public class DBManagern{n /**n * 單例對象本身n * */n private static volatile DBManager dbManager;nn private StudentManager studentManager;nn /**n * 單例暴露給外部調用n * */n public static DBManager getInstance(Context context)n {n synchronized (DBManager.class)n {n if(dbManager == null)n {n dbManager = new DBManager(context);n }n }n return dbManager;n }nn private DBManager(Context context)n {n DBHelper dbHelper = new DBHelper(context);n studentManager = new StudentManager(dbHelper);n }nn /**n * 學生表n * */n public StudentManager getStudentManager()n {n return this.studentManager;n }n}n

可以看到,在這個類中,持有一個StudentManager對象,它負責Student表的所有操作。讓我們看看database/student/*包目錄下的代碼。首先,我們需要定義表結構,即,表中有哪些欄位。

database/student/StudentColumns.java

public interface StudentColumns extends BaseColumnsn{n String SID = "sid";n String NAME = "name";n String AGE = "age";n}n

接著,我們通過靜態類StudentContact,對Student表的創建,刪除,以及CRUD進行包裝。對於數據查詢,返回的是Cursor對象,通過該對象進一步對數據進行查詢。而對於數據的插入和更新,需要被包裝成ContentValues對象。對於數據量的CRUD操作,SQliteDatabase都為我們提供了相應的方法,不僅如此,它還提供了一個叫做execSQL的方法,方便我們直接執行SQL語句,提供了相當大的靈活性。

database/student/StudentContact.java

public class StudentContactn{n /**n * 表名n * */n public static final String TABLE_NAME = "student";nn /**n * 所有的欄位n * */n private static final String[] AVAILABLE_PROJECTION = new String[]{n StudentColumns.SID,n StudentColumns.NAME,n StudentColumns.AGE,n };nn /**n * 創建表的語句n * */n public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + "("n + StudentColumns._ID + " INTEGER PRIMARY KEY,"n + StudentColumns.SID + " TEXT,"n + StudentColumns.NAME + " TEXT,"n + StudentColumns.AGE + " INTEGER"n + ")";nn /**n * 刪除表的語句n * */n public static final String DROP_TABLE = "DROP TABLE IF EXISTS " + TABLE_NAME;nn /**n * 判斷下所請求的欄位是否都存在,分支出現操作的欄位不存在的錯誤n */n private static void checkColumns(String[] projection)n {n if (projection != null)n {n HashSet<String> requestedColumns = new HashSet<String>(Arrays.asList(projection));n HashSet<String> availableColumns = new HashSet<String>(Arrays.asList(AVAILABLE_PROJECTION));n if (!availableColumns.containsAll(requestedColumns))n {n throw new IllegalArgumentException(TAG + "checkColumns()-> Unknown columns in projection");n }n }n }nn /**n * 記錄是否存在n * */n public static boolean isExist(SQLiteOpenHelper helper, String studentId)n {n SQLiteDatabase db = helper.getReadableDatabase();n Cursor cursor = db.query(TABLE_NAME, AVAILABLE_PROJECTION, StudentColumns.SID + " =? ", new String[] {studentId}, null, null, null);n if (cursor.moveToFirst())n {n Log.d(TAG, "緩存存在");n cursor.close();n return true;n }n elsen {n Log.d(TAG, "緩存不存在");n cursor.close();n return false;n }n }nn /**n * 查詢所有的學生n * */n public static List<Student> query(SQLiteOpenHelper helper)n {n SQLiteDatabase db = helper.getReadableDatabase();n Cursor cursor = db.query(TABLE_NAME, AVAILABLE_PROJECTION, null, null, null, null, null, null);n List<Student> students = new ArrayList<>();n while (cursor.moveToNext())n {n students.add(getStudentFromCursor(cursor));n }n cursor.close();n return students;n }nn /**n * 查詢某個學生n */n public static Student query(SQLiteOpenHelper helper, String studentId)n {n SQLiteDatabase db = helper.getReadableDatabase();n Cursor cursor = db.query(TABLE_NAME, AVAILABLE_PROJECTION, StudentColumns.SID + " =? ", new String[] {studentId}, null, null, null, null);n Student student = null;n if (cursor != null)n {n cursor.moveToFirst();n student = getStudentFromCursor(cursor);n }n return student;n }nn /**n * 更新學生對象n * */n public static void update(SQLiteOpenHelper helper, Student student)n {n SQLiteDatabase db = helper.getWritableDatabase();n db.update(TABLE_NAME, toContentValues(student), StudentColumns.SID + " =? ", new String[] {student.id});n }nn /**n * 插入新數據n * */n public static void insert(SQLiteOpenHelper helper, Student student)n {n SQLiteDatabase db = helper.getWritableDatabase();n db.insert(TABLE_NAME, null, toContentValues(student));n }nn /**n * 插入新數據,如果已經存在就替換n * */n public static void insertOrReplace(SQLiteOpenHelper helper, Student student)n {n SQLiteDatabase db = helper.getWritableDatabase();n db.insertWithOnConflict(TABLE_NAME, null, toContentValues(student), SQLiteDatabase.CONFLICT_REPLACE);n }nn /**n * 刪除某條記錄n * */n public static void delete(SQLiteOpenHelper helper, String studentId)n {n SQLiteDatabase db = helper.getWritableDatabase();n db.delete(TABLE_NAME, StudentColumns.SID + " =? ", new String[] {studentId});n }nn /**n * 清空學生表n */n public static void clear(SQLiteOpenHelper helper)n {n SQLiteDatabase db = helper.getWritableDatabase();n db.delete(TABLE_NAME, null, null);n }nn /**n * 將對象保證成ContentValuesn * */n private static ContentValues toContentValues(Student student)n {n ContentValues contentValues = new ContentValues();n contentValues.put(StudentColumns.SID, student.id);n contentValues.put(StudentColumns.NAME, student.name);n contentValues.put(StudentColumns.AGE, student.age);n return contentValues;n }nn /**n * 將學生對象從Cursor中取出n * */n private static Student getStudentFromCursor(Cursor cursor)n {n Student student = new Student();n student.id = cursor.getString(cursor.getColumnIndex(StudentColumns.SID));n student.name = cursor.getString(cursor.getColumnIndex(StudentColumns.NAME));n student.age = cursor.getString(cursor.getColumnIndex(StudentColumns.AGE));n return student;n }n}n

對學生表的操作進行再次包裝,目的有兩個:

  • 對外屏蔽具體的實現。
  • 對傳入的數據的合法性進行檢查和驗證。

database/student/StudentManager.java

public class StudentManagern{n private SQLiteOpenHelper dbHelper;nn public StudentManager(SQLiteOpenHelper dbHelper)n {n this.dbHelper = dbHelper;n }nn /**n * 記錄是否存在n * */n public boolean isExist(String studentId)n {n if(TextUtils.isEmpty(studentId))n {n return false;n }n return StudentContact.isExist(dbHelper, studentId);n }nn /**n * 查詢所有學生n * */n public List<Student> query()n {n return StudentContact.query(dbHelper);n }nn /**n * 查詢某個學生n * */n public Student query(String studentId)n {n if(TextUtils.isEmpty(studentId))n {n return null;n }n return StudentContact.query(dbHelper, studentId);n }nn /**n * 插入一條數據n * */n public void insert(Student student)n {n if(student == null)n {n return;n }n StudentContact.insert(dbHelper, student);n }nn /**n * 插入某條記錄,如果已經存在就覆蓋n * */n public void insertOrReplace(Student student)n {n if(student == null)n {n return;n }n StudentContact.insertOrReplace(dbHelper, student);n }nn /**n * 更新某條記錄n * */n public void update(Student student)n {n if(student == null)n {n return;n }n StudentContact.update(dbHelper, student);n }nn /**n * 刪除某條記錄n * */n public void delete(String studentId)n {n if(TextUtils.isEmpty(studentId))n {n return;n }n StudentContact.delete(dbHelper, studentId);n }nn /**n * 清空表n * */n public void clear()n {n StudentContact.clear(dbHelper);n }n}n

最後,我們回到Activity,在Activity中演示了如何通過單例DBManager,對Student表進行操作。其中所有的操作都需要在子線程中完成。嚴格說來,這不是必須的,因為在UI線程中操作並不是不可以,但是我們不建議在UI線程中操作,因為會讓軟體不夠流暢,影響用戶體驗,而且如果操作的數據量巨大,甚至可能引發ANR錯誤。在本例中,我採用AsyncTask作為子線程。

MainActivity.java

public class MainActivity extends AppCompatActivityn{n /**n * ListView對應的數據源適配器n * */n private StudentAdapter studentAdapter;n private InsertTask insertTask;n private RefreshTask refreshTask;n private DeleteTask deleteTask;nn @Overriden protected void onCreate(Bundle savedInstanceState)n {n super.onCreate(savedInstanceState);n setContentView(R.layout.activity_main);n iniComponent();n iniData();n }nn private void iniComponent()n {n ListView lvList = (ListView) findViewById(R.id.lvList);n studentAdapter = new StudentAdapter(this, new ArrayList<Student>());n lvList.setAdapter(studentAdapter);n lvList.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener()n {n @Overriden public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id)n {n //長按顯示一個確認刪除的對話框n showDeleteConfirmDialog(studentAdapter.getItem(position).id);n return true;n }n });n }nn private void iniData()n {n //注意:可以直接在UI線程中操作資料庫,但是不建議這樣做n DBManager.getInstance(MainActivity.this).getStudentManager().clear();nn //批量插入一些初始數據n List<Student> students = new ArrayList<>();n for (int i = 0; i < 20; i++)n {n Student student = new Student();n student.id = String.valueOf(i);n student.age = String.valueOf(10 + i);n student.name = "Michael" + i;n students.add(student);n }n insert(students);n }nn private void showDeleteConfirmDialog(final String studentId)n {n AlertDialog alertDialog = new AlertDialog.Builder(MainActivity.this).create();n alertDialog.setTitle("刪除該記錄?");n alertDialog.setButton(AlertDialog.BUTTON_POSITIVE, "OK",n new DialogInterface.OnClickListener()n {n public void onClick(DialogInterface dialog, int which)n {n delete(studentId);n dialog.dismiss();n }n });n alertDialog.show();n }nn private class InsertTask extends AsyncTask<Void, Void, Void>n {n private List<Student> students;nn InsertTask(List<Student> students)n {n this.students = students;n }nn @Overriden protected Void doInBackground(Void... params)n {n for (Student student : students)n {n DBManager.getInstance(MainActivity.this).getStudentManager().insert(student);n }n return null;n }nn @Overriden protected void onPostExecute(Void aVoid)n {n super.onPostExecute(aVoid);n refresh();n }n }nn private class RefreshTask extends AsyncTask<Void, Void, List<Student>>n {n @Overriden protected List<Student> doInBackground(Void... params)n {n return DBManager.getInstance(MainActivity.this).getStudentManager().query();n }nn @Overriden protected void onPostExecute(List<Student> students)n {n super.onPostExecute(students);n if (students != null)n {n studentAdapter.setAll(students);n }n }n }nn private class DeleteTask extends AsyncTask<String, Void, Void>n {n @Overriden protected Void doInBackground(String... studentIds)n {n DBManager.getInstance(MainActivity.this).getStudentManager().delete(studentIds[0]);n return null;n }nn @Overriden protected void onPostExecute(Void aVoid)n {n super.onPostExecute(aVoid);n refresh();n }n }nn /**n * 刪除數據n */n private void delete(String studentId)n {n cancelInsertTaskIfRunning();n deleteTask = new DeleteTask();n deleteTask.execute(studentId);n }nn /**n * 插入數據n */n private void insert(List<Student> students)n {n cancelInsertTaskIfRunning();n insertTask = new InsertTask(students);n insertTask.execute();n }nn /**n * 從資料庫中重新獲取一遍數據n */n private void refresh()n {n cancelRefreshTaskIfRunning();n refreshTask = new RefreshTask();n refreshTask.execute();n }nn private void cancelInsertTaskIfRunning()n {n if (insertTask != null)n {n insertTask.cancel(true);n }n }nn private void cancelRefreshTaskIfRunning()n {n if (refreshTask != null)n {n refreshTask.cancel(true);n }n }nn private void cancelDeleteTaskIfRunning()n {n if (deleteTask != null)n {n deleteTask.cancel(true);n }n }nn @Overriden protected void onDestroy()n {n super.onDestroy();n cancelRefreshTaskIfRunning();n cancelInsertTaskIfRunning();n cancelDeleteTaskIfRunning();n }n}n

以上就完成了Android中SQLite的創建,以及基本的增刪改查操作。實際使用中,這樣不會有太大的問題,但是該工程還存在一些不足:

  • 使用起來太麻煩了,每次對資料庫的操作都需要在子線程中完成。
  • 該資料庫只能被應用程序本身訪問,無法實現資料庫內容的跨進程共享。

下一篇,我們將引入ContentResolver和ContentProvider,它們是Android為我們提供的,另外一種對資料庫的訪問方式。它解決的是資料庫內容的跨進程共享問題,並且在某些情況下,通過ContentResolver對資料庫進行訪問,可以不需要在子線程中進行。

完整的示例代碼,託管在我的GitHub上,SqliteDemo,歡迎clone。


推薦閱讀:

Android SO 高階黑盒利用
MIUI 是否會被其他各廠商同質化?
實戰Kotlin@Andorid(二): 界面構建與擴展方法
當前旗艦智能手機的綜合性能已經達到了哪個時期的PC水平?
Android 上有些遊戲為什麼針對 CPU / GPU 做這麼多適配版本?

TAG:Android | SQLite | 数据库 |