在日常项目开发中,我们不可避免的会使用到数据库。Android中自带SQLite数据库是我们在开发中必须知道和了解的,那么如何更好的使用SQLite数据库就是我们需要学习的内容了。
首先,需要创建一个类继承SQLiteOpenHelper,并重写onCreate()与onUpdate()方法。
public class DBHelper extends SQLiteOpenHelper {
private static final int DB_VERSION = 1;
private static final String DB_NAME = "test.db";
private static final String TABLE_NAME = "t_test";
/**
* 构造方法,创建数据库
*
* @param context
*/
public DBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
/**
* 创建表
*
* @param db
*/
@Override
public void onCreate(SQLiteDatabase db) {
String sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (Id integer PRIMARY KEY,UserName text, Age integer, Country text)";
db.execSQL(sql);
}
/**
* 升级表
*
* @param db
* @param oldVersion
* @param newVersion
*/
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
String sql = "DROP TABLE IF EXISTS " + TABLE_NAME;
db.execSQL(sql);
onCreate(db);
}
}
拥有这个类之后,就可以通过类实例的getWritableDatabase()和getReadableDatabase()方法获取到SQLiteDatabase对象,对数据库进行增删改查操作了。那么这两个方法获取到的SQLiteDatabase对象有什么区别呢?
- 两个方法获取到的SQLiteDatabase对象都具有读与写的功能。
- getWritableDatabase()方法以读写的方式打开数据库,若数据库已满,则此时调用getWritableDatabase()方法获取SQLiteDatabase实例将会发生错误(异常)。
- getReadableDatabase()先以getWritableDatabase()方式打开数据库,若数据库已满,则以getReadableDatabase()获取SQLiteDatabase实例以只读的方式打开数据库。
拿到SQLiteDatabase对象后,我们就可以操作数据库了,但是在使用的时候要注意数据库的线程安全问题。在不同线程中使用DBHelper去操作数据库将会抛出如下异常:
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
这是因为你每次去new一个SQLiteOpenHelper对象时都会去创建一个新的数据库连接,所以如果你试图同时从不同的连接去操作数据库时,就会抛出此异常。因此,在操作数据库时我们需要确保我们只用一个数据库连接。因此,我们可以创建一个DataBaseHelper单例类用于管理数据库连接。代码如下所示:
public class DataBaseHelper {
private static DataBaseHelper instance;
private static SQLiteOpenHelper mDatabaseHelper;
public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
if (instance == null) {
instance = new DataBaseHelper();
mDatabaseHelper = helper;
}
}
public static synchronized DataBaseHelper getInstance() {
if (instance == null) {
throw new IllegalStateException(DataBaseHelper.class.getSimpleName() +
" is not initialized, call initialize(..) method first.");
}
return instance;
}
public synchronized SQLiteDatabase getDatabase() {
return mDatabaseHelper.getWritableDatabase();
}
}
这样子,我们在不同线程中操作数据库就没有问题了吗?然而事实并没有你想像的那么简单。我们都知道,在我们操作数据库的时候,每次操作完成之后就必须关闭数据库以节省资源,这样在不同线程中会出现这么一种情形,一个线程中的数据库操作结束后将数据库关闭,而另一个线程中的数据库操作却还没有完成,因此,系统会抛出如下异常:
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase
当然,我们可以在数据库操作结束后不去关闭连接,然而,这样会造成程序内存泄漏,系统提示如下:
Leak found
Caused by: java.lang.IllegalStateException: SQLiteDatabase created and never closed
所以,怎么样才能让数据库的操作在不同的线程中保持线程安全呢?我们可以使用一个AtomicInteger 变量去判断数据库使用的总数,从而进行相关的操作,完整代码如下:
public class DataBaseHelper {
private AtomicInteger mOpenCounter = new AtomicInteger();
private static DataBaseHelper instance;
private static SQLiteOpenHelper mDatabaseHelper;
private static SQLiteDatabase mDatabase;
public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
if (instance == null) {
instance = new DataBaseHelper();
mDatabaseHelper = helper;
}
}
public static synchronized DataBaseHelper getInstance() {
if (instance == null) {
throw new IllegalStateException(DataBaseHelper.class.getSimpleName() +
" is not initialized, call initialize(..) method first.");
}
return instance;
}
public synchronized SQLiteDatabase openDatabase() {
if (mOpenCounter.incrementAndGet() == 1) {
// Opening new database
mDatabase = mDatabaseHelper.getWritableDatabase();
}
return mDatabase;
}
public synchronized void closeDatabase() {
if (mOpenCounter.decrementAndGet() == 0) {
// Closing database
mDatabase.close();
}
}
}
有了上述两个类的准备,我们就可以对数据库进行增删改查的操作了,同时可以保证它是线程安全的。我们需要在自己的Application类中对DataBaseHelper进行初始化操作,代码如下:
@Override
public void onCreate() {
super.onCreate();
DataBaseHelper.initializeInstance(new DBHelper(this));
}
别忘了在清单文件中配置自定义的Application。对于数据库的增、删、改,我们可以通过调用execSQL(String sql)方法或对应的操作API:insert()、delete()、update()。但是对于查的操作,我们就需要调用query()或rawQuery()方法了。
增加数据
我们可以使用execSQL(String sql)或者insert(String table,String nullColumnHack,ContentValues values)方法来插入,示例代码如下:
//通过execSQL()方法插入数据
db.beginTransaction();
db.execSQL("insert into " + DBHelper.TABLE_NAME + " (Id, UserName, Age, Country) values (1, 'Arc', 30, 'China')");
db.setTransactionSuccessful();
DataBaseHelper.getInstance().closeDatabase();
//通过insert()方法插入数据
db = DataBaseHelper.getInstance().openDatabase();
db.beginTransaction();
ContentValues values = new ContentValues();
values.put("Id", 2);
values.put("UserName", "Alice");
values.put("Age", 25);
values.put("Country", "America");
db.insert(DBHelper.TABLE_NAME, null, values);
db.setTransactionSuccessful();
DataBaseHelper.getInstance().closeDatabase();
删除数据
删除数据的方法除了execSQL还有delete(String table,String whereClause,String[] whereArgs),whereClause是删除条件,whereArgs是删除条件值数组,代码如下:
//通过delete()方法删除Id=2的数据
db = DataBaseHelper.getInstance().openDatabase();
db.beginTransaction();
db.delete(DBHelper.TABLE_NAME, "Id = ?", new String[]{String.valueOf(1)});
db.setTransactionSuccessful();
DataBaseHelper.getInstance().closeDatabase();
点击delete方法进入源码可以发现,源码内拼装了删除条件和删除条件值得数组,源码如下:
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();
}
}
修改数据
修改数据可以调用execSQL()和update(String table,ContentValues values,String whereClause, String[] whereArgs)方法,代码如下:
//通过update()方法修改Id=1的数据Age=22
db = DataBaseHelper.getInstance().openDatabase();
db.beginTransaction();
ContentValues cv = new ContentValues();
cv.put("Age", 22);
db.update(DBHelper.TABLE_NAME,
cv,
"Id = ?",
new String[]{String.valueOf(1)});
db.setTransactionSuccessful();
DataBaseHelper.getInstance().closeDatabase();
查找数据
查找数据有两个方法,一是public Cursor query(String table,String[] columns,String selection,String[] selectionArgs,String groupBy,String having,String orderBy,String limit);,另外一个是public Cursor rawQuery(String sql, String[] selectionArgs)。rawQuery的写法类似上面的execSQL,在此不做介绍,query方法中的参数如下:
- table:表名称
- columns:列名称数组
- selection:条件字句,相当于where
- selectionArgs:条件字句,参数数组
- groupBy:分组列
- having:分组条件
- orderBy:排序列
- limit:分页查询限制
- Cursor:返回值,相当于结果集ResultSet
返回值Cursor是一个游标对象,其常用的方法如下表所示:
方法名 | 方法描述 |
---|---|
getCount() | 获得总数据条数 |
isFirst() | 判断是否为第一条记录 |
isLast() | 判断是否为最后一条记录 |
moveToFirst() | 移动到第一条记录 |
moveToLast() | 移动到最后一条记录 |
move(int offset) | 移动到指定记录 |
moveToPrevious() | 移动到上一条记录 |
moveToNext() | 移动到下一条记录 |
getColumnIndexOrThrow() | 根据列名获取索引 |
getInt(int columnIndex) | 获取指定索引的int类型数据 |
getString(int columnIndex) | 获取指定索引的String类型数据 |
查询数据的代码如下所示:
//查询表中所有数据
db = DataBaseHelper.getInstance().openDatabase();
Cursor cursor = db.query(DBHelper.TABLE_NAME, null, null,
null, null, null, null);
if (cursor.getCount() > 0) {
while (cursor.moveToNext()) {
int id = cursor.getInt(cursor.getColumnIndex("Id"));
String userName = cursor.getString(cursor.getColumnIndex("UserName"));
int age = cursor.getInt(cursor.getColumnIndex("Age"));
String country = cursor.getString(cursor.getColumnIndex("Country"));
}
}
cursor.close();
DataBaseHelper.getInstance().closeDatabase();
参考资料: