Android中SQLite数据操作
1. 概述
SQLite 为嵌入式系统上的一个开源数据库管理系统,它支持标准的关系型数据库查询语句SQL 语法,支持事务(Transaction) 操作 。而且SQLite 数据库在Andrioid平台上大约只需要250K的内存空间。在Android平台上无需任何数据库设置和管理,你只需使用SQL语句来访问 Database,SQL自动为你管理数据库。在Android平台上使用数据库可能比较慢,这是因为访问数据库涉及的大量的读写操作(一般在SD卡)。 因此从性能上考虑,建议不在UI线程进行访问数据库的操作,可以使用AsyncTask来进行数据库的读写。SQLite 支持的数据类型有 TEXT (类似Java中的String 类型),INTEGER(类似Java中的long 类型)以及REAL (类似Java 中的double类型),所有其它数据类型最终都必须转化成这三种类型之一才能存放到数据库中。要注意的是SQLite 本身不校验字段的数据类型,也就是说你可以将整数写到字符串字段中。
如果你的应用创建一个SQLite数据库,它的缺省路径为”DATA/data/APP_NAME/databases/FILENAME”.
说明:
DATA 为使用Environment.getDataDirectory()返回的路径,一般为你的SD卡的路径。 APP_Name为你的应用的名称
FILENAME为你的数据库的文件名。
一般来说,应用创建的数据库只能有创建它的应用访问,如果你想共享你的数据,你可以使用Content provider 来实现。
2. android.database.sqlite 类定义
Sqlite 是以库函数的形式提供的,而不是以单独的进程来提供数据库服务(如Desktop平台上SQL Server,这样做的效果是,由应用程序创建的SQLite数据库成为应用的一个部分,从而降低了外部依赖,减小数据访问的延迟,简化了数据的事务处理 时的同步和锁定操作。在Android平台上SQLite 支持定义在android.database.sqlite (其实是Android系统中SQLite C函数的Java接口),其主要的类和接口的类关系图说明如下:
其中ContentValues 定义在包android.content 包中,可以用来做数据库表插入一行,每个ContentValues对象可以表示数据表中的一行,为一组列名和列值的集合。
所有SQL 查询的返回结果为一个Cursor对象,使用Cursor(游标)对象可以访问返回的数据集中每行,前进或后退 如moveToFirst, moveToNext, getCount ,getColumnName 等。
SQLiteOpenHelper 为一抽象类,它简化了数据库的创建,打开,升级操作。
SQLiteDatabase 代表一个数据库对象,提供来inert, update ,delete 以及execSQL 等操作来查询,读写数据库。
SQLiteQuery 代表一个查询,一般不能直接使用,而是由SQLiteCursor使用。
SQLiteStatement 代表一个预编译的数据库查询(类似stored proc).
SQLiteQueryBuild 为一辅助类,用于帮助创建SQL查询。
注:本教程不涉及SQL 语句本身的用法,假定你以有基本的数据库SQL知识,教程侧重如何在Android平台上使用SQLite数据库。
3. 创建数据库
我们将使用一个TodoList 为例介绍SQLite 的基本用法,在设计SQLite数据库表时有几点建议:
对于一些文件资源(如图像或是声音等)不建议直接存放在数据库表中,而今存储这些资源对应的文件名或是对于的content provider 使用的URL 全名。
尽管不是强制的,在设计数据库的关键字时,所有表都包含一个_id 域做为表的关键字,它是一个自动增长的整数域。 如果使用SQLite 作为一个Content Provider,此时唯一的_id域就是必须的。
设计Todolist的数据库结构如下:
前面说过Android SDK中提供了一个SQLiteOpenHelper来帮助创建,打开,和管理数据库,一个比较常见的设计模式是创建一个数据库的Adpater,为应用程序访问数据库提供一个抽象层,该层可以封装与数据库之间的直接交互:
按照上面的原则,我们为TodoList 数据库创建两个类:ToDoDBAdapter和ToDoDBOpenHelper ,其中ToDoDBOpenHelper作为ToDoDBAdapter 内部类定义(也可以作为单独类)。构造创建数据库的SQL 语句,初始的类定义如下:
public class ToDoDBAdapter { private static final String DATABASE_NAME = "todoList.db"; private static final String DATABASE_TABLE = "todoItems"; private static final int DATABASE_VERSION = 1; private SQLiteDatabase mDb; private final Context mContext; public static final String KEY_ID = "_id"; public static final String KEY_TASK = "task"; public static final int TASK_COLUMN = 1; public static final String KEY_CREATION_DATE = "creation_date"; public static final int CREATION_DATE_COLUMN = 2; private ToDoDBOpenHelper dbOpenHelper; public ToDoDBAdapter(Context context) { mContext = context; dbOpenHelper = new ToDoDBOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION); } public void close() { mDb.close(); } public void open() throws SQLiteException { try { mDb = dbOpenHelper.getWritableDatabase(); } catch (SQLiteException ex) { mDb = dbOpenHelper.getReadableDatabase(); } } private static class ToDoDBOpenHelper extends SQLiteOpenHelper { public ToDoDBOpenHelper(Context context, String name, CursorFactory factory, int version) { super(context, name, factory, version); } // SQL statement to create a new database private static final String DATABASE_CREATE = "create table " + DATABASE_TABLE + " (" + KEY_ID + " integer primary key autoincrement , " + KEY_TASK + " text not null, " + KEY_CREATION_DATE + " long);"; @Override public void onCreate(SQLiteDatabase db) { db.execSQL(DATABASE_CREATE); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.w("TaskDBAdapter", "Upgrading from version " + oldVersion + " to " + newVersion); db.execSQL("DROP TABLE IF EXISTS " + DATABASE_TABLE); onCreate(db); } } }
ToDoDBOpenHelper 为 SQLiteOpenHelper 的子类,一般需要重载onCreate(SQLiteDatabase) 和 onUpgrade(SQLiteDabase, int ,int) 如有需要也可以重载onOpen (SQLiteDatabase), 这个类可以在数据库未创建时自动创建数据库,如果已有数据库则可以打开。
它提供了两个方法getReadableDatabase() 和getWriteableDatabase() 来取得SQLiteDatabase数据库对象,此时 SQLiteOpenHelper 会根据需要创建onCreate 或升级onUpdate 数据库,如果数据库已存在,则打开对应的数据库。 尽管是两个方法,通常两个方法返回时是同一个数据库对象,除非在出现问题时(如磁盘空间满)此时getReadableDatabase() 可能返回只读数据库对象。
ToDoDBAdapter 的open 方法首先是试图获取一个可读写的数据库对象,如果不成功,则再试图取得一个只读类型数据库。
4. 读写数据库操作
有了数据库对象之后,可以使用execSQL 提供SQL语言来添加,删除,修改或查询数据库。除了通用的execSQL 之外,SQLiteDatabase 提供了 insert, update, delete ,query 方法来简化数据库的添加,删除,修改或查询操作。
此外SQLite不强制检测数据库的数据类型,通过DBAdapter 可以使用强数据类型来修改,删除数据等,这也是使用DBAdapter的一个好处:
这里我们先定义了一个TodoItem 类,表示一个Todo 项:
public class TodoItem { private String mTask; private Date mCreated; public String getTask(){ return mTask; } public Date getCreated(){ return mCreated; } public TodoItem(String task){ this(task,new Date(System.currentTimeMillis())); } public TodoItem(String task,Date created){ mTask=task; mCreated=created; } @Override public String toString(){ SimpleDateFormat sdf=new SimpleDateFormat("dd/mm/yy"); String dateString= sdf.format(mCreated); return "("+ dateString+ ")" + mTask; } }
下面的方法提供使用TodoItem类型做参数使用添加,删除和修改TodoItem
//insert a new task public long insertTask(TodoItem task){ ContentValues newTaskValues=new ContentValues(); //assign values for each row newTaskValues.put(KEY_TASK, task.getTask()); newTaskValues.put(KEY_CREATION_DATE, task.getCreated().getTime()); //insert row return mDb.insert(DATABASE_TABLE,null,newTaskValues); } public boolean removeTask(long rowIndex){ return mDb.delete(DATABASE_TABLE,KEY_ID+"="+rowIndex, null)>0; } public boolean updateTask(long rowIndex,String task){ ContentValues newValue=new ContentValues(); newValue.put(KEY_TASK, task); return mDb.update(DATABASE_TABLE, newValue, KEY_ID+"="+rowIndex, null)>0; }
其中ContentValues 定义了列名到列值的映射,类似于Hashtable。
SQLiteDatabase 的Query方法的一个定义如下:
public Cursorquery(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy)
table : 数据库名称
columns: 需要返回的列名称,
selection: 查询条件,为WHERE语句(不含WHERE)。
selectionArgs: 如果selection 中带有? ,这里可以给出? 的替代值。
groupBy: Group 语句除去GROUP BY。
having: Having语句除去Having
OrderBy: Order by 语句。
下面代码给出查询某个todoItem 项:
public Cursor getAllToDoItemsCursor(){ return mDb.query(DATABASE_TABLE, new String[]{KEY_ID,KEY_TASK,KEY_CREATION_DATE}, null, null, null, null, null); } public Cursor setCursorToToDoItem(long rowIndex) throws SQLException { Cursor result=mDb.query(DATABASE_TABLE, new String[]{KEY_ID,KEY_TASK}, KEY_ID+"="+rowIndex, null, null, null, null); if(result.getCount()==0 || !result.moveToFirst()){ throw new SQLException ("No to do item found for row:" + rowIndex); } return result; } public TodoItem getToDoItem(long rowIndex) throws SQLException { Cursor cursor=mDb.query(DATABASE_TABLE, new String[]{KEY_ID,KEY_TASK,KEY_CREATION_DATE}, KEY_ID+"="+rowIndex, null, null, null, null); if(cursor.getCount()==0 || !cursor.moveToFirst()){ throw new SQLException ("No to do item found for row:" + rowIndex); } String task=cursor.getString(TASK_COLUMN); long created=cursor.getLong(CREATION_DATE_COLUMN); TodoItem result=new TodoItem(task,new Date(created)); return result; }
Query方法还有几个重载的方法,具体可以参见Android文档
5. 导出数据库到XML 文件
使用SQLiteOpenHelper 创建的数据库为应用程序私有,其路径一般为DATA/data/APP_NAME/databases/FILENAME
DATA 为使用Environment.getDataDirectory()返回的路径,一般为你的SD卡的路径。
APP_Name为你的应用的名称
FILENAME为你的数据库的文件名
其它程序一般无法访问这个文件,因此也给调试带来了不便,当然你可以使用Android SDK 的sqlite3 工具来直接访问这个数据,但个人还是觉的sqlite 使用起来不是十分方便。
你也可以将数据库创建者SD卡上面,此时可以使用SQLiteDatabase 的openOrCreateDatabase 指定要创建的数据库的文件名(指定SD卡上的某个文件名)。
也可以将数据库使用代码复制到SD卡上。 此时可以使用一些桌面系统上的SQLite管理工具,比如Firefox 的SQL Lite manager 插件来访问这个数据库。
一种简洁的方法是将数据库导出到XML文件,下面类DatabaseDump的实现,可以将如何一个数据库所有表和表的内容导出到XML文件中。
import java.io.BufferedOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; class DatabaseDump { public DatabaseDump(SQLiteDatabase db,String destXml) { mDb = db; mDestXmlFilename=destXml; try { // create a file on the sdcard to export the // database contents to File myFile = new File(mDestXmlFilename); myFile.createNewFile(); FileOutputStream fOut = new FileOutputStream(myFile); BufferedOutputStream bos = new BufferedOutputStream(fOut); mExporter = new Exporter(bos); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } } public void exportData() { try { mExporter.startDbExport(mDb.getPath()); // get the tables out of the given sqlite database String sql = "SELECT * FROM sqlite_master"; Cursor cur = mDb.rawQuery(sql, new String[0]); cur.moveToFirst(); String tableName; while (cur.getPosition() < cur.getCount()) { tableName = cur.getString(cur.getColumnIndex("name")); // don't process these two tables since they are used // for metadata if (!tableName.equals("android_metadata") && !tableName.equals("sqlite_sequence")) { exportTable(tableName); } cur.moveToNext(); } mExporter.endDbExport(); mExporter.close(); } catch (IOException e) { e.printStackTrace(); } } private void exportTable(String tableName) throws IOException { mExporter.startTable(tableName); // get everything from the table String sql = "select * from " + tableName; Cursor cur = mDb.rawQuery(sql, new String[0]); int numcols = cur.getColumnCount(); cur.moveToFirst(); // move through the table, creating rows // and adding each column with name and value // to the row while (cur.getPosition() < cur.getCount()) { mExporter.startRow(); String name; String val; for (int idx = 0; idx < numcols; idx++) { name = cur.getColumnName(idx); val = cur.getString(idx); mExporter.addColumn(name, val); } mExporter.endRow(); cur.moveToNext(); } cur.close(); mExporter.endTable(); } private String mDestXmlFilename = "/sdcard/export.xml"; private SQLiteDatabase mDb; private Exporter mExporter; class Exporter { private static final String CLOSING_WITH_TICK = "'>"; private static final String START_DB = "<export-database name='"; private static final String END_DB = "</export-database>"; private static final String START_TABLE = "<table name='"; private static final String END_TABLE = "</table>"; private static final String START_ROW = "<row>"; private static final String END_ROW = "</row>"; private static final String START_COL = "<col name='"; private static final String END_COL = "</col>"; private BufferedOutputStream mbufferos; public Exporter() throws FileNotFoundException { this(new BufferedOutputStream(new FileOutputStream(mDestXmlFilename))); } public Exporter(BufferedOutputStream bos) { mbufferos = bos; } public void close() throws IOException { if (mbufferos != null) { mbufferos.close(); } } public void startDbExport(String dbName) throws IOException { String stg = START_DB + dbName + CLOSING_WITH_TICK; mbufferos.write(stg.getBytes()); } public void endDbExport() throws IOException { mbufferos.write(END_DB.getBytes()); } public void startTable(String tableName) throws IOException { String stg = START_TABLE + tableName + CLOSING_WITH_TICK; mbufferos.write(stg.getBytes()); } public void endTable() throws IOException { mbufferos.write(END_TABLE.getBytes()); } public void startRow() throws IOException { mbufferos.write(START_ROW.getBytes()); } public void endRow() throws IOException { mbufferos.write(END_ROW.getBytes()); } public void addColumn(String name, String val) throws IOException { String stg = START_COL + name + CLOSING_WITH_TICK + val + END_COL; mbufferos.write(stg.getBytes()); } } }