Android SQLite数据库访问

Android中提供了对SQLite数据库操作的辅助类SQLiteOpenHelper,它内部封装了SQLiteDatabase的创建和升级操作,里面对数据库容量已满、内部文件损坏等错误都做了异常处理,由于SQLiteOpenHelper事一个抽象类开发中需要继承它并覆盖其中的onCreate()创建方法和onUpgrade()升级方法,当用户调用getReadableDatabase()或getWritableDatabase()时如果数据库还未创建或者数据库版本更改就会执行onCreate()或onUpgrade()操作。

SQLiteHelper的构造函数包含四个参数,第一个时android.content.Context类型,可以在Application对象初始化的时候传入,第二个是数据库名称,第三个是Cursor游标对象的创建工厂,大部分情况下游标对象都不需要自定义,传入空就使用Android的内部的游标创建工厂,最后一个是数据的版本号,版本号是个正数,如果之前有数据库,传入的版本号比之前数据大就属于升级操作,在升级数据库时不会调用onCreate(),只会调用onUpgrade()方法。onCreate()方法会在没有数据库的时候创建数据并调用,它里面通常都会放一些创建数据库表的SQL语句执行代码。

// 数据库创建辅助类
public class DBOpenHelper extends SQLiteOpenHelper {
private static final int DB_VERSION = 1; // 数据库版本号
private static final String DB_NAME = "grade.db";

public DBOpenHelper(Context context) {
// 第三个null就是自定义的游标工厂,null代表使用Android的游标创建工厂
super(context, DB_NAME, null, DB_VERSION);
}

@Override
public void onCreate(SQLiteDatabase db) {
// 第一次创建数据库时调用,创建数据库表
StudentDao.createTable(db);
}

@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// 在DB_VERSION增加后会调用
}
}

SQLite连接封装

SQLite数据库链对象在访问数据库是会锁定数据库,如果此时有其他的连接同时要求访问数据库就会发现数据库目前被锁定无法访问,新连接就会抛出SQLiteDatabaseLockedException终止当前访问。

// 插入更新每次都打开新数据库连接
public void save(Student student) {
	DBOpenHelper helper = new DBOpenHelper(MyApplication.getContext());
	SQLiteDatabase db = helper.getWritableDatabase(); // 每次都打开新数据库连接
	db.execSQL(INSERT, new Object[] { student.getId(), student.getName(), 
		student.getPhone(), student.getAddress(), student.getAge()});
	IOUtils.close(db);
}

public void update(Student student) {
	DBOpenHelper helper = new DBOpenHelper(MyApplication.getContext());
	SQLiteDatabase db = helper.getWritableDatabase();// 每次都打开新数据库连接
	db.execSQL(UPDATE, new Object[] {  student.getName(), student.getPhone(), 
		student.getAddress(), student.getAge(), student.getId()});
	IOUtils.close(db);
}
// 多线程并发执行
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (code 5)
	at android.database.sqlite.SQLiteConnection.nativeExecuteForChangedRowCount
	at android.database.sqlite.SQLiteConnection.executeForChangedRowCount
	at android.database.sqlite.SQLiteSession.executeForChangedRowCount
	at android.database.sqlite.SQLiteStatement.executeUpdateDelete(SQLiteStatement.java:64)
	at android.database.sqlite.SQLiteDatabase.executeSql(SQLiteDatabase.java:1679)
	at android.database.sqlite.SQLiteDatabase.execSQL(SQLiteDatabase.java:1658)
	at com.example.mydatabase.test.StudentDao.update(StudentDao.java:53)

SQLite数据库在底层并未没有处理多连接同时请求的情况,如果每次请求都开启新连接访问,当存在数据并发情况时就会出现数据库访问异常。上面的代码演示了每次插入和更新数据时都打开新的数据库连接,测试时开启多个线程同时执行save()和update()操作,运行时就会抛出数据库已被锁定异常。为此在Android应用运行的某一时刻只保持一个数据库连接,在该时刻所有的数据库访问都使用同一个数据库连接访问就能避免上面访问异常。同一时刻只有单独的数据库连接可以使用单例模式保存该数据库连接对象,不过该数据库链接需要和应用生命周期保持一致,会导致额外的资源消耗。

// 数据库连接单例实现
public class DBManager {
private static final String TAG = "DBManager";
private static volatile DBManager sDBManager; // 注意volatile

private static Context sContext;
private SQLiteDatabase mSQLite;
 // 在Application.onCreate()方法中传入context对象,否则会抛出异常
public static void init(Context context) {
sContext = context.getApplicationContext();
}

private DBManager() {
DBOpenHelper dbOpenHelper = new DBOpenHelper(sContext);
try {
mSQLite = dbOpenHelper.getWritableDatabase();
} catch (Exception e) {
LogUtils.printException(TAG, e);
}
}
// Double Check Lock实现的单例对象
public static DBManager getInstance() {
if (sDBManager == null) {
synchronized (DBManager.class) {
if (sDBManager == null) {
sDBManager = new DBManager();
}
}
}
return sDBManager;
}
// 返回操作数据库表对象
public StudentDao getStudentDao() {
return new StudentDao(mSQLite);
}
}

为了避免单例模式始终保持数据库连接导致资源浪费,可以为数据库连接提供计数引用,在并发访问时只打开一个数据库连接,其他线程获取连接引用会自动增加引用计数,当线程连接使用完毕后需要减少数据库连接引用计数,在引用计数变为零时关闭数据库连接。下一次需要访问数据库时需要重新创建新的数据库连接,该连接的生命周期管理和之前的连接完全一样,都通过引用计数决定是否保持连接状态。

// 数据库连接计数引用实现
public class DBReferenceManager {
    private int referenceCount = 0; // 引用计数

    private static final String TAG = "DBManager";
    private static volatile DBReferenceManager sDBManager; // 注意volatile

    // 省略DCL单例实现代码和其他成员对象
    // 同步方法获取数据库连接,引用计数为0时打开新连接
    public synchronized SQLiteDatabase openConnection() {
        Log.i(TAG, "openConnection() referenceCount = " + referenceCount);
        if (referenceCount == 0) {
            sqLiteDatabase = dbOpenHelper.getWritableDatabase();
        }
        referenceCount++;
        return sqLiteDatabase;
    }
// 同步方法关闭数据库连接,引用计数不为0,只做引用计数减一操作
// 引用计数为0关闭数据库连接
    public synchronized void closeConnection() {
        if (referenceCount <= 0) {
            return;
        }
        referenceCount--;
        Log.i(TAG, "closeConnection() referenceCount = " + referenceCount);
        if (referenceCount == 0) {
            sqLiteDatabase.close();
            sqLiteDatabase = null;
        }
    }
}

上诉代码使用了引用计数管理数据库连接的计数,在openConnection()调用的时候首先根据引用计数是否为零判断目前已经有在被使用的数据库连接,有数据库连接就直接返回,没有则需要创建新数据库连接,返回连接后需要即使递增引用计数。在closeConnection()时引用计数是否为零,如果为零表示当前数据库连接已经被关闭,不为零则先递减数据库引用计数,递减后引用计数为零代表没有任何线程引用数据库连接可以直接关闭数据库连接,不为零时代表依然还有线程在使用当前的数据库连接,需要继续保持数据库连接。注意引用计数被synchronized锁对象保护,不会出现更新丢失的情况。使用引用计数的连接管理一定要记得openConnection()和closeConnection()成对调用,否则引用计数没有及时更新还是可能会导致数据库连接资源泄漏的情况。

// 数据库连接计数引用使用
public void save(Student student) {
	try {
		SQLiteDatabase db = DBReferenceManager.getInstance().openConnection();
		db.execSQL(INSERT, new Object[] { student.getId(), student.getName(), 
			student.getPhone(), student.getAddress(), student.getAge()});
	} finally {
		DBReferenceManager.getInstance().closeConnection(); // 关闭连接,减少计数引用
	}
}

public void update(Student student) {
	try {
		SQLiteDatabase db = DBReferenceManager.getInstance().openConnection();
		db.execSQL(UPDATE, new Object[] {  student.getName(), student.getPhone(), 
			student.getAddress(), student.getAge(), student.getId()});
	} finally {
		DBReferenceManager.getInstance().closeConnection();// 关闭连接,减少计数引用
	}
}

测试前面封装的数据库连接,在大量线程并发的情况下数据库的插入和更新操作没有在抛出数据库被锁定的异常,可见上面的单例封装和计数引用封装是成功的。

SQLite数据访问封装

数据库的创建对象和单例模式保存数据库连接都是固定步骤,不同的Android应用相差其实并不是很大。接着就需要创建数据库表对应的实体类(Entity)和数据访问对象(Data Access Object, DAO),实体类就是普通的JavaBean对象,Dao对象内部需要操作数据表完成插查改删(Create Read Update Delete, CRUD)四种操作。

// Student数据操作封装
public class StudentDao {
// 去掉了删除和加载操作
private static final String INSERT = "INSERT INTO student(id, name, phone, address, age) VALUES(?, ?, ?, ?, ?)";
private static final String UPDATE = "UPDATE student SET name = ?, phone = ?, address = ?, age = ? WHERE id = ?";
private static final String QUERY_LIST = "SELECT * FROM student WHERE age < ?";

private SQLiteDatabase mDb;

public StudentDao(SQLiteDatabase db) {
this.mDb = db;
}
// 创建数据库表的操作是静态方法
public static void createTable(SQLiteDatabase db) {
db.execSQL("create table student(id integer primary key, name varchar(255), phone varchar(128), address varchar(255), age integer);");
}

public void save(Student student) {
mDb.execSQL(INSERT, new Object[] { student.getId(), 
student.getName(), student.getPhone(),
 student.getAddress(), student.getAge()});
}

public void update(Student student) {
mDb.execSQL(UPDATE, new Object[] { student.getName(), 
student.getPhone(), student.getAddress(), 
student.getAge(), student.getId()});
}

public List<Student> queryByAge(int age) {
Cursor cursor = mDb.rawQuery(QUERY_LIST, new String[] { String.valueOf(age) });
List<Student> list = new ArrayList<>();
while (cursor.moveToNext()) {
Student student = getStudent(cursor);
list.add(student);
} // 省略Cursor关闭代码
return list;
}
    // 将Cursor数据库数据转换成Java对象
private Student getStudent(Cursor cursor) {
Student student = new Student();
student.setId(cursor.getInt(cursor.getColumnIndex("id")));
student.setAddress(cursor.getString(cursor.getColumnIndex("address")));
student.setName(cursor.getString(cursor.getColumnIndex("name")));
student.setPhone(cursor.getString(cursor.getColumnIndex("phone")));
student.setAge(cursor.getInt(cursor.getColumnIndex("age")));
return student;
}
}

上面的代码展示了封装基本的查删改查的数据访问对象(Data Access Object, Dao), 在Dao查找实现中会返回Cursor对象,它类似于迭代器对象,可以向前向后迭代,它的内部会持有查询到的数据内存资源,使用完成后需要立即关闭否则会发生内存泄漏。Dao对象内部的逻辑基本上就是定义SQL执行语句,绑定SQL参数值,最后执行并获取结果。
通过前面简单的封装已经可以在Android应用中做数据库的CRUD操作,后面增加Teacher老师和Course学科表数据,只要创建好它们的实体类,将StudentDao数据访问类拷贝一下修改方法中的表名和字段名,就在数据库中增加了新的对象CRUD功能。现在重新审视上面的StudentDao里的代码,特别是getStudent()方法,当增加TeacherDao时需要创建Teacher对象,接着读取Teacher表里的数据逐一填充到Teacher对象里,上面的save()和update()方法也都是修改了字段名和属性名之间的对应关系。

// Cursor转换成Teacher对象
private Teacher getTeacher(Cursor cursor) {
Teacher teacher = new Teacher();
teacher.setId(cursor.getInt(cursor.getColumnIndex("id")));
teacher.setCourse(cursor.getString(cursor.getColumnIndex("course")));
teacher.setName(cursor.getString(cursor.getColumnIndex("name")));
teacher.setTitle(cursor.getString(cursor.getColumnIndex("title")));
teacher.setGender(cursor.getString(cursor.getColumnIndex("gender")));
return teacher;
}

样板代码

示例代码getStudent()和getTeacher()执行的逻辑类似但是具体细节又有所不同,这样的代码一般都被称作样板代码。样板代码和相同代码并不一样,相同代码指的是代码的内容完全相同,但是出现在多个地方,相同代码需要做成一个方法供使用它的地方调用;样板代码在逻辑上具有一致性,但在具体的细节又不一样,比如代码2-8的成绩数据处理,要计算及格的分数和优秀的分数都是遍历并比较分数,再把符合要求的分数加入到对应的列表中,循环判断是逻辑类似,判断条件则是细节不同。

// 循环样板代码
List<Integer> scores = Arrays.asList(100, 50, 79, 78, 99, 60, 85, 66, 89, 92, 43);
// 过滤的循环样板代码
List<Integer> passed = new ArrayList<>();
List<Integer> good = new ArrayList<>();
for (int i = 0, size = scores.size(); i < size; i++) {
int score = scores.get(i);
if (score >= 60) { // 过滤及格的分数
passed.add(score);
}
}
for (int i = 0, size = scores.size(); i < size; i++) {
int score = scores.get(i);
if (score >= 80) { // 过滤优秀的分数
good.add(score);
}
}

// 过滤的流封装代码
passed = scores.stream().filter(score -> score >= 60).collect(Collectors.toList());
good = scores.stream().filter(score -> score >= 80).collect(Collectors.toList());

Java Stream流操作就对过滤样板代码做了封装,只需要传入过滤的判断条件就可以获取到筛选结果,可以看出流操作代码更加简单明了。样板代码通常都需要提取共同操作,封装不同的实现逻辑,那么前面提到的StudentDao和TeacherDao数据访问它们的不同实现逻辑就是数据库表字段和实体类属性的对应关系,想要封装就需要使用到Java的反射机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
一.创建一个DataBaseHelper DataBaseHelper是一个访问SQLite的助类,提供两个方面的功能 1.getReadableDatebase(),getWriteableDatabase()可以获取SQLiteDatabase对象,通过 2.提供了onCreate()和onUpdate()两个回调函数,允许我们常见和升级数据库是进行使用 A、 在SQLiteOpenHelper的子类当中,必须要有的构造函数 B、该函数是在第一次创建数据库的时候执行,实际上是在第一次得到SQLiteDataBase对象的时候onCreate 二、创建一个实体person类并且给字段和封装 三、创建一个业务类对SQL的CRUD操作 1.getWritableDatabase()和getReadableDatabase()的区别 ,两个方法都可以获取一个用于操作数据库SQLiteDatabase实例 2.execSQL(增,删,改都是这个方法)和close();android内部有缓存可关闭也不关闭也行,查询rawQuery是方法 3.在分页有到Cursor(游标)取游标下一个值cursor.moveToNext(),用游标对象接数据 "select * from person limit ?,?" person不能加上where 关键字 4.在删除注意:sb.deleteCharAt(sb.length() - 1); 四、AndroidCRUD业务对SQLite的CRUD操作 1.ContentValues对象的使用 2.android内部insert添加数据的方法,而且values这个不给值也必须要执行,而主键是不是null的其他字段的值是为null 3.insert update query delete 五、单元测试类要注意的 AndroidCRUDService curdService = new AndroidCRUDService(this.getContext()); /* * 注意:getContext必须在我们使用前已经注解进去的,在使用前要实力化,而且是使用后才有上下文 *一般设置为局部对象 */ 六、AndroidManifest.xml的配置 <!-- 配置用户类库android.test.runner测试 --> package jll.sqlitedb; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.database.sqlite.SQLiteDatabase.CursorFactory; /** * *@author Administrator DataBaseHelper是一个访问SQLite的助类,提供两个方面的功能 * 1.getReadableDatebase(),getWriteableDatabase()可以获取SQLiteDatabase对象,通过 * 2.提供了onCreate()和onUpdate()两个回调函数,允许我们常见和升级数据库是进行使用 */ public class DataBaseHelper extends SQLiteOpenHelper { // 给一个默认的SQLite数据库名 private static final String DataBaseName = "SQLite_DB"; private static final int VERSION = 2; // 在SQLiteOpenHelper的子类当中,必须要有的构造函数 public DataBaseHelper(Context context, String name, CursorFacto
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值