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

         应用开发最怕的就是重复造轮子,即使这个轮子是自己造的,很多人批评码农工作就是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,友情分享,无技术支持~
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值