说明
我们知道savedInstanceState、文件与SharedPreference都能够保存数据,但他们都无法满足应用持久化保存数据的需求,Android为此提供了长期存储地:即sqlite数据库。
概述
sqlite是一个轻量级的关系型数据库,运算速度快,占用资源少,很适合在移动设备上使用, 不仅支持标准sql语法,还遵循ACID(数据库事务)原则,无需账号,使用起来非常方便!
sqlite是类似于MysqL和Postgresql的开源关系型数据库。不同于其他数据库的是, sqlite使用单个文件存储数据,使用sqlite库读取数据。
小结下特点:
sqlite通过文件来保存数据库,一个文件就是一个数据库,数据库中又包含多个表格,表格里又有 多条记录,每个记录由多个字段构成,每个字段有对应的值,每个值我们可以指定类型,也可以不指定 类型(主键除外)
关于sqlite数据库支持存储的数据类型及相关的基本操作语句可以移步到android中的数据库操作或者SQLite在线文档。
Android标准库包含sqlite库以及配套的一些Java辅助类。
使用sqlite本地数据库
Step 1 : 定义Schema
我们以上一篇RecyclerView的基本用法为例,将每一个View对象中的内容存入数据库。
创建数据库前,首先要清楚存储什么样的数据。 我们要保存的是一条条Info信息
记录,这需要定义如图所示的infos数据表。
sql中一个重要的概念是schema:一种DB结构的正式声明,用于表示database的组成结构。schema是从创建DB的sql语句中生成的。我们会发现创建一个伴随类(companion class)是很有益的,这个类称为合约类(contract class),它用一种系统化并且自动生成文档的方式,显示指定了schema样式。
Contract Clsss是一些常量的容器。它定义了例如URIs,表名,列名等。这个contract类允许在同一个包下与其他类使用同样的常量。 它让我们只需要在一个地方修改列名,然后这个列名就可以自动传递给整个code。
组织contract类的一个好方法是在类的根层级定义一些全局变量,然后为每一个table来创建内部类。
首先,我们来创建定义schema的Java类。创建时,新建一个包为databas,在包下新建类命名为InfoDbSchema,这样,就可以将InfoDbSchema.java文件放入专门的database包中,实现数据库操作相关代码的组织和归类。
在InfoDbSchema类中,再定义一个描述数据表的InfoTable内部类:
public class InfoDbScheme {
public static final class InfoTable{
public static final String NAME = "infos";
}
}
InfoTable内部类唯一的用途就是定义描述数据表元素的String常量。首先要定义的是数据库表名(InfoTable.NAME)。
接下来定义数据表字段:
public class InfoDbScheme {
public static final class InfoTable{
public static final String NAME = "infos";
public static final class Col{
public static final String UUID = "uuid";
public static final String TITLE = "title";
public static final String DATE = "date";
}
}
}
有了这些数据表元素,就可以在Java代码中安全地引用了。例如, InfoTable.Cols.TITLE就是指Info记录的title字段。此外,这种定义方式还给修改字段名称或新增表元素带来了方便。
step 2 : 使用sql Helper创建初始数据库
定 义 完 数 据 库 schema , 就 可 以 创 建 数 据 库 了 。 openOrCreateDatabase(…) 和databaseList()方法是Android提供的Context底层方法,可以用来打开数据库文件并将其转换为sqliteDatabase实例。
不过,实际开发时,建议总是遵循以下步骤。
确认目标数据库是否存在。
如果存在,打开并确认InfoDbSchema是否是最新版本。
-
令人高兴的是, Android提供的sqliteOpenHelper类可以帮我们处理这些。在数据库包中创建InfoBaseHelper类(InfoBaseHelper.java):
public class InfoBaseHelper extends sqliteOpenHelper {
private static final int VERSION = 1;
private static final String DATABASE_NAME = "infoBase.db";
public InfoBaseHelper(Context context) {
super(context,DATABASE_NAME,null,VERSION);
}
@Override
public void onCreate(sqliteDatabase db) {
}
@Override
public void onUpgrade(sqliteDatabase db,int oldVersion,int newVersion) {
}
}
有了sqliteOpenHelper类,打开sqliteDatabase的繁杂工作都可以交给它处理。在InfoLab中用它创建infos数据库(InfoLab.java):
public class InfoLab {
private static InfoLab sInfoLab;
private Context mAppContext;
private ArrayList<Info> mInfos;
private sqliteDatabase mDateBase;
private InfoLab(Context appContext){
mAppContext = appContext.getApplicationContext();
mDateBase = new InfoBaseHelper(mAppContext).getWritableDatabase();
mInfos = new ArrayList<Info>();
/* for(int i = 0;i<100;i++){ Info info = new Info(); info.setmTtitle("Info #"+i); mInfos.add(info); }*/
}
...
}
这里调用getWritableDatabase()方法时, CrimeBaseHelper要做如下工作。
打开/data/data/com.example.sqlitetest2/databases/crimeBase.db数据库;如果不存在,就先创建crimeBase.db数据库文件。
如果已创建过数据库,首先检查它的版本号。如果InfoOpenHelper中的版本号更高,就调用onUpgrade(sqliteDatabase,int,int)方法升级。
最后,再做个总结: onCreate(sqliteDatabase)方法负责创建初始数据库; onUpgrade(sqliteDatabase,int)方法负责与升级相关的工作。
我 们 在onCreate(…)方法中创建数据库表,这需要导入InfoDbSchema类的InfoTable内部类。(InfoBaseHelper.java)
public void onCreate(sqliteDatabase db) {
db.execsql("create table " + InfoTable.NAME + "(" +
" _id integer primary key autoincrement," +
InfoTable.Col.UUID + "," +
InfoTable.Col.TITLE + "," +
InfoTable.Col.DATE +
")"
);
}
现在我们就在手机本地文件中创建了一个本地数据库,数据库名字叫做infoBase.db,在数据库中还创建了一个数据库表,表的名字叫做infos,在表中我们还创建了几个字段,uuid、title还有date。
我们可以在手机目录/data/data/[your package name]下查看(前提是手机要root),你就可以看到下图这样的文件。
当然现在info表里我们还没有添加数据。
step 3 : 写入数据库
要使用sqliteDatabase,数据库中首先要有数据。数据库写入操作有:向infos表中插入新记录以及在Info变更时更新原始记录。
我们修改InfoLab类,不用List来存储数据,改用mDateBase来存储数据,首先要删除掉InfoLab类中的ArrayList<Info>
代码,增加一个添加数据的方法及更新数据的方法,改动完成如下:(InfoLab.java)
public class InfoLab {
private static InfoLab sInfoLab;
private Context mAppContext;
// private ArrayList<Info> mInfos;
private sqliteDatabase mDateBase;
private InfoLab(Context appContext){
mAppContext = appContext.getApplicationContext();
mDateBase = new InfoBaseHelper(mAppContext).getWritableDatabase();
// mInfos = new ArrayList<Info>();
/* for(int i = 0;i<100;i++){ Info info = new Info(); info.setmTtitle("Info #"+i); mInfos.add(info); }*/
}
public static InfoLab get(Context c){
if(sInfoLab==null){
sInfoLab = new InfoLab(c.getApplicationContext());
}
return sInfoLab;
}
public ArrayList<Info> getInfos(){
// return mInfos;
return new ArrayList<>();
}
public Info getInfo(UUID uuid){
// for(Info i:mInfos){
// if(i.getmId().equals(uuid)){
// return i;
// }
// }
return null;
}
public void addInfo(Info info){
// mInfos.add(info);
}
public void updateInfo(Info info){
}
}
在InfoListFragment.java类中增加添加数据的按钮,修改info_list_activity.xml的代码如下:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".InfoListActivity">
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/info_recycler_view" android:layout_width="match_parent" android:layout_height="wrap_content"/>
<LinearLayout android:id="@+id/empty_crime_list" android:layout_width="wrap_content" android:layout_height="123dp" android:orientation="vertical" android:layout_gravity="center">
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="16dp" android:text="没有Info记录可以显示"/>
<Button android:id="@+id/add_crime_button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:padding="16dp" android:text="@string/new_crime"/>
</LinearLayout>
</LinearLayout>
public class InfoListActivity extends AppCompatActivity {
...
private LinearLayout mLinearLayout;
private Button addButton;
...
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.info_list_activity);
mLinearLayout = (LinearLayout)this.findViewById(R.id.empty_crime_list);
addButton = (Button)this.findViewById(R.id.add_crime_button);
mInfoRecyclerView = (RecyclerView)this.findViewById(R.id.info_recycler_view);
mInfoRecyclerView.setLayoutManager(new LinearLayoutManager(this));
updateUI();
}
...
private void updateUI() {
InfoLab infoLab = InfoLab.get(this);
List<Info> infos = infoLab.getInfos();
if(mAdapter==null){
mAdapter = new InfoAdapter(infos);
mInfoRecyclerView.setAdapter(mAdapter);
}
else{
mAdapter.notifyDataSetChanged();
}
if(infos.size()>0){
mLinearLayout.setVisibility(View.GONE);
}
else{
mLinearLayout.setVisibility(View.VISIBLE);
addButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Log.d("hehe","hehe");
Info info = new Info();
InfoLab.get(InfoListActivity.this).addInfo(info);
Intent intent = new Intent(InfoListActivity.this,InfoDetailActivity.class);
intent.putExtra(EXTRA_INFO_ID,info.getmId());
startActivity(intent);
}
});
}
mInfoRecyclerView.addItemDecoration(new DividerItemDecoration(this,DividerItemDecoration.VERTICAL_LIST));
}
}
现在我们点击按钮,就会加载InfoDetailActivity.java页面。
接下来开始往数据库中写入数据:
使用 ContentValues
负责处理数据库写入和更新操作的辅助类是ContentValues。它是个键值存储类,类似于Java的HashMap和前面用过的Bundle。不同的是, ContentValues只能用于处理sqlite数据。
step 4 : 创建ContentValues( InfoLab.java )
public class InfoLab {
...
public static ContentValues getContentValues(Info info){
ContentValues values = new ContentValues();
values.put(InfoTable.Col.UUID,info.getmId().toString());
values.put(InfoTable.Col.TITLE,info.getmTtitle());
values.put(InfoTable.Col.DATE,info.getmDate().toString());
return values;
}
}
step 5 : 插入和更新记录( InfoLab.java )
public void addInfo(Info info){
// mInfos.add(info);
ContentValues values = getContentValues(info);
mDateBase.insert(InfoTable.NAME,values);
}
insert(String,String,ContentValues)方法有两个重要的参数,还有一个很少用到。
传入的第一个参数是数据库表名,最后一个是要写入的数据。
第二个参数称为nullColumnHack。它有什么用途呢?
别急,举个例子你就明白了。假设你想调用insert(…)方法,但传入了ContentValues
空值。这时, sqlite不干了, insert(…)方法调用只能以失败告终。
然而,如果能以uuid值作为nullColumnHack传入的话, sqlite就可以忽略ContentValues空值,而且还会自动传入一个带uuid且值为null的ContentValues。结果, insert(…)方法得以成功调用并插入了一条新记录
public void updateInfo(Info info){
String uuidString = info.getmId().toString();
ContentValues values = getContentValues(info);
mDateBase.update(InfoTable.NAME,values,InfoTable.Col.UUID + " = ?",new String[] { uuidString });
}
update(String,ContentValues,String[])更新方法类似于insert(…)方法,向其传入要更新的数据表名和为表记录准备的ContentValues。然而,与insert(…)方法不同的是,你要确定该更新哪些记录。具体的做法是:创建where子句(第三个参数) ,然后指定where子句中的参数值(String[]数组参数)。
问题来了,为什么不直接在where子句中放入uuidString呢?这可比使用?然后传入String[]简单多了!
事实上,很多时候, String本身会包含sql代码。如果将它直接放入query语句中,这些代码
可能会改变query语句的含义,甚至会修改数据库资料。这实际就是sql脚本注入, 危害相当严重。
使用?的话,就不用关心String包含什么,代码执行的效果肯定就是我们想要的。
step 6 : Info数据刷新( InfoDetailActivity.java )
public class InfoDetailActivity extends AppCompatActivity {
...
public void onCreate(Bundle savedInstanceState) {
...
}
@Override
protected void onResume() {
super.onResume();
InfoLab.get(this).updateInfo(mInfo);
}
}
这样,点击按钮,你就可以往里面插入数据了,因为还没有完成会导致闪退,但是数据库中已经成功的添加了一条数据,打开数据库目录可以看到:
step 7 : 读取数据库
读取sqlite数据库中数据需要用到query(…)方法。这个方法有好几个重载版本。我们要用的版本如下:
public Cursor query(
String table,String[] columns,String where,String[] whereArgs,String groupBy,String having,String orderBy,String limit)
参数table是要查询的数据表。参数columns指定要依次获取哪些字段的值。参数where和whereArgs的作用与update(…)方法中的一样。
新增一个便利方法调用query(…)方法查询InfoeTable中的记录( InfoLab.java )
private Cursor queryCrimes(String whereClause,String[] whereArgs) {
Cursor cursor = mDatabase.query(
InfoTable.NAME,// Columns - null selects all columns
whereClause,whereArgs,// groupBy
null,// having
null // orderBy
);
return cursor;
}
step 8 : 使用 CursorWrapper
Cursor是个神奇的表数据处理工具,其任务就是封装数据表中的原始字段值。
创建InfoCursorWrapper类(InfoCursorWrapper.java)
public class InfoCursorWrapper extends CursorWrapper {
/** * Creates a cursor wrapper. * * @param cursor The underlying cursor to wrap. */
public InfoCursorWrapper(Cursor cursor) {
super(cursor);
}
...
}
新增getCrime()方法(InfoCursorWrapper.java)
public class InfoCursorWrapper extends CursorWrapper {
/** * Creates a cursor wrapper. * * @param cursor The underlying cursor to wrap. */
public InfoCursorWrapper(Cursor cursor) {
super(cursor);
}
public Info getInfo() {
String uuidString = getString(getColumnIndex(InfoTable.Col.UUID));
String title = getString(getColumnIndex(InfoTable.Col.TITLE));
long date = getLong(getColumnIndex(InfoTable.Col.DATE));
Info info = new Info(UUID.fromString(uuidString));
info.setmTtitle(title);
info.setmDate(new Date(date));
return info;
}
}
step 9 : 使用cursor封装方法(InfoLab.java)
private InfoCursorWrapper queryInfo(String whereClaues,String[] whereArgs){
Cursor cursor = mDateBase.query(
InfoTable.NAME,// Columns - null selects all columns
whereClaues,// groupBy
null,// having
null // orderBy
);
return new InfoCursorWrapper(cursor);
}
step 10 :返回info列表(InfoLab.java)
public ArrayList<Info> getInfos(){
// return mInfos;
//return new ArrayList<>();
ArrayList<Info> infos = new ArrayList<>();
InfoCursorWrapper cursor = queryInfo(null,null);
try {
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
infos.add(cursor.getInfo());
cursor.moveToNext();
}
} finally {
cursor.close();
}
return infos;
}
要从cursor中取出数据,首先要调用moveToFirst()方法移动cursor指向第一个元素。读取行记录后,再调用moveToNext()方法,读取下一行记录,直到isAfterLast()告诉我们没有数据可取为止。
step11 :重写getInfo(UUID)方法(InfoLab.java)
public Info getInfo(UUID uuid){
// for(Info i:mInfos){
// if(i.getmId().equals(uuid)){
// return i;
// }
// }
// return null;
InfoCursorWrapper cursor = queryInfo(
InfoTable.Col.UUID + " = ?",new String[] { uuid.toString() }
);
try {
if (cursor.getCount() == 0) {
return null;
}
cursor.moveToFirst();
return cursor.getInfo();
} finally {
cursor.close();
}
}
上述代码的作用如下。
现在可以插入info记录了。也就是说,点击New Crime菜单项,实现将info添加到InfoLab的代码可以正常工作了。
数据库查询没有问题了。 InfoDetailActivity现在能够看见InfoLab中的所有Info了。
InfoLab.getInfo(UUID) 方 法 也 能 正 常 工 作 了 。 InfoDetailActivity 终于可以显示真正的Info对象了。
step 12 : 刷新模型层数据
添加setInfos(List<Info>)
方法(InfoListActivity.java):
private class InfoAdapter extends RecyclerView.Adapter<InfoHolder> {
...
@Override
public int getItemCount() {
return mInfos.size();
}
public void setInfos(List<Info> infos){
mInfos = infos;
}
}
然后在updateUI()方法中调用setInfos(List<Info> infos)
方法(InfoListActivity.java)
private void updateUI() {
InfoLab infoLab = InfoLab.get(this);
List<Info> infos = infoLab.getInfos();
if(mAdapter==null){
mAdapter = new InfoAdapter(infos);
mInfoRecyclerView.setAdapter(mAdapter);
}
else{
mAdapter.setInfos(infos);
mAdapter.notifyDataSetChanged();
}
...
}
现在,可以验证我们的成果了。运行应用,新增一项info记录,然后按回退键,
确认InfoListActivity中会出现刚才新增的记录。数据库中也添加了info记录。
源码在这里。