引入
savedInstanceState无法满足应用持久化保存数据
Android为此提供了长期储存地——闪存上的本地文件系统
Android设备上的应用都有一个沙盒目录,路径通常是:
/data/data/packed name
文件保存再沙盒中,可阻止其他应用和未获得root权限的设备用户
SQLite是类似于MySQL和Postgresql的开源关系型数据库
与其他数据库不同的是,SQLite使用单个文件存储数据
代码部分
建立整个表的框架,将表名字段名定义在类中方便之后的使用
CrimeDbSchema.java
public class CrimeDbSchema {
//CrimeTable内部类唯一用途是定义描述数据表元素的String常量
public static final class CrimeTable{
//数据库的表名
public static final String NAME="crimes";
//定义数据表字段
public static final class Cols {
public static final String UUID = "uuid";
public static final String TITLE = "title";
public static final String DATE = "date";
public static final String SOLVED = "solved";
}
}
}
创建数据库
openOrCreateDatabase(),databaseList()是android提供的Context底层方法,用于打开数据库文件并将其转换为SQLite实例
要打开一个数据库,首先确认目标数据库是否存在,不存再则先创建,若存在则判断是否是最新版本,是就打开不是就升级
这一过程可以用SQLiteOpenHelper类来处理
要覆盖两个方法onCreate,onUpgrade
onCreate中的建表语句
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL("create table "+ CrimeTable.NAME+"("+
"_id integer primary key autoincrement, "+
CrimeTable.Cols.UUID+", "+
CrimeTable.Cols.TITLE+", "+
CrimeTable.Cols.DATE+", "+
CrimeTable.Cols.SOLVED+
")"
);
}
书写建表语句时要注意table与逗号后面的空格
onUpgrade
有时需要调整数据库表结构,比如新增字段
常规做法是在SQLiteOpenHelper记录版本号,然后在onUpgrade方法中升级数据表
但这样的常规方法很复杂,不如直接删除数据库文件然后再重新创建它
删除数据库文件可直接在设备上删除应用
CrimeBaseHelper.java
public class CrimeBaseHelper extends SQLiteOpenHelper {
private static final int VERSION=1;
private static final String DATABASE_NAME="crimeBase.db";
public CrimeBaseHelper(Context context){
super(context,DATABASE_NAME,null,VERSION);
//参数一:context,参数二:数据库名
//参数三:允许在查询数据时返回自定义cursor,一般传入null
//参数四:当前数据库的版本号,可用于对数据库进行升级操作
}
@Override
public void onCreate(SQLiteDatabase db){
db.execSQL("create table "+ CrimeTable.NAME+"("+
"_id integer primary key autoincrement, "+
CrimeTable.Cols.UUID+", "+
CrimeTable.Cols.TITLE+", "+
CrimeTable.Cols.DATE+", "+
CrimeTable.Cols.SOLVED+
")"
);
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
}
}
打开数据库
(再CrimeLab的构造函数中)
private CrimeLab(Context context){
mContext=context.getApplicationContext();
mSQLiteDatabase=new CrimeBaseHelper(mContext)
.getWritableDatabase();
}
调用getWritableDatabase时CrimeBaseHelper的工作
1.打开沙盒目录下的crimeBase.db数据库(名字是在CrimeBaseHelper里定义的);若不存在就先创建
2.若是首次创建则调用onCreate方法,并保存最新版本号
3.若是已创建过的数据库,首先检查它的版本号,若CrimeOpenHelper中的更高(?),则调用onUpgrade方法
新建的数据库文件存放在/data/data/<package name>/databases/
还有一个getReadableDatabase()方法
两个方法都可以创建或打开一个现有的数据库,并返回一个可对数据库进行读写操作的对象
当数据库不可写入时(磁盘空间已满)时,getReadableDatabase()方法返回的对象将以只读的方式打开数据库,getWritableDatabase()方法出现异常
普通的建表语句
create table table_name(
id integer primary key autoincrement,//primary key声明为主键
//autoincrement表示id列是自增的
字段名 字段类型,
……)
SQLite建表是不是必需要指定字段类型
ContentValues键值存储类,类似Bundle,但只用于SQLite数据,负责处理数据库写入与更新操作。
如何把一个Crime数据转换为ContentValues
private static ContentValues getContentValues(Crime crime){
ContentValues values=new ContentValues();
values.put(CrimeTable.Cols.UUID,crime.getId().toString());
values.put(CrimeTable.Cols.TITLE,crime.getTitle());
values.put(CrimeTable.Cols.DATE,crime.getDate().getTime());
values.put(CrimeTable.Cols.SOLVED,crime.getSolved()?1:0);
return values;
}
写入数据库
(CrimeLab的addCrime方法)
mDatabase.insert(String TableName,String nullColumnHack,ContentValues values)
public void addCrime(Crime c){
ContentValues values=getContentValues(c);
mSQLiteDatabase.insert(CrimeTable.NAME,null,values);
}
更新数据库
(CrimeLab的upDateCrime)
mDatabase.update(String TableName,ContentValues,String where,String[] whereArgs)
三四句用于确认需要更新哪些数据
参数三创建where语句,参数四指定where子句的参数
不再where语句中直接放入参数的原因是,String本身很可能包含SQL代码,若直接放入语句中,可能会改变语义十分危险(SQL脚本注入)
public void updateCrime(Crime crime){
String uuidString=crime.getId().toString();
ContentValues values=getContentValues(crime);
mSQLiteDatabase.update(CrimeTable.NAME,values,
CrimeTable.Cols.UUID+"=?",
new String[] {uuidString});
}
之前使用List<>来存crime时,在CrimeFragment中有改动发生时,使用了crime.setTitle之类的方法,crimes中数据也会跟着改变,但现在使用的是数据库,数据库返回的数据的一个快照而不是数据本身,想要修改数据还是要调用数据库的update方法
CrimeFragment.java
@Override
public void onPause()
{
super.onPause();
CrimeLab.get(getActivity()).updateCrime(mCrime);
}
读取数据库
query方法有好几个重载版本
其中之一
public Cursor query(
String table,
String[] columns,
String where,
String whereArgs,
String groupBy,
String having,
String orderBy,
String limit)
Cursor是表处理工具,封装数据表中的原始字段
CursorWrapper可以封装一个个Cursor对象,并在其上添加新的方法,可用来快速方便的创建Cursor的子类
(那直接使用继承Cursor然后添加新方法不就好了,为什么要用CursorWrapper呢)
query方法得到的是cursor
在CrimeCursorWrapper中添加从cursor转换的crime的方法
public class CrimeCursorWrapper extends CursorWrapper {
public CrimeCursorWrapper(Cursor cursor){
super(cursor);
}
public Crime getCrime(){
String uuidString=getString(getColumnIndex(CrimeTable.Cols.UUID));
String title=getString(getColumnIndex(CrimeTable.Cols.TITLE));
long date=getLong(getColumnIndex(CrimeTable.Cols.DATE));
int isSolved=getInt(getColumnIndex(CrimeTable.Cols.SOLVED));
Crime crime=new Crime(UUID.fromString(uuidString));
crime.setTitle(title);
//这一步要用long类型数据造一个Date型数据来
crime.setDate(new Date(date));
crime.setSolved(isSolved!=0);
return crime;
}
}
查询数据并获得CrimeCursorWrapper
(CrimeLab中)
private CrimeCursorWrapper queryCrimes(String whereClause, String[] whereArgs){
Cursor cursor=mSQLiteDatabase.query(
CrimeTable.NAME,
null,
whereClause,
whereArgs,
null,
null,
null
);
return new CrimeCursorWrapper(cursor);
}
重写CrimeLab.getCrimes
public List<Crime> getCrimes(){
List<Crime> crimes=new ArrayList<>();
CrimeCursorWrapper cursor=queryCrimes(null,null);
try{
cursor.moveToFirst();
while(!cursor.isAfterLast()){
crimes.add(cursor.getCrime());
cursor.moveToNext();
}
}finally {
cursor.close();
}
return crimes;
}
cursor中可能有很多条数据,要从中取出数据,首先要调用moveToFirst()方法使其指向第一行记录,然后在调用moveToNext指向下一行记录,直到isAfterLast说明没有数据了
一定要关闭cursor,不然可能会导致应用崩溃
重写CrimeLab.getCrime
public Crime getCrime(UUID id){
CrimeCursorWrapper cursor=queryCrimes(
CrimeTable.Cols.UUID+"=?",
new String[]{id.toString()}
);
try{
if (cursor.getCount()==0){
return null;
}
cursor.moveToFirst();
return cursor.getCrime();
}finally {
cursor.close();
}
}
删除数据
mSQLiteDatabase.delete(String table_name,String where,String[] whereArgs)
给数据库新增一张表
数据库已存在后,就不会在执行DatabaseHelper里的onCreate方法了,若想新增一张表,只在onCreate里加db.execSQL是没用的
public static final String CREATE_CATEGORY="create table Category("
+"id integer primary key autoincrement,"
+"category_name text,"
+"category_code integer)";
public void onCreate(SQLiteDatabase db){
db.execSQL(CREATE_BOOK);
db.execSQL(CREATE_CATEGORY);
Toast.makeText(mContext,"Create succeeded",Toast.LENGTH_SHORT).show();
}
@Override
public void onUpgrade(SQLiteDatabase db,int oldVersion,int newVersion){
db.execSQL("drop table if exists Book");
//如果这张表存在就将它删掉,因为在创建表的时候若发现此表已存在会报错
db.execSQL("drop table if exists Category");
onCreate(db);
}
在构造SQLiteOpenHelper对象是,传入的第四个参数版本号,只要比之前大,就会执行onUpgrade()