参考《Professional Android 4 Development》
Database and Content Providers
Android Database简介
Android使用SQLite数据库和ContentProvider来实现结构化数据的读写。在Android中,SQLite是以lib的形式存在的,每个应用程序含有自己的SQLite lib,减少了数据库层的耦合,并且提升了安全性。SQLite的数据文件默认存储在这个目录:
/data/data/<package_name>/databases
Content Provider使用URI的方式提供了一个统一的数据访问接口,Schema是content://,与SQLite不同,Content Provider可以跨应用程序使用,可以通过发布自己的Content Provider供他人使用。
Content Values和Cursors
Content Values用于插入数据,Cursor则是返回的查询结果。Content Values对应数据中的一行数据。Cursor类提供了移动游标的方法,常用的有这些:
- moveToFirst
- moveToNext
- moveToPrevious
- getCount
- getColumnIndexOrThrow
- getColumnName
- getColumnNames
- moveToPosition
- getPosition
SQLiteOpenHelper
SQLiteOpenHelper是一个抽象类,使用它可以更方便地进行操作数据库。SQLiteOpenHelper会缓存数据库实例,提升访问效率,也正是因为这种缓存策略,用户只有在不再访问数据库的时候才需要关闭数据库连接。下面是一个SQLiteOpenHelper的实现类例子:
private static class HoardDBOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = “myDatabase.db"; private static final String DATABASE_TABLE = “GoldHoards"; private static final int DATABASE_VERSION = 1; // SQL Statement to create a new database. private static final String DATABASE_CREATE = “create table “ + DATABASE_TABLE + “ (“ + KEY_ID + “ integer primary key autoincrement, “ + KEY_GOLD_HOARD_NAME_COLUMN + “ text not null, “ + KEY_GOLD_HOARDED_COLUMN + “ float, “ + KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + “ integer);"; public HoardDBOpenHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } // Called when no database exists in disk and the helper class needs to create a new one. @Override public void onCreate(SQLiteDatabase db) { db.execSQL(DATABASE_CREATE); } // Called when there is a database version mismatch meaning that the version of the database on disk needs to be upgraded to // the current version. @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // Log the version upgrade. Log.w(“TaskDBAdapter", “Upgrading from version “ + oldVersion + “ to “ + newVersion + “, which will destroy all old data"); // Upgrade the existing database to conform to the new version. Multiple previous versions can be handled by comparing oldVersion and newVersion values. // The simplest case is to drop the old table and create a new one. db.execSQL(“DROP TABLE IF IT EXISTS “ + DATABASE_TABLE); // Create a new one. onCreate(db); } }
调用SQLiteOpenHelper的getWritableDatabase或者getReadableDatabase方法可以获得一个数据库实例,若不存在数据库实例,SQLiteOpenHelper的内部实现是使用onCreate()方法来创建一个数据库实例并返回。
使用其他方式创建或打开SQLite数据库
SQLiteDatabase db = context.openOrCreateDatabase(DATABASE_NAME, Context.MODE_PRIVATE, null);
使用上面的代码可以获得数据库,onCreate和onUpdate方法中的逻辑需要手动来完成。
Android数据库设计需要注意的事情
1. 文件不要直接存在数据库中。更好的方法是将文件路径放到数据库中。
2. 表中加入auto-increment的id字段。
数据库查询
使用query方法,传入下面这些参数,即可实现数据库查询:
- Boolean类型的参数,用于指示是否包含重复数据。
- 要查询的数据库表名。
- Projection,指定要返回的列。
- Where,可以用?做占位符。
- Where参数。
- Group by。
- Having。
- 字符串,指定返回行的顺序。
- 字符串,用于指定返回的最大行数
// Specify the result column projection. Return the minimum set of columns required to satisfy your requirements. String[] result_columns = new String[] {KEY_ID, KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, KEY_GOLD_HOARDED_COLUMN }; // Specify the where clause that will limit our results. String where = KEY_GOLD_HOARD_ACCESSIBLE_COLUMN + “=" + 1; // Replace these with valid SQL statements as necessary. String whereArgs[] = null; String groupBy = null; String having = null; String order = null; SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase(); Cursor cursor = db.query(HoardDBOpenHelper.DATABASE_TABLE, result_columns, where, whereArgs, groupBy, having, order);
从Cursor中提取数据
从Cursor中提取数据需要两步。首先使用前面介绍的moveTo<loction>等方法首先移动到某行,然后使用get<type>方法获取某列的值。使用getColumnIndexOrThrow或getColumnIndex方法可以通过列名获得该列在Cursor中的index。若找不到该列,getColumnIndex方法将返回-1,而getColumnIndexOrThrow将抛出异常。
int columnIndex = cursor.getColumnIndex(KEY_COLUMN_1_NAME); if (columnIndex > -1) { String columnValue = cursor.getString(columnIndex); // Do something with the column value. } else { // Do something else if the column doesn’t exist. }
下面是一个从Cursor中读取数据的完整示例:
float totalHoard = 0f; float averageHoard = 0f; // Find the index to the column(s) being used. int GOLD_HOARDED_COLUMN_INDEX = cursor.getColumnIndexOrThrow(KEY_GOLD_HOARDED_COLUMN); // Iterate over the cursors rows. // The Cursor is initialized at before first, so we can check only if there is a “next" row available. If the // result Cursor is empty this will return false. while (cursor.moveToNext()) { float hoard = cursor.getFloat(GOLD_HOARDED_COLUMN_INDEX); totalHoard += hoard; } // Calculate an average -- checking for divide by zero errors. float cursorCount = cursor.getCount(); averageHoard = cursorCount > 0 ? (totalHoard / cursorCount) : Float.NaN; // Close the Cursor when you’ve finished with it. cursor.close();
增、改、删
插入数据:
// Create a new row of values to insert. ContentValues newValues = new ContentValues(); // Assign values for each row. newValues.put(KEY_GOLD_HOARD_NAME_COLUMN, hoardName); newValues.put(KEY_GOLD_HOARDED_COLUMN, hoardValue); newValues.put(KEY_GOLD_HOARD_ACCESSIBLE_COLUMN, hoardAccessible); // [ ... Repeat for each column / value pair ... ] // Insert the row into your table SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase(); db.insert(HoardDBOpenHelper.DATABASE_TABLE, null, newValues);
更新数据:
// Create the updated row Content Values. ContentValues updatedValues = new ContentValues(); // Assign values for each row. updatedValues.put(KEY_GOLD_HOARDED_COLUMN, newHoardValue); // [ ... Repeat for each column to update ... ] // Specify a where clause the defines which rows should be updated. Specify where arguments as necessary. String where = KEY_ID + “=" + hoardId; String whereArgs[] = null; // Update the row with the specified index with the new values. SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase(); db.update(HoardDBOpenHelper.DATABASE_TABLE, updatedValues, where, whereArgs);
删除数据:
// Specify a where clause that determines which row(s) to delete. // Specify where arguments as necessary. String where = KEY_GOLD_HOARDED_COLUMN + “=" + 0; String whereArgs[] = null; // Delete the rows that match the where clause. SQLiteDatabase db = hoardDBOpenHelper.getWritableDatabase(); db.delete(HoardDBOpenHelper.DATABASE_TABLE, where, whereArgs);
创建Content Providers
Content Provider是一个接口,Content Resolver可以通过这个接口读取数据。通过继承ContentProvider类,即可创建一个Content Provider:
public class MyContentProvider extends ContentProvider
注册Content Provider
将Content Provider加到Manifest后,Content Resolver才能找到并使用。每个Content Provider标签中都有authorities属性,这个属性标识Content Provider。
<provider android:name=".MyContentProvider" android:authorities="com.paad.skeletondatabaseprovider" />
发布Content Provider
每个ContentProvider都应该还有一个静态字符串变量CONTENT_URI,这个变量由该ContentProvider的authorities和data path组成,如:
public static final Uri CONTENT_URI = Uri.parse(“content://com.paad.skeletondatabaseprovider/elements”);
在CONTENT_URI后面加上行号,即可获得某一行数据的URI,如:
content://com.paad.skeletondatabaseprovider/elements/5
在实际使用中,最好将这两种URI都发布出来,并配合UriMatcher使用:
// Create the constants used to differentiate between the different URI requests. private static final int ALLROWS = 1; private static final int SINGLE_ROW = 2; private static final UriMatcher uriMatcher; // Populate the UriMatcher object, where a URI ending in ‘elements’ will correspond to a request for all items, // and ‘elements/[rowID]’ represents a single row. static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(“com.paad.skeletondatabaseprovider”, “elements”, ALLROWS); uriMatcher.addURI(“com.paad.skeletondatabaseprovider”, “elements/#”, SINGLE_ROW); }
UriMatcher经常和SQLiteQueryBuilder配合使用:
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); // If this is a row query, limit the result set to the passed in row. switch (uriMatcher.match(uri)) { case SINGLE_ROW : String rowID = uri.getPathSegments().get(1); queryBuilder.appendWhere(KEY_ID + “=” + rowID); default: break; }
创建Content Provider的数据库
使用SQLiteOpenHelper实现ContentProvider的onCreate方法:
private MySQLiteOpenHelper myOpenHelper; @Override public boolean onCreate() { // Construct the underlying database. // Defer opening the database until you need to perform a query or transaction. myOpenHelper = new MySQLiteOpenHelper(getContext(), MySQLiteOpenHelper.DATABASE_NAME, null, MySQLiteOpenHelper.DATABASE_VERSION); return true; }
实现Content Provider查询
@Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Open the database. SQLiteDatabase db; try { db = myOpenHelper.getWritableDatabase(); } catch (SQLiteException ex) { db = myOpenHelper.getReadableDatabase(); } // Replace these with valid SQL statements if necessary. String groupBy = null; String having = null; // Use an SQLite Query Builder to simplify constructing the database query. SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); // If this is a row query, limit the result set to the passed in row. switch (uriMatcher.match(uri)) { case SINGLE_ROW : String rowID = uri.getPathSegments().get(1); queryBuilder.appendWhere(KEY_ID + “=” + rowID); default:
break; } // Specify the table on which to perform the query. This can be a specific table or a join as required. queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE); // Execute the query. Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, having, sortOrder); // Return the result Cursor. return cursor; }
实现query方法之后,还需要提供返回数据的MIME信息。因此要重写getType方法并返回一个字符串来描述你的数据类型。数据类型有两种,一种是单条数据,另一种是集合数据:
- 单条数据:vnd.android.cursor.item/vnd.<companyname>.<contenttype>
- 所有数据:vnd.android.cursor.dir/vnd.<companyname>.<contenttype>
@Override public String getType(Uri uri) { // Return a string that identifies the MIME type for a Content Provider URI switch (uriMatcher.match(uri)) { case ALLROWS: return “vnd.android.cursor.dir/vnd.paad.elemental”; case SINGLE_ROW: return “vnd.android.cursor.item/vnd.paad.elemental”; default: throw new IllegalArgumentException(“Unsupported URI: “ + uri); } }
Content Provider Transactions
在修改完数据后,需要调用Content Resolver的notifyChange方法,这样Content Observer即可获得数据修改通知。下面是一段比较全面的示例代码:
@Override public int delete(Uri uri, String selection, String[] selectionArgs) { // Open a read / write database to support the transaction. SQLiteDatabase db = myOpenHelper.getWritableDatabase(); // If this is a row URI, limit the deletion to the specified row. switch (uriMatcher.match(uri)) { case SINGLE_ROW : String rowID = uri.getPathSegments().get(1); selection = KEY_ID + “=” + rowID + (!TextUtils.isEmpty(selection) ?“ AND (“ + selection + ‘)’ : “”); default: break; } // To return the number of deleted items you must specify a where clause. To delete all rows and return a value pass in “1”. if (selection == null) selection = “1”; // Perform the deletion. int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE, selection, selectionArgs); // Notify any observers of the change in the data set. getContext().getContentResolver().notifyChange(uri, null); // Return the number of deleted items. return deleteCount; } @Override public Uri insert(Uri uri, ContentValues values) { // Open a read / write database to support the transaction. SQLiteDatabase db = myOpenHelper.getWritableDatabase(); // To add empty rows to your database by passing in an empty // Content Values object you must use the null column hack parameter to specify the name of the column that can be set to null. String nullColumnHack = null; // Insert the values into the table long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE, nullColumnHack, values); // Construct and return the URI of the newly inserted row. if (id > -1) { // Construct and return the URI of the newly inserted row. Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id); // Notify any observers of the change in the data set. getContext().getContentResolver().notifyChange(insertedId, null); return insertedId; }else return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // Open a read / write database to support the transaction. SQLiteDatabase db = myOpenHelper.getWritableDatabase(); // If this is a row URI, limit the deletion to the specified row. switch (uriMatcher.match(uri)) { case SINGLE_ROW : String rowID = uri.getPathSegments().get(1); selection = KEY_ID + “=” + rowID + (!TextUtils.isEmpty(selection) ?“ AND (“ + selection + ‘)’ : “”); default: break; } // Perform the update. int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE, values, selection, selectionArgs); // Notify any observers of the change in the data set. getContext().getContentResolver().notifyChange(uri, null); return updateCount; }
在Content Provider中存储文件
文件存储的最佳设计模式是将文件路径存储在数据库中,使用Content Provider的接口方法读写。文件路径应该存放在数据库表中的_data列中,并重写Content Provider的openFile方法,返回ParcelFileDescriptor:
@Override public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException { // Find the row ID and use it as a filename. String rowID = uri.getPathSegments().get(1); // Create a file object in the application’s external files directory. String picsDir = Environment.DIRECTORY_PICTURES; File file = new File(getContext().getExternalFilesDir(picsDir), rowID); // If the file doesn’t exist, create it now. if (!file.exists()) { try { file.createNewFile(); } catch (IOException e) { Log.d(TAG, “File creation failed: “ + e.getMessage()); } } // Translate the mode parameter to the corresponding Parcel File Descriptor open mode. int fileMode = 0; if (mode.contains(“w”)) fileMode |= ParcelFileDescriptor.MODE_WRITE_ONLY; if (mode.contains(“r”)) fileMode |= ParcelFileDescriptor.MODE_READ_ONLY; if (mode.contains(“+”)) fileMode |= ParcelFileDescriptor.MODE_APPEND; // Return a Parcel File Descriptor that represents the file. return ParcelFileDescriptor.open(file, fileMode); }
Content Provider的Skeleton示例
import android.content.ContentProvider; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.UriMatcher; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteQueryBuilder; import android.net.Uri; import android.text.TextUtils; import android.util.Log; public class MyContentProvider extends ContentProvider { public static final Uri CONTENT_URI = Uri.parse(“content://com.paad.skeletondatabaseprovider/elements”); // Create the constants used to differentiate between the different URI requests. private static final int ALLROWS = 1; private static final int SINGLE_ROW = 2; private static final UriMatcher uriMatcher; // Populate the UriMatcher object, where a URI ending in ‘elements’ will correspond to a request for all // items, and ‘elements/[rowID]’ represents a single row. static { uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); uriMatcher.addURI(“com.paad.skeletondatabaseprovider”, “elements”, ALLROWS); uriMatcher.addURI(“com.paad.skeletondatabaseprovider”, “elements/#”, SINGLE_ROW); } // The index (key) column name for use in where clauses. public static final String KEY_ID = “_id”; // The name and column index of each column in your database. // These should be descriptive. public static final String KEY_COLUMN_1_NAME = “KEY_COLUMN_1_NAME”; // TODO: Create public field for each column in your table. // SQLite Open Helper variable private MySQLiteOpenHelper myOpenHelper; @Override public boolean onCreate() { // Construct the underlying database. // Defer opening the database until you need to perform a query or transaction. myOpenHelper = new MySQLiteOpenHelper(getContext(), MySQLiteOpenHelper.DATABASE_NAME, null, MySQLiteOpenHelper.DATABASE_VERSION); return true; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { // Open the database. SQLiteDatabase db = myOpenHelper.getWritableDatabase(); // Replace these with valid SQL statements if necessary. String groupBy = null; String having = null; SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); queryBuilder.setTables(MySQLiteOpenHelper.DATABASE_TABLE); // If this is a row query, limit the result set to the passed in row. switch (uriMatcher.match(uri)) { case SINGLE_ROW : String rowID = uri.getPathSegments().get(1); queryBuilder.appendWhere(KEY_ID + “=” + rowID); default: break; } Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, groupBy, having, sortOrder); return cursor; } @Override public int delete(Uri uri, String selection, String[] selectionArgs){ // Open a read / write database to support the transaction. SQLiteDatabase db = myOpenHelper.getWritableDatabase(); // If this is a row URI, limit the deletion to the specified row. switch (uriMatcher.match(uri)) { case SINGLE_ROW : String rowID = uri.getPathSegments().get(1); selection = KEY_ID + “=” + rowID + (!TextUtils.isEmpty(selection) ?“ AND (“ + selection + ‘)’ : “”); default: break; } // To return the number of deleted items, you must specify a where clause. To delete all rows and return a value, pass in “1”. if (selection == null) selection = “1”; // Execute the deletion. int deleteCount = db.delete(MySQLiteOpenHelper.DATABASE_TABLE, selection, selectionArgs); // Notify any observers of the change in the data set. getContext().getContentResolver().notifyChange(uri, null); return deleteCount; } @Override public Uri insert(Uri uri, ContentValues values) { // Open a read / write database to support the transaction. SQLiteDatabase db = myOpenHelper.getWritableDatabase(); // To add empty rows to your database by passing in an empty // Content Values object, you must use the null column hack parameter to specify the name of the column that can be set to null. String nullColumnHack = null; // Insert the values into the table long id = db.insert(MySQLiteOpenHelper.DATABASE_TABLE, nullColumnHack, values); if (id > -1) { // Construct and return the URI of the newly inserted row. Uri insertedId = ContentUris.withAppendedId(CONTENT_URI, id); // Notify any observers of the change in the data set. getContext().getContentResolver().notifyChange(insertedId, null); return insertedId; }else return null; } @Override public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { // Open a read / write database to support the transaction. SQLiteDatabase db = myOpenHelper.getWritableDatabase(); // If this is a row URI, limit the deletion to the specified row. switch (uriMatcher.match(uri)) { case SINGLE_ROW : String rowID = uri.getPathSegments().get(1); selection = KEY_ID + “=” + rowID + (!TextUtils.isEmpty(selection) ?“ AND (“ + selection + ‘)’ : “”); default: break; } // Perform the update. int updateCount = db.update(MySQLiteOpenHelper.DATABASE_TABLE, values, selection, selectionArgs); // Notify any observers of the change in the data set. getContext().getContentResolver().notifyChange(uri, null); return updateCount; } @Override public String getType(Uri uri) { // Return a string that identifies the MIME type for a Content Provider URI switch (uriMatcher.match(uri)) { case ALLROWS: return “vnd.android.cursor.dir/vnd.paad.elemental”; case SINGLE_ROW: return “vnd.android.cursor.item/vnd.paad.elemental”; default: throw new IllegalArgumentException(“Unsupported URI: “ + uri); } } private static class MySQLiteOpenHelper extends SQLiteOpenHelper { // [ ... SQLite Open Helper Implementation ... ] } }