源地址: http://www.vogella.com/tutorials/AndroidSQLite/article.html
1.sqlite 和 Android
1.1 什么是sqlite?
sqlite是开源数据库。sqlite支持标准的关系数据库的特性,例如sql语法,事务操作,prepared statement. 只需要很少的运行内存(大概250k),很适合嵌入到一些应用程序中。
sqlite支持的数据类型有:TEXT
(类似java
中的String
)、INTEGER
(类似java
中的long
)、和REAL
(类似java
中的double
)。所有的类型在存入数据中前必须先转换成这几种类型。数据库自己不会去检查写入的数据是否是对应的数据类型,例如,你可以讲一个integer
类型数据写入到string
条目中,相反也可以。
更多关于sqlite的信息,可以访问sqlite网站:http://www.sqlite.org
1.2 Android中的sqlite
sqlite被嵌入在每一个Android设备中。在Android中使用sqlite数据库,不需要安装也不需要管理员权限。
你只需要定义创建和更新数据库的语句。然后Android系统会自动帮你维护这个数据库。
对sqlite数据库的访问需要访问文件系统。这可能比较慢。因此推荐异步执行数据库操作。
如果你的应用创建了数据库,那么数据库被默认保存在这个目录下:DATA/data/APP_NAME/databases/FILENAME
。
上面的数据库路径会符合这么几个规则:DATA
是Environment.getDataDirectory()
方法返回的路径。APP_NAME
是应用名字。FILENAME
是你在代码中为数据库指定的名字。
2.看这篇教程的必要基础
掌握Android开发的基础知识。
3.sqlite知识结构
3.1 有关的包
android.database
包 包括所有和数据库操作相关的类。android.database.sqlite
包括和sqlite相关的类。
3.2 使用sqliteOpenHelper
来创建和更新数据库
可以通过创建一个继承sqliteOpenHelper
类的子类来创建和升级数据库。在类的构造函数中调用sqliteOpenHelper
的super()
方法,指定数据库的名字和当前版本。
在这个新创建的类中,你需要覆盖这么几个方法来创建和更新数据库:
onCreate()
- 如果数据库还没有创建,会被调用。onUpgrade()
- 如果数据库版本升级会被调用。这个方法允许你更新当前数据库的机构或者删掉当前的数据库使用onCreate()
重新创建数据库。
两个方法都会收到sqliteDatabase
对象作为参数,是数据库的java封装。
sqliteOpenHelper
提供两个方法getReadableDatabase()
、getWriteableDatabase()
来获取sqliteDatabase
对象,用于从数据库中读取信息或者写入数据库。
数据库表中,应该用_id
来作为键值。几个Android功能依赖这一设定。
窍门:每个表建立一个类是个很好的编程习惯。这个类定义静态的
onCreate()
方法和onUpgrade()
方法。在sqliteOpenHelper
对应的方法中调用这些方法。这样即使你有多个表,也可以保持sqliteOpenHelper
实现类的可读性。
3.3 sqliteDatabase
sqliteDatabase
是Android中操作sqlite数据库的基础类,它提供了方法去打开、查询、更新和关闭数据库。
特定的,sqliteDatabase
提供了insert()
、update()
、delete()
等方法。
另外还提供了execsql()
方法,来直接执行一个sql语句。
ContentValues
对象可以用来定义键值对。键代表数据库表中某一条目的标识符,值代表数据库某一行纪录中这个条目对应的内容。ContentValues
可以用来插入或者更新数据库。
查询可以通过rawQuery()
或者query()
来完成。或者通过sqliteQueryBuilder
类来完成。
rawQuery()
直接接受select
查询语句。
query()
提供一个指定SQL查询的接口。
sqliteQueryBuilder
是一个很方便构建SQL查询的类。
3.4 rawQuery()示例
下面是一个rawQuery()
调用例子:
Cursor cursor = getReadableDatabase(). rawQuery("select * from todo where _id = ?",new String[] { id });
3.5 query()示例
下面是一个query()
例子:
return database.query(DATABASE_TABLE,new String[] { KEY_ROWID,KEY_CATEGORY,KEY_SUMMARY,KEY_DESCRIPTION },null,null);
方法query()
有下面这几个参数:表 1. 方法query()
的参数
参数 | 解释 |
---|---|
String dbName | 需要查询的数据库表 |
String[] columnNames | 查询需要返回的列名集合,null 表示所有列 |
String whereClause | Where-clause,也就是过滤出需要返回的条目,null 会选择所有条目 |
String[] selectionArgs | whereClause 中可能包括? 占位符,这些占位符会被selectionArgs数组中的值代替 |
String[] groupBy | 表示如何将结果分组,null 表示不分组 |
String[] having | 用来过滤分组后的结果,null 表示不过滤 |
String[] orderBy | 被用来排序的条目,null 表示不排序 |
如果那个参数不需要,传null
就可以了,例如,group by语句。
“whereClause”语句中不包括“where”字符,例如一个“where”语句可能像这样“_id=19 and summary=?”。
如果你通过?
在where语句中指定占位符,那么需要在selectionArgs参数中传递对应的参数。
3.6 Cursor
一个查询操作会返回一个Cursor
对象。一个Cursor代表查询结果,基本上指向查询结果的一行。这样Android可以有效地缓存查询结果,因为不需要一次性把所有的数据装载到内存。
可以通过getCount()
来返回查询结果的数目。
可以通过方法moveToFirst()
、moveToNext()
从行与行之间切换。方法isAfterLast()
可以用来检查,是不是所有的查询结果已经被访问了。
Cursor
提供get*()
方法,例如getLong(columnIndex)
,getString(columnIndex)
来访问当前行中的特定列的内容。“columnIndex”表示你要访问的列的下标。
Cursor
用方法getColumnIndexOrThrow(String)
根据列名字返回列的下标。
一个Cursor
需要用close()
方法来关闭。
3.7 ListView,ListActivities 和 SimpleCursorAdapter
ListViews
是可以显示一系列元素的Views
。
ListAcitivties
用来更方便的使用ListViews
。
为了将数据库和ListViews
连接在一起,你可以使用SimpleCursorAdapter
。SimpleCursorAdapter
允许为ListViews
的每一个元素创建layout。
你需要定义一个包含数据库列名的数组,和另外一个包含Views
中需要填充数据的元素的ID。
SimpleCursorAdapter
会把Cursor
代表的数据和ListView
中的每个条目中需要填充数据的元素映射起来。
为了获得Cursor
对象,你需要使用Loader
类。
4.教程:使用sqlite
4.1 下面这些内容演示如何使用sqlite 数据库。我们将使用一个DAO对象来管理数据。这个DAO负责和数据库的连接,还有查询和修改数据。它也负责将数据库数据转换为java数据类型,所以用户界面的代码不需要处理和数据库的连接层。
app最终会想下面这样:
使用DAO并不总是正确的选择。DAO创建java的model
对象;直接使用数据库或者通过ContentProvider
可能在有效利用资源方面是更好的选择,因为你可以不用创建model
对象。
我仍然会展示如何使用DAO,作为一个相对简单的使用数据库的例子。使用的是Android4.0的系统。API等级是15.另外我更愿意介绍Loader
类,用来展示在Android3.0用来维护数据库Cursor
。而且这个类有额外的复杂性。
4.2 创建项目
以de.vogella.android.sqlite.first
来创建Android project,并同时创建一个名叫TestDatabaseActivity
的activity。
4.3 数据库和数据模型
创建一个MysqLiteHelper
类。这个类负责创建数据库。onUpgrade()
负责删掉现在的数据库并重新创建一个表。它也定义了几个常量表示表名和表里面的列名。
package de.vogella.android.sqlite.first; import android.content.Context; import android.database.sqlite.sqliteDatabase; import android.database.sqlite.sqliteOpenHelper; import android.util.Log; public class MysqLiteHelper extends sqliteOpenHelper { public static final String TABLE_COMMENTS = "comments"; public static final String COLUMN_ID = "_id"; public static final String COLUMN_COMMENT = "comment"; private static final String DATABASE_NAME = "commments.db"; private static final int DATABASE_VERSION = 1; // Database creation sql statement private static final String DATABASE_CREATE = "create table " + TABLE_COMMENTS + "(" + COLUMN_ID + " integer primary key autoincrement," + COLUMN_COMMENT + " text not null);"; public MysqLiteHelper(Context context) { super(context,DATABASE_NAME,DATABASE_VERSION); } @Override public void onCreate(sqliteDatabase database) { database.execsql(DATABASE_CREATE); } @Override public void onUpgrade(sqliteDatabase db,int oldVersion,int newVersion) { Log.w(MysqLiteHelper.class.getName(),"Upgrading database from version " + oldVersion + " to " + newVersion + ",which will destroy all old data"); db.execsql("DROP TABLE IF EXISTS " + TABLE_COMMENTS); onCreate(db); } }
创建Comment
类。这个类是我们的数据模型,包含着我们要保存到数据库中的用户界面数据。
package de.vogella.android.sqlite.first; public class Comment { private long id; private String comment; public long getId() { return id; } public void setId(long id) { this.id = id; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } // Will be used by the ArrayAdapter in the ListView @Override public String toString() { return comment; } }
创建CommentsDataSource
类。这个类是我们的DAO。它维护着和数据库的连接,并且支持向数据库中添加数据和获取数据。
package de.vogella.android.sqlite.first; import java.util.ArrayList; import java.util.List; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; import android.database.sqlException; import android.database.sqlite.sqliteDatabase; public class CommentsDataSource { // Database fields private sqliteDatabase database; private MysqLiteHelper dbHelper; private String[] allColumns = { MysqLiteHelper.COLUMN_ID,MysqLiteHelper.COLUMN_COMMENT }; public CommentsDataSource(Context context) { dbHelper = new MysqLiteHelper(context); } public void open() throws sqlException { database = dbHelper.getWritableDatabase(); } public void close() { dbHelper.close(); } public Comment createComment(String comment) { ContentValues values = new ContentValues(); values.put(MysqLiteHelper.COLUMN_COMMENT,comment); long insertId = database.insert(MysqLiteHelper.TABLE_COMMENTS,values); Cursor cursor = database.query(MysqLiteHelper.TABLE_COMMENTS,allColumns,MysqLiteHelper.COLUMN_ID + " = " + insertId,null); cursor.moveToFirst(); Comment newComment = cursorToComment(cursor); cursor.close(); return newComment; } public void deleteComment(Comment comment) { long id = comment.getId(); System.out.println("Comment deleted with id: " + id); database.delete(MysqLiteHelper.TABLE_COMMENTS,MysqLiteHelper.COLUMN_ID + " = " + id,null); } public List<Comment> getAllComments() { List<Comment> comments = new ArrayList<Comment>(); Cursor cursor = database.query(MysqLiteHelper.TABLE_COMMENTS,null); cursor.moveToFirst(); while (!cursor.isAfterLast()) { Comment comment = cursorToComment(cursor); comments.add(comment); cursor.moveToNext(); } // make sure to close the cursor cursor.close(); return comments; } private Comment cursorToComment(Cursor cursor) { Comment comment = new Comment(); comment.setId(cursor.getLong(0)); comment.setComment(cursor.getString(1)); return comment; } }
4.4 用户界面
把res/layout文件夹下面的main.xml文件改成下面这样。这个布局中包含两个按钮分别用于添加和删除评论,和一个用来显示当前所有评论的ListView
。评论内容一会在activity中随机生成。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <LinearLayout android:id="@+id/group" android:layout_width="wrap_content" android:layout_height="wrap_content" > <Button android:id="@+id/add" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Add New" android:onClick="onClick"/> <Button android:id="@+id/delete" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Delete First" android:onClick="onClick"/> </LinearLayout> <ListView android:id="@android:id/list" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/hello" /> </LinearLayout>
把TestDatabaseActivity
类改成下面这样:
package de.vogella.android.sqlite.first; import java.util.List; import java.util.Random; import android.app.ListActivity; import android.os.Bundle; import android.view.View; import android.widget.ArrayAdapter; public class TestDatabaseActivity extends ListActivity { private CommentsDataSource datasource; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); datasource = new CommentsDataSource(this); datasource.open(); List<Comment> values = datasource.getAllComments(); // use the SimpleCursorAdapter to show the // elements in a ListView ArrayAdapter<Comment> adapter = new ArrayAdapter<Comment>(this,android.R.layout.simple_list_item_1,values); setListAdapter(adapter); } // Will be called via the onClick attribute // of the buttons in main.xml public void onClick(View view) { @SuppressWarnings("unchecked") ArrayAdapter<Comment> adapter = (ArrayAdapter<Comment>) getListAdapter(); Comment comment = null; switch (view.getId()) { case R.id.add: String[] comments = new String[] { "Cool","Very nice","Hate it" }; int nextInt = new Random().nextInt(3); // save the new comment to the database comment = datasource.createComment(comments[nextInt]); adapter.add(comment); break; case R.id.delete: if (getListAdapter().getCount() > 0) { comment = (Comment) getListAdapter().getItem(0); datasource.deleteComment(comment); adapter.remove(comment); } break; } adapter.notifyDataSetChanged(); } @Override protected void onResume() { datasource.open(); super.onResume(); } @Override protected void onPause() { datasource.close(); super.onPause(); } }
4.5 运行这个应用
安装这个app,并使用Add和Delete按钮。重启你的应用,验证数据是一直存在的。
5.Content Provider 和数据共享
5.1 什么是content provider
如果你想和别的应用共享数据,你可以使用content provider(简称provider)。Provider提供基于URI封装的数据。任何以content://
开头指向资源的URI都以通过provider来访问。通过content provider一个资源URI允许你执行数据基本的CRUD操作(Create,Read,Update,Delete)。
provider允许应用访问数据。这个数据可以存储在数据库中,文件系统中,或者远程服务器上的一个文件。
一般content provider被用在应用中是为了和别的应用分享数据。应用数据通常是默认应用私有的,一个content provider是一个方便的和别的应用分享数据的接口。
一个content provider必须在应用的manifest文件中声明。
5.2 content provider的基本URI
访问content provider的基本URI被定义为content://
和provider命名空间的组合。这个命名空间通过android:authorities
属性定义在manifest文件中。例如content://test/
。
基本URI代表一个数据集合。如果基本URI后跟一个实例标识,例如content://test/2
,则表示单一实例。
5.3 访问content provider
因为访问一个provider需要知道它的URI,所以把provider的URI作为公共常量提供给别的开发者是一个很好的开发习惯。
许多Android数据, 例如联系人, 都是通过content provider访问的。
5.4 自定义content provider
创建自定义content provider,你必须创建继承android.content.ContentProvider
的类。然后将这个类在应用manifest文件中声明。相应的必须声明android:authorities
属性,用来标识这个content provider。这个属性是访问数据的基本URI,所以必须是独一无二的。
<provider android:authorities="de.vogella.android.todos.contentprovider" android:name=".contentprovider.MyTodoContentProvider" > </provider>
你的content provider必须实现几个方法, 例如query()
,insert()
, delete()
,getType()
,onCreate()
。对于不支持的方法最好抛一个异常UnsupportedOperationException()
。
query()方法必须返回Cursor对象。
5.5 content provider和安全
在Android 4.2之前,content provider都是默认对别的应用可用的。Android 4.2以后必须明确地声明为导出的。
在manifest文件中的content provider的声明中,可以使用android:exported=false|true
参数来制定content provider的透明性。
技巧:明确的设置
android:exported
参数来确保在不同的版本间的一致性。
5.6 线程安全
如果直接访问数据库,并且在不同的线程都有写操作,那么你将会陷入并发问题。
一个content provider可以在同一时间被多个程序访问,所以必须实现线程安全性。最简单的方法是在content provider每个方法前加上synchoronized
关键字, 这样同一时间只有一个线程可以访问。
如果你不需要Android同步对provider的访问,在manifest文件中provider的生命中设置参数android:multiprocess=true
。这样就允许在每个客户端进程中创建一个content provider实例,省去了IPC(interprocess communication)。
6.教程:使用ContentProvider
6.1 介绍
下面的内容将会使用Contact(联系人)应用中的ContentProvider
。
6.2 在你的手机上创建联系人
对于这个例子来说,我们需要几个已经存在的联系人内容。选择菜单按钮然后是People(联系人)按钮来创建联系人。
应用会问你是否想登陆,选择登陆或者暂不登陆。选择“创建新联系人”。你可以创建本地联系人。
创建完一个新的联系人,然后可以点击 + 按钮添加更多的联系人。这样app中应该有一些可以使用的联系人数据。
6.3 使用Contact中的ContentProvider
创建一个新的Android项目,名字是de.vogella.android.contentprovider
和一个名字是ContactsActivity
的activity。
更改res/layout中对应的layout文件。把当中的TextView
id改为contactview。删除默认的text内容。
布局文件最后回想下面这样:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:id="@+id/contactview" android:layout_width="match_parent" android:layout_height="match_parent" /> </LinearLayout>
访问联系人中的ContentProvider需要一定的权限,因为不是所有应用都应该有访问联系人信息的权限。打开应用的AndroidManifest.xml文件,选择Permission标签。点击添加按钮,选择使用权限。从下拉菜单中选择android.permission.READ_CONTACTS。
将activity改成下面这样:
package de.vogella.android.contentprovider; import android.app.Activity; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; import android.widget.TextView; public class ContactsActivity extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_contacts); TextView contactView = (TextView) findViewById(R.id.contactview); Cursor cursor = getContacts(); while (cursor.moveToNext()) { String displayName = cursor.getString(cursor .getColumnIndex(ContactsContract.Data.DISPLAY_NAME)); contactView.append("Name: "); contactView.append(displayName); contactView.append("\n"); } } private Cursor getContacts() { // Run query Uri uri = ContactsContract.Contacts.CONTENT_URI; String[] projection = new String[] { ContactsContract.Contacts._ID,ContactsContract.Contacts.DISPLAY_NAME }; String selection = ContactsContract.Contacts.IN_VISIBLE_GROUP + " = '" + ("1") + "'"; String[] selectionArgs = null; String sortOrder = ContactsContract.Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC"; return managedQuery(uri,projection,selection,selectionArgs,sortOrder); } }
如果你运行这个应用,那么它将会从联系人应用的ContentProvider
读取数据,并且显示在TextView
中。通常这样的数据应该显示在ListView
中。
7.Loader
7.1 Loader类的作用
Loader
类允许你在activity或者fragment下异步的加载数据。它们可以在数据源发生变化时,将新的数据更新过来。也可以维护配置变化前后数据的一致性。
如果数据是在和activity或者fragment断开后获取的,则缓存这些数据。
Loader类在Android3.0被引入进来,在android1.6有兼容包支持。
7.2 实现一个Loader
你可以将AsyncTaskLoader
作为基类来实现你自己的Loader类。
Activity或者Fragment的LoaderManager
可以管理一或多个Loader
实例Loader的创建是通过下面的调用来实现的:
#start a new loader or re-connect to existing one getLoaderManager().initLoader(0,this)
第一个参数是用来识别的ID,回调类用来标识Loader类。第二个参数用来给回调类额外信息。
initLoader()
的第三个参数表示初始化一旦开始就回调用的类(回调类)。这个类必须实现LoaderManager.LoaderCallbacks
接口。推荐用activity或者fragment使用Loader并且实现LoaderManager.LoaderCallbacks
接口。
Loader
不是通过LoaderManager.initLoader()
直接创建的,但是必须在 onCreateLoader()
中由回调类来创建。
当Loader
异步获取完数据,回调类的onLoadFinished()
方法会被调用。在这里,你可以更新你的用户接口。
Android提供了一个默认Loader
实现CursorLoader
来管理sqlite数据库连接。
对于基于sqlite数据库的ContentProvider,一般需要使用CursorLoader
类。这个类在后台线程执行数据库查询,这样就不会阻塞应用程序。
CursorLoader
用来替换已经废弃的使用acivity自己维护cursors的方法。
如果Cursor
失效,回调类的onLoaderReset()
会被调用。
8.Cursor 和 Loaders
访问数据库的问题之一就是访问速度太慢。另一个问题就是应用得正确考虑组件的生命周期,例如当配置改变的时候应该关闭和打开数据库。
为了维护组件的生命周期,在Android3.0之前可以使用managedQuery()
。
但是这个方法在Android3.0之后被弃用了,你应该使用Loader
框架来访问ContentProvider
。
SimpleCursorAdapter
可以和ListViews
一起使用,它由swapCursor()
方法。你的Loader可以使用这个方法在onLoadFinished()
方法里面更新Cursor
。CursorLoader
在配置变化之后会重新连接Cursor
。