Android数据库(3)、Android对SQLite数据库的支持

Android官方默认的数据库就是SQLite数据库,并且相应的Android API 也对SQLite数据做了一定的支持,下面主要通过四个方面来了解一下,具体在Android的程序代码当中如何使用相应的数据库。

一、创建数据库,(SQLiteOpenHelper)

Android系统当中,数据库其实就是一个文件系统的文件,不过这个文件是以DB结尾的文件。创建数据库,主要分为两种,就是使用默认的SQLiteOpenHelper来创建属于当前应用程序的沙盒数据库,什么是沙盒数据库呢,就是默认的db文件在/data/data当中,第二种创建数据库就是在SD卡当中创建数据库。下面通过两个示例来了解一下。

1、使用默认的SQLiteOpenHelper创建沙盒数据库。

使用这种方式,主要就是让数据库当中的数据只给当前的应用来使用,当然通过ContentProvider属于另当别论了,想要创建一个数据库,很简单,我们只需要创建一个类,让这个来继承相应的SQLiteOpenHelper即可,重写相应的方法,来让Android来进行回调,完成一个数据库的创建,下面就是示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
* 创建数据库
* @author pengchengliu
*/
public class KeyValueDataHelper extends SQLiteOpenHelper{

private static final String DB_NAME = "test.db";
private static final int DB_VERSION = 1 ;

private static final String DB_INIT_TABLE = "CREATE TABLE test(id INTEGER " +
"PRIMARY KEY NOT NULL,key TEXT,value TEXT)" ;

/**
* 创建相应的数据库,
* @param context:底层需要通过Context拿到相应的沙盒文件路径,以及通过Context来打开或者创建数据库
*
* 到相应的沙盒文件路径
* final String path = mContext.getDatabasePath(mName).getPath();
*
* 通过Context来打开或者创建数据库
* db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
* Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
* mFactory, mErrorHandler);
*/
public KeyValueDataHelper(Context context) {
//CursorFactory:
//String name, 数据库文件的名字
//CursorFactory factory, Cursor工厂,查询时返回自定义的Cursor对象
//int version:当前数据库的版本,通过该标示来判断数据库的降级以及升级操作
super(context, DB_NAME, null, DB_VERSION);
}

/**
* 数据库刚刚建立的时候进行初始化工作,在我们日常开发当中常常使用这个方法来创建我们需要表结构,以及一些常用的
* 初始化工作。
* @param db :底层刚刚创建好的DataBase数据库操作对象,通过该对象我们就可以创建表结构。
*
* 回调时机:
* SQLiteOpenHelper#getDatabaseLocked(){
* try {
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* } else {
* onUpgrade(db, version, mNewVersion);
* }
* }
* }
*
*/
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(DB_INIT_TABLE);
}

/**
* 数据库刚刚建立的时候进行初始化工作,在我们日常开发当中常常使用这个方法来配置我们的数据库,
* 比如打开我们数据的关系完整性等,具体常用配置,如下所示。
*
* @param db:底层刚刚创建好的DataBase数据库操作对象,通过该对象我们就可以通过这个对象来配置我们的数据库
*
* pragma auto_vacuum = 0|1; 没有数据有效,有数据无效,顾应该在创建表之前
* pragma cache_size=9000;default_cache_size
* pragma page_size = bytes;
* pragma synchronous = Full/Normal/off
* pragma temp_store = Memory/File/Default
*
* 注意:这个方法会在,我们SQLiteOpenHelper#onCreate回调之前进行回调,
*
* 回调时机
* SQLiteOpenHelper#getDatabaseLocked(){
* onConfigure(db);
* final int version = db.getVersion();
* if (version != mNewVersion) {
* if (db.isReadOnly()) {
* throw new SQLiteException("Can't upgrade read-only database from version " +
* db.getVersion() + " to " + mNewVersion + ": " + mName);
* }
* db.beginTransaction();
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* onUpgrade(db, version, mNewVersion);
* }
* }
* db.setVersion(mNewVersion);
* db.setTransactionSuccessful();
* } finally {
* db.endTransaction();
* }
* onOpen(db);
* }
* 默认不会对数据库做任何配置。
*/
@Override
public void onConfigure(SQLiteDatabase db) {
super.onConfigure(db);
}

/**
* 数据库版本升级的时候,我们就可以使用这个方法来维护我们的数据库,让其变为更加强大的存在。
* @param db :底层创建好的DataBase数据库操作对象,通过该对象我们就可以在版本变动的时候,维护我们的数据了
*
* 回调时机:
* SQLiteOpenHelper#getDatabaseLocked(){
* try {
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* } else {
* onUpgrade(db, version, mNewVersion);
* }
* }
* }
*
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}

/**
* 数据库版本降级的时候,会进行回调,注意,这个方法不是空实现,这个方法的实现如下所示:
*
* public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
* throw new SQLiteException("Can't downgrade database from version " +
* oldVersion + " to " + newVersion);
* }
*
* 这也是为什么我们数据降级的时候,会报错的主要原因,如果要解决上述这个问题,那么我们需要重写这个方法,让其空实现即可
*
* @param db :底层创建好的DataBase数据库操作对象,通过该对象我们就可以在版本变动的时候,维护我们的数据了
*
* 回调时机:
* SQLiteOpenHelper#getDatabaseLocked(){
* try {
* if (version == 0) {
* onCreate(db);
* } else {
* if (version > mNewVersion) {
* onDowngrade(db, version, mNewVersion);
* } else {
* onUpgrade(db, version, mNewVersion);
* }
* }
* }
*
*/
@Override
public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
super.onDowngrade(db, oldVersion, newVersion);
}

/**
* 获取数据库的名字。
* @return 数据库的名字,其值就是我们在SQLiteOpenHelper构造函数当中传入的Name的值
*/
@Override
public String getDatabaseName() {
return super.getDatabaseName();
}

/**
* 获取可读的数据库操作对象SQLiteDatabase,其底层是通过一个getDatabaseLocked方法来实现的
* @return 数据库操作对象SQLiteDatabase
*/
@Override
public SQLiteDatabase getReadableDatabase() {
//getDatabaseLocked
return super.getReadableDatabase();
}

/**
* 获取可写的数据库操作对象SQLiteDatabase,其底层是通过一个getDatabaseLocked方法来实现的
* @return 数据库操作对象SQLiteDatabase
*/
@Override
public SQLiteDatabase getWritableDatabase() {
//getDatabaseLocked
return super.getWritableDatabase();
}

/**
* 启用或禁用数据库的预写日志记录的使用。
* 预写日志记录不能与只读数据库一起使用,如果数据库以只读方式打开,则忽略此标志。
* @param enabled,启用预写式日志记录,true为启用,false为被禁用。
*
* 注意:其底层是通过
* Database.enableWriteAheadLogging();
* Database.disableWriteAheadLogging();来实现的。
*/
@Override
public void setWriteAheadLoggingEnabled(boolean enabled) {
super.setWriteAheadLoggingEnabled(enabled);
}
}

上述代码当中,应该注意的东西都使用注释的形式,进行了相应的标注,这里还有两个比较容易混淆的两个概念,和一个关于初始化SQLiteOpenHelper的相应问题,需要我们具体的进行讨论。

1.1、getReadableDatabase()方法和getWritableDatabase()的区别究竟是什么?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//SQLiteDatabaseHelper#getDatabaseLocked(boolean writable)
if (db != null) {
if (writable && db.isReadOnly()) {
db.reopenReadWrite();
}
} else if (mName == null) {
db = SQLiteDatabase.create(null);
} else {
try {
if (DEBUG_STRICT_READONLY && !writable) {
final String path = mContext.getDatabasePath(mName).getPath();
db = SQLiteDatabase.openDatabase(path, mFactory,
SQLiteDatabase.OPEN_READONLY, mErrorHandler);
} else {
db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
Context.MODE_ENABLE_WRITE_AHEAD_LOGGING : 0,
mFactory, mErrorHandler);
}
} catch (SQLiteException ex) {
if (writable) {
throw ex;
}
Log.e(TAG, "Couldn't open " + mName
+ " for writing (will try read-only):", ex);
final String path = mContext.getDatabasePath(mName).getPath();
db = SQLiteDatabase.openDatabase(path, mFactory,
SQLiteDatabase.OPEN_READONLY, mErrorHandler);
}
}

//SQLiteDatabaseHelper#openDatabase
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
DatabaseErrorHandler errorHandler) {
SQLiteDatabase db = new SQLiteDatabase(path, flags, factory, errorHandler);
db.open();
return db;

在源码当中找到了,上述一段代码。可以大体看一下,上述代码用语言总结一下就是

  • getWritableDatabase()和getReadableDatabase()方法都可以获取一个用于操作数据库的SQLiteDatabase实例。但是getWritableDatabase()方法以读写方式打开数据库,一旦数据库的磁盘空间满了,数据库就只能读而不能写,getWritableDatabase()打开数据库就会出错。getReadableDatabase()方法先以读写方式打开数据库,倘若使用如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读方式打开数据库。

一般情况下两者返回情况都是相同的,唯一的区别是:在数据库仅开放只读权限或磁盘已满时,getReadableDatabase只会返回一个只读的数据库对象。

1.2、SQLiteOpenHelper的初始化,做了哪些重量级的操作,真正的初始化工作,是在什么地方开始做的。

通过研究SQLiteOpenHelper的源码,我们发现正常去和数据库建立连接不是在new SQLiteOpenHelper的时候触发的,真正触发的点就是getWritableDatabase()和getReadableDatabase(),所以在使用的时候,切记UI线程当中最好不要去调用getReadableDatabase/getWritableDatabase这两个方法,这只是为了我们程序的稳定性来说的。

2、在文件系统的SD当中创建数据库。

通过研究和仿照SQLiteOpenHelper,可以提取到精髓的地方,那就是可以使用Google的方式,在SD卡当中创建一个数据库,首先说说在什么情况下,需要在SD卡上面创建我们的数据库,也就是说在SD卡上面创建数据库的优点是什么?

2.1、当系统卸载我们的应用的时候,当我们重新安装应用的时候,数据不会丢失。
2.2、方便备份、恢复。

当然缺点也是有的,数据库不安全等。笔者这里以技术的角度来说明,如何在SD卡上面创建我们的数据,但是不代表笔者赞同这一种做法,这种做法会很严重的破坏Android的生态,对于用户来说,安装了应用,在卸载了应用以后,数据没有清干净,这就代表着,应用程序不是以一个完整沙盒的模式呈现给操作系统,操作系统也不方便管理应用程序,

笔者在这里的建议就是,最好使用Google原生提供的目录来存储哪些需要存储的数据。给出以下目录,供各位参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 获得缓存文件路径,磁盘空间不足或清除缓存时数据会被删掉,一般存放一些临时文件
// /data/data/<application package>/cache目录
File cacheDir = getCacheDir();
Log.d("TAG", "getCacheDir() : " + cacheDir.getAbsolutePath());

// 获得文件存放路径,一般存放一些需要长期保留的文件
// /data/data/<application package>/files目录
File fileDir = getFilesDir();
Log.d("TAG", "getFilesDir() : " + fileDir.getAbsolutePath());

// 这是一个可以存放你自己应用程序自定义的文件,你可以通过该方法返回的File实例来创建或者访问这个目录
// /data/data/<application package>/
File dir = getDir("fileName", MODE_PRIVATE);
Log.d("TAG", "getDir() : " + dir.getAbsolutePath());

// 获取应用程序外部存储的缓存目录路径
// SDCard/Android/data/<application package>/cache目录
File externalCacheDir = getExternalCacheDir();
Log.d("TAG", "getExternalCacheDir() : " + externalCacheDir.getAbsolutePath());

// 获取应用程序外部存储的某一类型的文件目录,
// SDCard/Android/data/<application package>/files目录
// 这里的类型有
// Environment.DIRECTORY_MUSIC音乐
// Environment.DIRECTORY_PODCASTS 音频
// Environment.DIRECTORY_RINGTONES 铃声
// Environment.DIRECTORY_ALARMS 闹铃
// Environment.DIRECTORY_NOTIFICATIONS 通知铃声
// Environment.DIRECTORY_PICTURES 图片
// Environment.DIRECTORY_MOVIES 视频
File externalFilesDir = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
Log.d("TAG", "getExternalFilesDir() : " + externalFilesDir.getAbsolutePath());

// 获取应用的外部存储的缓存目录
File[] externalCacheDirs = getExternalCacheDirs();
for (int i = 0; i < externalCacheDirs.length; i++) {
Log.d("TAG", "getExternalCacheDirs() " + i + " : " + externalCacheDirs[i].getAbsolutePath());
}

// 获取应用的外部存储的某一类型的文件目录
File[] externalFilesDirs = getExternalFilesDirs(Environment.DIRECTORY_MUSIC);
for (int i = 0; i < externalFilesDirs.length; i++) {
Log.d("TAG", "getExternalFilesDirs() " + i + " : " + externalFilesDirs[i].getAbsolutePath());
}

// 获取应用的外部媒体文件目录
File[] externalMediaDirs = getExternalMediaDirs();
for (int i = 0; i < externalMediaDirs.length; i++) {
Log.d("TAG", "getExternalMediaDirs() " + i + " : " + externalMediaDirs[i].getAbsolutePath());
}

// 获得应用程序指定数据库的绝对路径
// /data/data/<application package>/database/database.db目录
File databasePath = getDatabasePath("database.db");

SD卡创建数据库的具体思路是: 通过android的SQLiteOpenHelper类的源码,可以看到SQLiteOpenHelper类的getWritableDatabase这个接口实际上调用的是Context的openOrCreateDatabase方法,而这个方法是不支持带路径的数据库名称的,也就是说,用这个方法创建的数据库只能放在/data/data/包名称/ 目录下;要想在SD卡上创建数据库,我们可以调用SQLiteDatabase类的openOrCreateDatabase方法,这个方法是支持带路径的数据库名称的。

下面来看一则示例,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
import android.database.sqlite.SQLiteDatabase;
import android.os.Environment;

import java.io.File;
import java.io.IOException;

/**
* SD卡创建数据库,SQLiteDatabase类的openOrCreateDatabase支持带参数,所以选择该方法
* Created by pengchengliu on 2018/3/24.
*/
public class DatabaseSDManager {

private static final String DATABASE_FILE_DIRECTORY = "/dateBase" ;
private static final String DATABASE_PATH = Environment.getExternalStorageDirectory().getPath() +
File.separator + DatabaseSDManager.class.getPackage().getName() + DATABASE_FILE_DIRECTORY;

private String mDataBaseName ;
private SQLiteDatabase.CursorFactory mFactory ;
private SQLiteDatabase mSQLiteDatabase ;

public DatabaseSDManager(String dtaBaseName){
this(dtaBaseName,null);
}

public DatabaseSDManager(String dataBaseName, SQLiteDatabase.CursorFactory factory ){
this.mDataBaseName = dataBaseName ;
this.mFactory = factory ;
}

public SQLiteDatabase getDataBase(){
if(mSQLiteDatabase == null){
mSQLiteDatabase = SQLiteDatabase.
openOrCreateDatabase(initDatabaseFilePath(), this.mFactory);
}
return mSQLiteDatabase;
}


/**
* 初始化DataBase的文件目录
*/
private File initDatabaseFilePath() {
boolean sdIsExist = Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
if(!sdIsExist){
throw new IllegalArgumentException("SD is not Find, Please check Sd state");
}

File dirfile = new File(DATABASE_PATH + File.separator);
if(!dirfile.exists()){
if(!dirfile.mkdirs()){
throw new RuntimeException("create file is fail");
}
}

boolean isCreateDbFileSuccess = false ;

File dbFile = new File(dirfile, this.mDataBaseName);
if(!dbFile.exists()){
try {
isCreateDbFileSuccess = dbFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
} else {
isCreateDbFileSuccess = true ;
}
return isCreateDbFileSuccess ? dbFile : null ;
}

public void close(){
if(mSQLiteDatabase != null){
mSQLiteDatabase.close();
mSQLiteDatabase = null ;
}
}
}

上述的Demo当中,我们没有加入Version的维护,只是一个简单的小例子,如果在开发工作当中,必定会加入版本的维护,具体代码如何实现,可以具体参考SQLiteOpenHelper的实现,

还有一种实现方式便是我们自定义Context来实现,我们通过源码知道,我们使用SQLiteOpenHelper的时候,具体的路径是写死的,我们可以自定义Context来实现我们的这个功能,具体的源码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
import android.content.Context;
import android.content.ContextWrapper;
import android.database.DatabaseErrorHandler;
import android.database.sqlite.SQLiteDatabase;
import android.util.Log;

import java.io.File;
import java.io.IOException;

/**
*
* Created by pengchengliu on 2018/3/25.
*/

public class DataBaseContext extends ContextWrapper {
/**
* 构造函数
*
* @param base 上下文环境
*/
public DataBaseContext(Context base) {
super(base);
}

/**
* 获得数据库路径,如果不存在,则创建对象对象
*/
@Override
public File getDatabasePath(String name) {
//判断是否存在sd卡
boolean sdExist = android.os.Environment.MEDIA_MOUNTED.equals(android.os.Environment.getExternalStorageState());
if (!sdExist) {//如果不存在,
Log.e("SD卡管理:", "SD卡不存在,请加载SD卡");
return null;
} else {//如果存在
//获取sd卡路径
String dbDir = android.os.Environment.getExternalStorageDirectory().toString();
dbDir += "/scexam";//数据库所在目录
String dbPath = dbDir + "/" + name;//数据库路径
//判断目录是否存在,不存在则创建该目录
File dirFile = new File(dbDir);
if (!dirFile.exists())
dirFile.mkdirs();

//数据库文件是否创建成功
boolean isFileCreateSuccess = false;
//判断文件是否存在,不存在则创建该文件
File dbFile = new File(dbPath);
if (!dbFile.exists()) {
try {
isFileCreateSuccess = dbFile.createNewFile();//创建文件
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else
isFileCreateSuccess = true;

//返回数据库文件对象
if (isFileCreateSuccess)
return dbFile;
else
return null;
}
}

/**
* 重载这个方法,是用来打开SD卡上的数据库的,android 2.3及以下会调用这个方法。
*
* @param name
* @param mode
* @param factory
*/
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode,
SQLiteDatabase.CursorFactory factory) {
SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
return result;
}

/**
* Android 4.0会调用此方法获取数据库。
*
* @param name
* @param mode
* @param factory
* @param errorHandler
* @see android.content.ContextWrapper#openOrCreateDatabase(java.lang.String, int,
* android.database.sqlite.SQLiteDatabase.CursorFactory,
* android.database.DatabaseErrorHandler)
*/
@Override
public SQLiteDatabase openOrCreateDatabase(String name, int mode, SQLiteDatabase.CursorFactory factory,
DatabaseErrorHandler errorHandler) {
SQLiteDatabase result = SQLiteDatabase.openOrCreateDatabase(getDatabasePath(name), null);
return result;
}
}

然后,我们使用的时候只要按照,最初的时候就OK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

/**
* DataBase helper class
* Created by pengchengliu on 2018/3/25.
*
*/
public class DbOpenHelper extends SQLiteOpenHelper {

private static final String DB_NAME = "test.db";
private static final int VERSION = 1;

public DbOpenHelper(Context context) {
super(context instanceof DataBaseContext ? context : new DataBaseContext(context),
DB_NAME, null, VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {//It will be called when the database was created first
db.execSQL("CREATE TABLE IF NOT EXISTS exam_type (id integer primary key autoincrement, type_name varchar(100), type_id INTEGER)");
}

@Override // It'll be called when the database was updated
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

}
}

到此,关于数据库的创建的所有知识都梳理了一遍。

二、管理数据库

据Google官方所说,一个打开数据库的连接,将会占用1kb的内存,在内存受限的移动设备来说,这1kb足够引起我们的注意,现在来讨论一下,如何管理一个数据库的生命周期,这里笔者给出两种管理策略,这两种策略也是我们日常开发当中常常使用的,具体的策略如下所示:

1、让应用程序来一直持有相应的数据库连接的引用,也就是一直持有SQLitDatabase这个类的引用。

这种方式很简单,就是申明一个成员变量来保持当前数据库引用。如果我们这样做了,其实就代表着这一块的内存,我们无法回收,并且无法重用。这里需要注意的是,当我们的应用程序至于后台,并且在手机内存不足的情况下回收内存的时候,如果我们没有显示的关闭和数据库的连接,那么应用程序将会报错。所以在使用这一种的策略的时候,一定要注意关于db的关闭问题。

其次还有一个需要我们注意的是在Activity当中持有一个数据库的连接的时候,必要的是在Actiivty销毁的时候,关闭该连接,其实如果我们的应用程序当中存在大量的数据访问工作,我们需要在Application当中去持有这个连接引用,好方便在多个Actiivty,Service当中去获取这个引用,具体实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import android.app.Application;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;

import suansuan.com.contentprovider.sql.KeyValueDataHelper;

/**
* 管理数据库的引用
* Created by pengchengliu on 2018/3/25.
*/

public class DemoApplication extends Application{

private SQLiteOpenHelper mDbHelper ;
private Thread mUiThread ;

@Override
public void onCreate() {
super.onCreate();

mDbHelper = new KeyValueDataHelper(this);
mUiThread = Thread.currentThread() ;
}

public SQLiteDatabase getSQLiteDatabase(){
if(Thread.currentThread().equals(mUiThread)){
throw new RuntimeException("Database opened on main Thread");
}
//根据自己的具体需求,确定使用可写数据库还是可读数据库。具体区别,可以参考上述章节内容
return mDbHelper.getWritableDatabase();
}
}

2、需要连接的时候再获取。

使用这种策略,必须要清楚当前应用程序当中有哪些对象获取了数据库的连接,关于Java的垃圾回收机制,我们知道当一个对象只有不被任何对象引用的时候,才会被回收,因此,如果我们关闭数据库而没有释放所有对它的引用,将是毫无用处的,因为除非释放最后一个引用,否则该对象不能被回收。我们知道SQLiteOpenHelper、SQLiteQuery、SQLiteCursor保存着数据库的引用,所以在不用的时候,记得回收。

三、操作数据库

1、基本SQL语句的嵌入

Android,当中提供了SQLiteDatebase这个类来操作数据库,这个类提供了一个最基本的和数据库交互的方法,那就是execSQL这个方法,通过这个最基本的方法,我们就可以去操作数据库,可以使用这个方法去创建表结构等,但是使用这种方法,有很多的确定,下列就来描述一下,对开发人员来说很不要的缺点。

  • A、在SQLite数据库当中有一部分的命令不是直接解释给SQL的,而是面向SQLite的,我们称这些为元命令,使用execSQL方法的第一个缺点就是不能执行所谓的元命令。常见元命令有以下这几种
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.nullvalue STRING    在 NULL 值的地方输出 STRING 字符串。
.output FILENAME 发送输出到 FILENAME 文件。
.output stdout 发送输出到屏幕。
.print STRING... 逐字地输出 STRING 字符串。
.prompt MAIN CONTINUE 替换标准提示符。
.quit 退出 SQLite 提示符。
.read FILENAME 执行 FILENAME 文件中的 SQL。
.schema ?TABLE? 显示 CREATE 语句。如果指定了 TABLE 表,则只显示匹配 LIKE 模式的 TABLE 表。
.separator STRING 改变输出模式和 .import 所使用的分隔符。
.show 显示各种设置的当前值。
.stats ON|OFF 开启或关闭统计。
.tables ?PATTERN? 列出匹配 LIKE 模式的表的名称。
.timeout MS 尝试打开锁定的表 MS 毫秒。
.width NUM NUM"column" 模式设置列宽度。
.timer ON|OFF 开启或关闭 CPU 定时器。
  • B、不能打包多条SQL语句,按照规定在使用方法execSQL这个方法的时候,按理应该只有一条语句。如果我们非要插入多条语句,并且使用分号隔开,那么以第一个分号结尾以后的语句将被忽略。

  • C、不能使用query相关的语句,execSQL这个方法是没有返回值的,所以涉及到查询语句将不能被使用。并且当我们使用execSQL这个方法查询时候,会抛出异常,提示要使用别的方法。这里会出现一个rawSQL的方法,其实这个方法也属于低级的方法。

一般来说,execSQL、rawSQL这两个方法来说就像是一起冷兵器,开发人员很少使用,最多execSQL用来创建表结构,触发器结构,而rawSQL开发人员基本就没有用过。不是作为编程执行SQL的一般性工具,相反,它们应该是用作访问不常用的SQL的最后手段,

为了替代应用程序代码中嵌入未经检查、非类型化的SQL字符串,Android SQLite API把SQL语义放入到API方法当中,主要分为四类,删除、更新、插入、查询。

2、删除

2.1、对外提供接口

通过观察SQLiteDatabase的API,我们不难发现有一个方法,是删除表里面的数据的,如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 删除数据库表当中的一条数据,
* @param table :要操作的表名字
* @param whereClause :where条件语句,其中的值可以使用占位符来代替
* @param whereArgs : 对前面占位的实体值
* @return : 该操作对一个表结构来说影响了几行
*/
public int delete(String table, String whereClause, String[] whereArgs) {
acquireReference();
try {
SQLiteStatement statement = new SQLiteStatement(this, "DELETE FROM " + table +
(!TextUtils.isEmpty(whereClause) ? " WHERE " + whereClause : ""), whereArgs);
try {
return statement.executeUpdateDelete();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}

通过源码,我们不难看出,这里SQLiteDatabase把相应的操作是委托给了SQLiteStatement,让其进行了相应的删除操作,并且相应的SQL语句也是在这里拼接的。具体内部是如何实现的,不是本章的内容。如果感兴趣可以给下层追一下。

3、更新

实现更新操作的Android API这一次列操作也是比较少的,包括两个常用方法update、updateWithOnConflict,这两个的具体实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public int update(String table, ContentValues values, String whereClause, String[] whereArgs) {
return updateWithOnConflict(table, values, whereClause, whereArgs, CONFLICT_NONE);
}

/**
* 数据库表字段的更新操作
*
* @param table : 要操作的表名
* @param values : 要修改的值
* @param whereClause : where的查询条件
* @param whereArgs : 前面查询条件的替换值
* @param conflictAlgorithm 当破坏主键或者其他字段,所采取的措施,有以下取值
*
* CONFLICT_FAIL :当一个发生违反约束的情况,命令将以返回码中止 SQLITE_CONSTRAINT。
* CONFLICT_IGNORE :当一个约束int违例发生,包含约束的一行 不会插入或更改违规。
* CONFLICT_NONE :U如果没有指定冲突行为,请参阅以下内容。
* CONFLICT_REPLACE :当发生UNIQUE约束冲突时,他预先存在的行是 导致在插入或更新之前删除约束条件 当前行。
* CONFLICT_ROLLBACK :发生约束冲突时,会立即发生ROLLBACK 结束当前交易,该命令将以一个返回码中止 SQLITE_CONSTRAINT。
*
* @return 影响的行数
*/
public int updateWithOnConflict(String table, ContentValues values,
String whereClause, String[] whereArgs, int conflictAlgorithm) {
if (values == null || values.size() == 0) {
throw new IllegalArgumentException("Empty values");
}

acquireReference();
try {
StringBuilder sql = new StringBuilder(120);
sql.append("UPDATE ");
sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(table);
sql.append(" SET ");

// move all bind args to one array
int setValuesSize = values.size();
int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length);
Object[] bindArgs = new Object[bindArgsSize];
int i = 0;
for (String colName : values.keySet()) {
sql.append((i > 0) ? "," : "");
sql.append(colName);
bindArgs[i++] = values.get(colName);
sql.append("=?");
}
if (whereArgs != null) {
for (i = setValuesSize; i < bindArgsSize; i++) {
bindArgs[i] = whereArgs[i - setValuesSize];
}
}
if (!TextUtils.isEmpty(whereClause)) {
sql.append(" WHERE ");
sql.append(whereClause);
}

SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeUpdateDelete();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}

可以看出这里,update属于updateWithOnConflict的子集,上述做了两件事,和上述的删除操作类似,拼接SQL语句,创建一个SQLiteStatement,最后通过statement去执行相应的操作,这里说一下这两个更新方法的区别是什么?

updateWithOnConflict方法含义呢,就是在如果修改中破坏了相应的主键约束,或者说是一定的约束条件,不仅仅是主键约束,需要底层需要如何处理,也就是当发生破坏表字段的约束的时候,需要数据库底层去准备哪些策略,具体策略,可以参考上述代码的注释

ContentValuesd,Android SQLite官方所引入的新的一种数据结构,当我们去修改,或者插入的时候,通过构造着一种数据结构,来整理数据,以便高效的插入。其实它的内部的实现就是一个简单的HashMap<K,V>,那么仔细想一下,为什么不直接使用Map就可以插入,还要多封装一种数据结构呢,其实很简单,只是为了适配相应的SQLite数据库,因为数据库是弱类型的,所以需要这种数据结构,我们看几个它的方法,相信各位看客就明白为什么需要它的原因了,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
public final class ContentValues implements Parcelable {
public static final String TAG = "ContentValues";

/** Holds the actual values */
private HashMap<String, Object> mValues;


/**
* Adds a value to the set.
*
* @param key the name of the value to put
* @param value the data for the value to put
*/
public void put(String key, Short value) {
mValues.put(key, value);
}

/**
* Adds a value to the set.
*
* @param key the name of the value to put
* @param value the data for the value to put
*/
public void put(String key, Integer value) {
mValues.put(key, value);
}

/**
* Adds a value to the set.
*
* @param key the name of the value to put
* @param value the data for the value to put
*/
public void put(String key, Long value) {
mValues.put(key, value);
}

….

/**
* Gets a value and converts it to a String.
*
* @param key the value to get
* @return the String for the value
*/
public String getAsString(String key) {
Object value = mValues.get(key);
return value != null ? value.toString() : null;
}

/**
* Gets a value and converts it to a Long.
*
* @param key the value to get
* @return the Long value, or {@code null} if the value is missing or cannot be converted
*/
public Long getAsLong(String key) {
Object value = mValues.get(key);
try {
return value != null ? ((Number) value).longValue() : null;
} catch (ClassCastException e) {
if (value instanceof CharSequence) {
try {
return Long.valueOf(value.toString());
} catch (NumberFormatException e2) {
Log.e(TAG, "Cannot parse Long value for " + value + " at key " + key);
return null;
}
} else {
Log.e(TAG, "Cannot cast value for " + key + " to a Long: " + value, e);
return null;
}
}
}

/**
* Gets a value and converts it to an Integer.
*
* @param key the value to get
* @return the Integer value, or {@code null} if the value is missing or cannot be converted
*/
public Integer getAsInteger(String key) {
Object value = mValues.get(key);
try {
return value != null ? ((Number) value).intValue() : null;
} catch (ClassCastException e) {
if (value instanceof CharSequence) {
try {
return Integer.valueOf(value.toString());
} catch (NumberFormatException e2) {
Log.e(TAG, "Cannot parse Integer value for " + value + " at key " + key);
return null;
}
} else {
Log.e(TAG, "Cannot cast value for " + key + " to a Integer: " + value, e);
return null;
}
}
}
}

4、插入

插入系列的方法个更新系列的方法几乎相同,但是我们需要注意一点比较重要的就是插入方法可以捕获SQLite的异常,通过insertOrThrow这个方法。还有一个方法就是replace这个方法,其实这个方法很危险,为什么说这个方法危险呢,主要就是它的功能导致的,它的功能为插入一条数据转化为使用OR_EREPLACE_ONCONFLICT算法,已解决违反约束的行为,如果插入数据没有造成冲突,replace方法将插入一个新行,如果造成了冲突,现有行将被替代为新的值。下面看看这三个方法在底层如何实现的,相信大家也就懂了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public long insert(String table, String nullColumnHack, ContentValues values) {
try {
return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
} catch (SQLException e) {
Log.e(TAG, "Error inserting " + values, e);
return -1;
}
}

public long insertOrThrow(String table, String nullColumnHack, ContentValues values)
throws SQLException {
return insertWithOnConflict(table, nullColumnHack, values, CONFLICT_NONE);
}

public long replace(String table, String nullColumnHack, ContentValues initialValues) {
try {
return insertWithOnConflict(table, nullColumnHack, initialValues,
CONFLICT_REPLACE);
} catch (SQLException e) {
Log.e(TAG, "Error inserting " + initialValues, e);
return -1;
}
}

public long replaceOrThrow(String table, String nullColumnHack,
ContentValues initialValues) throws SQLException {
return insertWithOnConflict(table, nullColumnHack, initialValues,
CONFLICT_REPLACE);
}

public long insertWithOnConflict(String table, String nullColumnHack,
ContentValues initialValues, int conflictAlgorithm) {
acquireReference();
try {
StringBuilder sql = new StringBuilder();
sql.append("INSERT");
sql.append(CONFLICT_VALUES[conflictAlgorithm]);
sql.append(" INTO ");
sql.append(table);
sql.append('(');

Object[] bindArgs = null;
int size = (initialValues != null && initialValues.size() > 0)
? initialValues.size() : 0;
if (size > 0) {
bindArgs = new Object[size];
int i = 0;
for (String colName : initialValues.keySet()) {
sql.append((i > 0) ? "," : "");
sql.append(colName);
bindArgs[i++] = initialValues.get(colName);
}
sql.append(')');
sql.append(" VALUES (");
for (i = 0; i < size; i++) {
sql.append((i > 0) ? ",?" : "?");
}
} else {
sql.append(nullColumnHack + ") VALUES (NULL");
}
sql.append(')');

SQLiteStatement statement = new SQLiteStatement(this, sql.toString(), bindArgs);
try {
return statement.executeInsert();
} finally {
statement.close();
}
} finally {
releaseReference();
}
}

5、查询

6、事物

7、批处理

这里还有三点,这个我们后面会说到

四、应用程序中的使用

1、数据库在应用程序中所处的地位

整个开发当中,数据无时无刻都起着很重要的作用,说白的了,程序就是操作数据的指令集而已,MVC,MVP等设计模式,也是将展示和数据分离,在Android当中已是如此,其实在很久以前,分层设计能很好的体现出数据源与视图的关系,熟悉J2EE开发人员应该清楚,程序的底层数据设计DAO,数据操作对象是以何种身份呈现说出来的,这种设计其实在Android当中也是适用的。统一数据源,通过特定的接口统一暴露给上层,然后通过相关的Adapter去设置数据。

网络数据的时候,由于是通过网络拿数据,很多开发者遍忽略了DAO,其实可以理解为网络数据,就是数据库在远端的情况,并且在很多的大型商业项目当中,也是通过ContentProvider去维护网络数据的,这个后面部分笔者应该也会提到,当我们去查找,或者删除的时候去触发远端数据的同步过程,

下面详细的讨论一下,数据库在Android应用程序当中所处的位置。

1.1、UI与DB

UI和DB就是MVP,MVC,MVVP这些设计模式当中提到的很重要的两个关键点,视图与数据源,Android当中的DB一般都会嵌入到ContentProvider当中使用,而相应的视图则有Activity来代替,而相应的中间交互对象则就是接口层CRUD和cursor,通常称之为REST-like模型,但是由于Android当中特殊的线程规定,主线程不能做耗时操作等,所以在从数据源当中拿去Corsur就有点曲折了。

1.2、Cursor与CursorFactory

Cursor就是我们从数据库当中查询到的数据集合,是一种二维形式的数据结构,有点类似于二维数组,包括行和列,还存在一个指向当前行的指针,该指针介于-1到getCount之间,

https://blog.csdn.net/italywj222/article/details/50418577
https://blog.csdn.net/howlaa/article/details/46707159