您的位置:首页 > 数据库

安卓高效开发:数据库基本

2013-11-28 12:11 351 查看
应用开发最怕的就是重复造轮子,即使这个轮子是自己造的,很多人批评码农工作就是Ctrl+C,可最近发现自己连这个组合键都懒得用了,复制过来的代码在新项目中还需要调整,费时费力,干脆把写过的东西整理下,形成类似框架的东西,以后开发岂不是so easy,先从数据库下手,这个用的最多,经测试基本可靠且易扩展。略喜。
数据库基本操作包括增删改查及更新,本文中使用ContentProvider(CP)对其进行了封装,不得不说CP是个好东西,提供统一接口及URI检查,还可以进行异常处理和自定义过程,虽然归根揭底还需要数据库句柄的操作,但其提供的统一标识还是有利与进程间数据共享的,使用CP后的数据库包结构如下图:



数据库包中含有三个文件
Contract.java 契约文件,类似于C语言里的头文件,客户只需要拿到这个文件,就知道怎么做了,我们需要把那些对外暴露的内容加到该文件中,契约这名字是我根据NotePad里注释直译的,感觉作用相像。
CustomDataBaseHelper.java, 帮助类,进行过数据库开发的朋友对这个帮助类多很熟悉,负责数据库建立、打开、更新等操作。实际的建表过程就是在数据库建立时进行的。
CustomProvider,这就是CP的核心文件了,包括该CP的具体增删改查过程,URI的判断及异常处理都在这里,废话不多说了,上代码,看注释。
1 Contract.java
package com.klpchan.provider;

import android.net.Uri;
import android.provider.BaseColumns;

/**
* 契约文件,用于向客户展现内容提供器(CP)的具体项,类似于C语言中的h文件。
* 好的契约文件能够向客户展示该CP的URIs、数据库名、表名、列名等,具体的数据
* 库操作不在该文件中。
* 一般情况,客户代码只需使用这个文件的内容就能够达到目的。
*/
public final class Contract {
/**
* 每个CP在向系统注册,都有个身份ID,这里用AUTHORITY表示。
* 在AndroidManifest.xml中注册CP时需使用该常量。本例设置为包名。
*/
public static String AUTHORITY = "com.klpchan.androidklpdatabase";
/**
* 协议类型,类似于网络中的"http://"格式,
* Android文件系统定位某个文件位置类似于: content://contacts/people/1
* 网络上定义某个文件位置也采用类似格式 : http://www.baidu.com/mp3/xxx.mp3 * 都是使用协议类型+域名+文件位置的方式。
*/
private static final String SCHEME = "content://";
/**
* 默认排序,以join_time项升序排列,
* 该默认排序表示的客户搜索的游标内容排序方式,实际在
* 数据库表中的数据序列不变,仍是按照创建先后顺序排列。
*/
public static final String DEFAULT_SORT_ORDER = "join_time ASC";
/**
* 自定义数据库的表及单项的MIME类型
* 通过该MIME类型可以判断需要CP操作的是对象是表整体还是单项,
* 同时向系统表明该CP可以提供的数据MIME类型。
* 这个结合Intent-filter使用较为常见。
*/
public static final String RAW_CONTACTS_TYPE = "vnd.android.cursor.dir/vnd.klp.db.raw_contacts";
public static final String RAW_CONTACTS_ITEM_TYPE = "vnd.android.cursor.item/vnd.klp.db.raw_contacts";

/**
* 上述MIME类型的匹配参数,用于判断需要CP处理的数据MIME类型,多结合Intent-filter使用。
*/
//需处理的URI是 1:表URI 2:具体数据项
public static final int RAW_CONTACTS_DB = 1;
public static final int RAW_CONTACTS_DB_ID = 2;
/**
* 数据库中包含很多表,将一个表的所有信息以一个内部类表示出来
* 包括表名、URI、列名及列相对位置。
* 列相对位置代码中通过cursor可以找到,考虑易读性,可在该内部类中定义。
* 每个内部类表示一张表。
*/
public static final class EmployeeTable implements BaseColumns {
//表名
public static final String TABLE_NAME = "employeeInfo";
//表URI
public static final Uri CONTENT_URI = Uri.parse(SCHEME + AUTHORITY + "/" + TABLE_NAME);
//表中列相对位置,从0开始
public static final int POS_COL_ID = 0;
public static final int POS_COL_NAME = 1;
public static final int POS_COL_JOB_TITLE = 2;
public static final int POS_COL_PRE_COMPANY = 3;
public static final int POS_COL_CREATED_DATE = 4;
public static final int POS_COL_WAGE_YEAR = 5;
//列名
public static final String KEY_NAME = "name";
public static final String KEY_JOB_TITLE = "job_title";
public static final String KEY_PRE_COMPANY = "pre_company";
public static final String KEY_CREATED_DATE = "join_time";
public static final String KEY_WAGE_YEAR = "wage_year";
}
}


2 CustomDataBaseHelper.java

package com.klpchan.provider;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

import com.klpchan.provider.Contract.EmployeeTable;

/**
* 数据库操作的帮助类,负责某一具体数据库的创建、打开、升级等操作。
*/
public class CustomDataBaseHelper extends SQLiteOpenHelper {
protected static final String TAG = "DB::CustomDataBaseHelper";
//帮助类负责的数据库名称
public static final String DATABASE_NAME = "HR.db";
//数据库当前版本
protected static final int DATABASE_VERSION = 1;
//单例静态帮助对象
static CustomDataBaseHelper mInstance = null;
//建表命令,在数据库被创建时执行
private static final String CMD_CREATE_TABLE =
"CREATE TABLE " + EmployeeTable.TABLE_NAME + " ("
+ EmployeeTable._ID + " INTEGER PRIMARY KEY AUTOINCREMENT , "
+ EmployeeTable.KEY_NAME + " TEXT , "
+ EmployeeTable.KEY_JOB_TITLE + " TEXT , "
+ EmployeeTable.KEY_PRE_COMPANY + " TEXT , "
+ EmployeeTable.KEY_CREATED_DATE + " TEXT , "
+ EmployeeTable.KEY_WAGE_YEAR + " TEXT "
+ " ) ";
//某个帮助类负责某个特定的数据库
private CustomDataBaseHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}

public static CustomDataBaseHelper getInstance(Context context) {
if (mInstance == null)
mInstance = new CustomDataBaseHelper(context);
return mInstance;
}
/**
* 当帮助类执行getWritableDatabase时
* 如果数据库不存在,则调用该方法创建数据库。
* 如存在,则调用onOpen方法打开数据库。
*/
@Override
public void onCreate(SQLiteDatabase db) {
Log.i(TAG, "create db");
db.execSQL(CMD_CREATE_TABLE);
}

@Override
public void onOpen(SQLiteDatabase db) {
super.onOpen(db);
Log.i(TAG, "open");
db.rawQuery("PRAGMA synchronous = 1", null);
db.rawQuery("PRAGMA journal_mode = WAL", null);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
Log.i(TAG, "ContactsDataBaseHelper onUpgrade - oldVersion : "
+ oldVersion + ", newVersion : " + newVersion);
//不删除表的内容,仅仅添加一列内容
/*		db.execSQL("ALTER TABLE " + EmployeeTable.TABLE_NAME + " ADD COLUMN "
+ EmployeeTable.KEY_ADDED_COLOUM + " INTEGER");*/
//删除旧表重建
db.execSQL("DROP TABLE IF EXISTS " + EmployeeTable.TABLE_NAME);
onCreate(db);
}
}

3 CustomProvider.java
package com.klpchan.provider;

import java.util.ArrayList;

import android.content.ContentProvider;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.OperationApplicationException;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.provider.BaseColumns;
import android.text.TextUtils;
import android.util.Log;

import com.klpchan.provider.Contract.EmployeeTable;

public class CustomProvider extends ContentProvider {
protected static final String TAG = "DB::CustomProvider";

//数据库帮助类及执行具体增删改查的数据库句柄
private static CustomDataBaseHelper mDBOpenHelper;
private SQLiteDatabase mDataBase;

/**
* 一个URI的匹配类,引入了两种URI模式,如果需CP处理的URI如下:
* content://com.klpchan.androidklpdatabase/employeeInfo : 对应表操作
* content://com.klpchan.androidklpdatabase/employeeInfo/# : 对于具体数据项操作
* 其余情况抛出异常,不予操作。
*/
private static final UriMatcher uriMatcher;

static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

uriMatcher.addURI(Contract.AUTHORITY, EmployeeTable.TABLE_NAME, Contract.RAW_CONTACTS_DB);
uriMatcher.addURI(Contract.AUTHORITY, EmployeeTable.TABLE_NAME + "/#", Contract.RAW_CONTACTS_DB_ID);
}

public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
throws OperationApplicationException {
Log.d(TAG, "applyBatch start");
SQLiteDatabase db = mDBOpenHelper.getWritableDatabase();
db.beginTransaction();
try {
ContentProviderResult[] results = super.applyBatch(operations);
db.setTransactionSuccessful();
return results;
} finally {
db.endTransaction();
}
}

public static void closeDataBase() {
if (mDBOpenHelper != null)
mDBOpenHelper.close();
}
/**
* 由于在Android.Manifest中已注册provider,程序在第一次执行时通过installProvider
* 调用该方法,provider安装成功后一直存在直到程序卸载。
*/
@Override
public boolean onCreate() {
Log.i(TAG, "Create provider");
mDBOpenHelper = CustomDataBaseHelper.getInstance(getContext());
if (mDBOpenHelper != null);
mDataBase = mDBOpenHelper.getWritableDatabase();
return true;
}
/**
* 判断需处理URI的MIME类型,多结合Intent-filter使用。
*/
@Override
public String getType(Uri uri) {
int match = uriMatcher.match(uri);

switch (match) {
case Contract.RAW_CONTACTS_DB:
return Contract.RAW_CONTACTS_TYPE;
case Contract.RAW_CONTACTS_DB_ID:
return Contract.RAW_CONTACTS_ITEM_TYPE;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
//CP的插入操作,会检查URI是否为表格式并加入数据建立时间
@Override
public Uri insert(Uri uri, ContentValues values) {
if (values == null)
return null;

if (uriMatcher.match(uri) != Contract.RAW_CONTACTS_DB ) {
throw new IllegalArgumentException("Unknown URI " + uri);
}

String table = (String) uri.getPathSegments().get(0);

if (uriMatcher.match(uri) == Contract.RAW_CONTACTS_DB) {
if (!values.containsKey(EmployeeTable.KEY_CREATED_DATE))
values.put(EmployeeTable.KEY_CREATED_DATE, Long.valueOf(System.currentTimeMillis()));
}
//在向数据库插入数据后,通知数据改变可以让依附于该数据库的观察者如Cursor重新查询,即时更新view
long rowId = mDataBase.insert(table, null, values);
if (rowId > 0) {
Uri genuri = ContentUris.withAppendedId(uri, rowId);
getContext().getContentResolver().notifyChange(genuri, null);
return genuri;
}

return null;
}
//CP的删除操作,只删除特定数据项。
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
String table = (String) uri.getPathSegments().get(0);
int match = uriMatcher.match(uri);

switch (match) {
case Contract.RAW_CONTACTS_DB:
break;
case Contract.RAW_CONTACTS_DB_ID:
selection = BaseColumns._ID
+ "="
+ (String) uri.getPathSegments().get(1)
+ (!TextUtils.isEmpty(selection) ? " AND (" + selection
+ ')' : "");
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}

int count = mDataBase.delete(table, selection, selectionArgs);
getContext().getContentResolver().notifyChange(uri, null);
return count;
}
//CP更新操作。注意在每次数据更新时NotifyChange,以供数据观察者进行处理
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
String table = (String) uri.getPathSegments().get(0);
int match = uriMatcher.match(uri);

switch (match) {
case Contract.RAW_CONTACTS_DB:
break;
case Contract.RAW_CONTACTS_DB_ID:
selection = BaseColumns._ID
+ "="
+ (String) uri.getPathSegments().get(1)
+ (!TextUtils.isEmpty(selection) ? " AND (" + selection
+ ')' : "");
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}

int count = mDataBase.update(table, values, selection, selectionArgs);
if (count > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return count;
}
//CP查询,默认查询方式为建立时间升序。
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String table = (String) uri.getPathSegments().get(0);

qb.setTables(table);

int match = uriMatcher.match(uri);

switch (match) {
case Contract.RAW_CONTACTS_DB:
break;
case Contract.RAW_CONTACTS_DB_ID:
qb.appendWhere(BaseColumns._ID + "=" + uri.getPathSegments().get(1));
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}

String orderBy = null;

if (match == Contract.RAW_CONTACTS_DB) {
if (TextUtils.isEmpty(sortOrder) && table.equals(EmployeeTable.TABLE_NAME)) {
orderBy = Contract.DEFAULT_SORT_ORDER;
} else {
orderBy = sortOrder;
}
} else {
orderBy = sortOrder;
}

Cursor c = qb.query(mDataBase, projection, selection, selectionArgs, null, null, orderBy);
c.setNotificationUri(getContext().getContentResolver(), uri);

return c;
}
}

在CP的增删改过程中,我们能看到notifyChange这个函数被调用,这个对UI设计比较关键,很多时候纠结于怎样让UI和数据库同步,也就是在适配器中Cursor的内容改变时即时调整UI,notifyChange可以让那些对数据库感兴趣的观察者么做一些实用的操作,类似于适配器的notifyDataSetChanged操作。
为了验证有效性,需要一个测试文件,测试文件对该数据库包的基本操作进行测试,测试结果真实有效,没有进行异常及压力测试,有兴趣的朋友可自行验证修改,测试文件代码如下。
TestActivity.java
package com.klpchan.androidklpdatabase;

import java.util.ArrayList;

import android.app.Activity;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.ContentObserver;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.ListView;
import android.widget.SimpleCursorAdapter;

import com.klpchan.androidklpdatabase.R;
import com.klpchan.provider.Contract.EmployeeTable;

public class TestActivity extends Activity {
private ContentResolver resolver;
private ListView listView;
protected static final String TAG = "DB::Test";
private Button Button1;
private Button Button2;
private Button Button3;
private Button Button4;
private Button Button5;
private Button Button6;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
resolver = getContentResolver();
listView = (ListView) findViewById(R.id.listView);

OnClickListener clickListener = new OnClickListener() {
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
init();
break;
case R.id.button2:
query();
break;
case R.id.button3:
insert();
break;
case R.id.button4:
update();
break;
case R.id.button5:
delete();
break;
case R.id.button6:
deleteAll();
break;
default:
break;
}
}
};

Button1 = (Button) this.findViewById(R.id.button1);
Button1.setOnClickListener(clickListener);
Button2 = (Button) this.findViewById(R.id.button2);
Button2.setOnClickListener(clickListener);
Button3 = (Button) this.findViewById(R.id.button3);
Button3.setOnClickListener(clickListener);
Button4 = (Button) this.findViewById(R.id.button4);
Button4.setOnClickListener(clickListener);
Button5 = (Button) this.findViewById(R.id.button5);
Button5.setOnClickListener(clickListener);
Button6 = (Button) this.findViewById(R.id.button6);
Button6.setOnClickListener(clickListener);

/*		getContentResolver().registerContentObserver(EmployeeTable.CONTENT_URI,
true, new MemoObserver(handler));*/
}

public void init() {
ArrayList<Memo> memos = new ArrayList<Memo>();

Memo memo1 = new Memo("Ella", "Engineer", "MS", 22);
Memo memo2 = new Memo("Jenny","Engineer","PG",23);
Memo memo3 = new Memo("Jessica","Tester","GMC",16);

memos.add(memo1);
memos.add(memo2);
memos.add(memo3);

for (Memo memo : memos) {
resolver.insert(EmployeeTable.CONTENT_URI, loadCV(memo));
}
}

@SuppressWarnings("deprecation")
public void query() {
Cursor c = resolver.query(EmployeeTable.CONTENT_URI, null, null, null, null);
printLog(c);
CursorWrapper cursorWrapper = new CursorWrapper(c);
SimpleCursorAdapter adapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_2, cursorWrapper, new String[] {
EmployeeTable.KEY_NAME,
EmployeeTable.KEY_JOB_TITLE
}, new int[] {
android.R.id.text1, android.R.id.text2
}
);
listView.setAdapter(adapter);
startManagingCursor(cursorWrapper);
}

public void insert() {
Memo memo = new Memo("Alina","Engineer","UN",18);
resolver.insert(EmployeeTable.CONTENT_URI, loadCV(memo));
}

public void update() {
Memo memo = new Memo("Alina","Senior Engineer","UN",25);
resolver.update(EmployeeTable.CONTENT_URI, loadCV(memo),
(EmployeeTable.KEY_NAME + " = ?"),
new String[] { memo.name });
}

public void delete() {
resolver.delete(EmployeeTable.CONTENT_URI, "name = 'Alina'", null);
}

public void deleteAll() {
resolver.delete(EmployeeTable.CONTENT_URI, null, null);
}

public class Memo {
public int _id;
public String name;
public String jobtitle;
public String precompany;
public long createdate;
public double wage;

public Memo(String name,String jobtitle,String precompany,double wage) {
this.name = name;
this.jobtitle = jobtitle;
this.precompany = precompany;
this.wage = wage;
}
}

private ContentValues loadCV(Memo memo){
ContentValues cv = new ContentValues();
cv.put(EmployeeTable.KEY_NAME, memo.name);
cv.put(EmployeeTable.KEY_JOB_TITLE, memo.jobtitle);
cv.put(EmployeeTable.KEY_PRE_COMPANY, memo.precompany);
cv.put(EmployeeTable.KEY_WAGE_YEAR, memo.wage);
return cv;
}

public class MemoObserver extends ContentObserver {
private Handler handler;

public MemoObserver(Handler handler) {
super(handler);
this.handler = handler;
}

@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
Message msg = new Message();
handler.sendMessage(msg);
}
}

void printLog(Cursor c){
if (null == c || c.getCount() <= 0) {
Log.w(TAG, "printLog : query result is empty");
}
for (int i = 0; i < c.getCount(); i++) {
c.moveToPosition(i);
Log.i(TAG, "get cursor item [" + i + "]:"
+ c.getString(EmployeeTable.POS_COL_NAME)
+ c.getString(EmployeeTable.POS_COL_JOB_TITLE)
+ c.getString(EmployeeTable.POS_COL_PRE_COMPANY)
+ c.getDouble(EmployeeTable.POS_COL_WAGE_YEAR));
}
}
}

测试界面及数据库内容如下:





不要忘了在AndroidManifest.xml中注册provider:
<provider
android:name="com.klpchan.provider.CustomProvider"
android:exported="false"
android:authorities="com.klpchan.androidklpdatabase" />
</application>
如果想要进行扩展,建立其它列内容的表时,只需修改以下部分
1) Contract.java 仿照EmployeeTable增加关于新表的内部类;添加新的MIME类型及对应识别码。
2) CustomDataBaseHelper.java 仿照CMD_CREATE_TABLE新添建立新表的sql语句,并在onCreate时使用。
3) CustomProvider.java在检测MIME类型的switch-case语句中加入新的类型标识,具体过程不变。
多快好省,一个个新表诞生了~
小结
本文主要是整理了以前用在项目中使用的数据库基本结构,通过过程细分及重构,方便以后扩展及二次开发,由于时间及水平问题,该结构可能还存在问题,以后会逐步优化,有需要的同学在使用时也可自行debug,友情分享,无技术支持~
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: