欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Android 数据库操作之:SQLiteOpenHelper 与 SQLiteDatabase(整理)

程序员文章站 2022-03-15 10:27:15
一、基本使用二、使用中要注意的问题三、一些原理参考文章:...

一、基本使用

1、SQLiteOpenHelper
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;

public class NumbersDBHelper extends SQLiteOpenHelper {

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

    interface TABLE {
        String NAME = "Numbers";
        String ID = "id";
        String NUMNAME = "numName";
        String DESCRIPTION = "description";
    }

    NumbersDBHelper(@Nullable Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    /**
     * 负责数据库的创建和初始化,只在第一次生成数据库的时候回调
     * <p>
     * TODO 只有第一次真正使用数据库时才会调用:
     * android.database.sqlite.SQLiteOpenHelper#getWritableDatabase()
     * android.database.sqlite.SQLiteOpenHelper#getReadableDatabase()
     * <p>
     * 文件路径:
     * data/data/[应用包名xxx.xxx.xxx]/databases/dbTest.db
     */
    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql = String.format(
                "create table if not exists %s (%s integer primary key, %s text, %s text)",
                TABLE.NAME, TABLE.ID, TABLE.NUMNAME, TABLE.DESCRIPTION);
        db.execSQL(sql);
    }

    /**
     * 数据库升级的时候才会回调
     */
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }

    /**
     * 每次成功打开数据库后首先被执行
     */
    @Override
    public void onOpen(SQLiteDatabase db) {
        super.onOpen(db);
    }
}
2、通过 Dao 来规范数据的操作(SQLiteDatabase)
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import androidx.annotation.Nullable;
import java.util.List;

public class NumbersDao extends BaseDao<NumbersDBHelper> {

    private static volatile NumbersDao mNumbersDao;

    static NumbersDao getInstance(@Nullable Context context) {
        if (mNumbersDao == null) {
            synchronized (NumbersDao.class) {
                if (mNumbersDao == null) {
                    mNumbersDao = new NumbersDao(context);
                }
            }
        }
        return mNumbersDao;
    }

    private NumbersDao(@Nullable Context context) {
        super(context);
    }

    @Override
    protected NumbersDBHelper initDbHelper(Context context) {
        return new NumbersDBHelper(context);
    }

    public boolean insert(NumberBean number) {
        boolean isSuccess = false;
        SQLiteDatabase writableDatabase = getWritableDatabase();
        try {
            ContentValues values = new ContentValues();
            values.put(NumbersDBHelper.TABLE.NUMNAME, number.getNumName());
            values.put(NumbersDBHelper.TABLE.DESCRIPTION, number.getDescription());
            writableDatabase.insertOrThrow(NumbersDBHelper.TABLE.NAME, null, values);
            isSuccess = true;
        } catch (SQLException e) {
            e.printStackTrace();
        }
        closeWritableDatabase(writableDatabase);
        return isSuccess;
    }

    public boolean insert(List<NumberBean> numbers) {
        boolean isSuccess = false;
        SQLiteDatabase writableDatabase = getWritableDatabase();
        // TODO 手动设置开始事务
        writableDatabase.beginTransaction();
        try {
            for (NumberBean number : numbers) {
                ContentValues values = new ContentValues();
                values.put(NumbersDBHelper.TABLE.NUMNAME, number.getNumName());
                values.put(NumbersDBHelper.TABLE.DESCRIPTION, number.getDescription());
                writableDatabase.insertOrThrow(NumbersDBHelper.TABLE.NAME, null, values);
            }
            // TODO 设置事务处理成功,不设置会自动回滚不提交
            writableDatabase.setTransactionSuccessful();
            isSuccess = true;
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // TODO 只有执行了endTransaction方法,事务操作才会真正提交到数据库
            writableDatabase.endTransaction();
            // 关闭数据库连接
            // writableDatabase.close();
            closeWritableDatabase(writableDatabase);
        }
        return isSuccess;
    }

    public boolean deleteById(int id) {
        boolean isSuccess = false;
        SQLiteDatabase writableDatabase = getWritableDatabase();
        try {
            writableDatabase.delete(
                    NumbersDBHelper.TABLE.NAME,// 表名
                    String.format("%s=%s", NumbersDBHelper.TABLE.ID, id),// 条件
                    null // 上面条件的占位符( You may include ?s in the where clause)(可以,也可以不用)
            );
            isSuccess = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        closeWritableDatabase(writableDatabase);
        return isSuccess;
    }

    public boolean updateById(int id, NumberBean number) {
        if (number == null) {
            return false;
        }
        boolean isSuccess = false;
        SQLiteDatabase writableDatabase = getWritableDatabase();
        try {
            ContentValues values = new ContentValues();
            values.put(NumbersDBHelper.TABLE.NUMNAME, number.getNumName());
            values.put(NumbersDBHelper.TABLE.DESCRIPTION, number.getDescription());
            writableDatabase.update(
                    NumbersDBHelper.TABLE.NAME,// 表名
                    values,
                    String.format("%s=%s", NumbersDBHelper.TABLE.ID, id),// 条件
                    null // 上面条件的占位符( You may include ?s in the where clause)(可以,也可以不用)
            );
            isSuccess = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        closeWritableDatabase(writableDatabase);
        return isSuccess;
    }

    public NumberBean queryById(int id) {
        SQLiteDatabase readableDatabase = getReadableDatabase();
        NumberBean number = null;
        try {
            // SELECT * FROM Numbers WHERE id=9
            Cursor cursor = readableDatabase.query(
                    NumbersDBHelper.TABLE.NAME,// 表名
                    null, // 返回的字段,null表示返回全部字段
                    String.format("%s=%s", NumbersDBHelper.TABLE.ID, id), // 查询条件
                    null, // 查询条件若带有问号,这里既是问号所包括数据数组,没有即为null
                    null, // 分组的字段
                    null, // having
                    null // 排序方式
            );
            number = new NumberBean();
            // Move the cursor to the first row.This method will return false if the cursor is empty.
            if (cursor.moveToFirst()) {
                number.setId(cursor.getInt(cursor.getColumnIndex(NumbersDBHelper.TABLE.ID)));
                number.setNumName(cursor.getString(cursor.getColumnIndex(NumbersDBHelper.TABLE.NUMNAME)));
                number.setDescription(cursor.getString(cursor.getColumnIndex(NumbersDBHelper.TABLE.DESCRIPTION)));
            }
            // TODO 关闭cursor,回收资源
            cursor.close();
            closeReadableDatabase(readableDatabase);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return number;
    }
}

Dao 基类

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import androidx.annotation.Nullable;

abstract class BaseDao<T extends SQLiteOpenHelper> {

    private final T mDbHelper;
    private int mOpenCounts;
    private SQLiteDatabase mWritableDatabase;

    BaseDao(@Nullable Context context) {
        this.mDbHelper = initDbHelper(context);
    }

    abstract T initDbHelper(Context context);

    synchronized SQLiteDatabase getWritableDatabase() {
        if (mOpenCounts == 0) {
            mWritableDatabase = mDbHelper.getWritableDatabase();
        }
        mOpenCounts++;
        return mWritableDatabase;
    }

    synchronized void closeWritableDatabase(SQLiteDatabase db) {
        mOpenCounts--;
        if (mOpenCounts <= 0) {
            db.close();
        }
    }

    SQLiteDatabase getReadableDatabase() {
        return mDbHelper.getReadableDatabase();
    }

    void closeReadableDatabase(SQLiteDatabase db) {
        db.close();
    }
}
3、其他

Manager 规范使用入口

import android.content.Context;
import androidx.annotation.Nullable;

public class DaoManager {

    public static NumbersDao getNumbersDao(@Nullable Context context) {
        return NumbersDao.getInstance(context.getApplicationContext());
    }

}

Bean 类

public class NumberBean {

    private int id;
    private String numName;
    private String description;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getNumName() {
        return numName;
    }

    public void setNumName(String numName) {
        this.numName = numName;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }
}

二、使用中要注意的问题

1、SQLiteDatabaseLockedException: database is locked

SQLite 是文件级别的锁,多个线程可以同时读,但是同时只能有一个线程写。Android 提供了 SqliteOpenHelper 类来操作数据库,一个 SqliteOpenHelper 绑定一个指定的数据库,可以通过 getWritableDatabase 或者 getReadableDatabase 拿到 SQLiteDatabase 对象,即为数据库连接。当多线程生成多个 SqliteOpenHelper 类的实例,且同时对数据库进行操作时(读/写),就可能出现这个情况。下面模拟一下:

    private void asyncInsert() {
        List<Thread> threads = new ArrayList<>();
        // TODO 因为测试机多核,所以模拟时线程数要超过CPU多核的数量
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    ArrayList<NumberBean> numbers = new ArrayList<>();
                    for (int i = 0; i < 10000; i++) {
                        NumberBean number = new NumberBean();
                        number.setNumName("张" + i);
                        number.setDescription("zhang" + i);
                        numbers.add(number);
                    }
                    DaoManager.getNumbersDao(SQLiteActivity.this).insert(numbers);
                }
            });
            threads.add(thread);
        }
        for (Thread thread : threads) {
            thread.start();
        }
    }

如果数据操作较快,很难重现这个问题(据说读/写操作的阻塞超过5秒钟才会报这个问题),所以上面通过超过 CPU 核数的线程数及较大的数据插入(其实也不大)来达到想要的效果,如下:

E/AndroidRuntime: FATAL EXCEPTION: Thread-24
Process: com.zzq.demo, PID: 17948
android.database.sqlite.SQLiteDatabaseLockedException: database is locked (Sqlite code 5 SQLITE_BUSY), (OS error - 2:No such file or directory)
... ...

这种情况只要保持只有一个 SqliteOpenHelper 对应的数据库连接就行,我的 demo 是通过 dao 类来操作 SqliteOpenHelper,dao 的单例同时实现 SqliteOpenHelper 的单例。

2、attempt to re-open an already-closed object 和 Cannot perform this operation because the connection pool has been closed

同一个 SQLiteOpenHelper 的 getWritableDatabase() 返回同一个 SQLiteDatabase 对象,当多线程情况下使用的是同一个 SqliteOpenHelper 来打开数据库连接时,某个线程在完成数据库操作后关闭数据库连接(database.close())而另一个线程还在进行数据库的操作,就会出现这个问题(关闭数据库连接是为了回收资源):线程 A 打开数据库连接使用数据库,这时 CPU 片段分配给线程 B,线程 A 挂起;线程 B 进入执行获取数据库连接时没有问题,线程 B 进行操作,在片段时间内数据操作完成,最后关闭数据库连接(database.close())线程 B 执行结束;切换到线程 A 执行,这时线程 A 插入数据或者其他操作时就会出现这个情况:

    public boolean insert(List<NumberBean> numbers) {
        ... ...
        SQLiteDatabase writableDatabase = mNumbersDBHelper.getWritableDatabase();
        ... ...
        try {
            ... ...
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            ... ...
            // 关闭数据库连接
            writableDatabase.close();
        }
        return isSuccess;
    }
E/AndroidRuntime: FATAL EXCEPTION: Thread-5
Process: com.zzq.demo, PID: 20599
java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/user/0/com.zzq.demo/databases/dbTest.db
... ...
E/AndroidRuntime: FATAL EXCEPTION: Thread-6
Process: com.zzq.demo, PID: 20599
java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
... ...

通过记录当前的数据库连接打开数量,来决定是否执行关闭操作:

abstract class BaseDao<T extends SQLiteOpenHelper> {

    private final T mDbHelper;
    private int mOpenCounts;
    private SQLiteDatabase mWritableDatabase;

    ... ...

    synchronized SQLiteDatabase getWritableDatabase() {
        if (mOpenCounts == 0) {
            mWritableDatabase = mDbHelper.getWritableDatabase();
        }
        mOpenCounts++;
        return mWritableDatabase;
    }

    synchronized void closeWritableDatabase(SQLiteDatabase db) {
        mOpenCounts--;
        if (mOpenCounts <= 0) {
            db.close();
        }
    }

    ... ...
}
    public boolean insert(List<NumberBean> numbers) {
        ... ...
        SQLiteDatabase writableDatabase = getWritableDatabase();
        ... ...
        try {
            ... ...
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            ... ...
            // 关闭数据库连接
            // writableDatabase.close();
            closeWritableDatabase(writableDatabase);
        }
        return isSuccess;
    }
3、事务

事务是原子性的,要么执行成功,要么执行失败,执行一半失败后会回滚,这样就保证了数据的完整性。
当需要向 SQLite 中插入大批量数据时,单独的使用添加方法会导致应用响应缓慢,因为 SQLite 插入数据的时候默认一条语句就是一个事务,有多少条数据就有多少次磁盘操作,会出现频繁调用数据库的情况,同时,为了保证数据的完整性,避免出现数据缺失等情况,使用事务就显得有极大的好处:采用事务后,批量操作会变成一次性提交操作,速度提升极为明显,且数据的完整性会得到保证。

    public boolean insert(List<NumberBean> numbers) {
        ... ...
        SQLiteDatabase writableDatabase = getWritableDatabase();
        // TODO 手动设置开始事务
        writableDatabase.beginTransaction();
        try {
            ... ...
            // TODO 设置事务处理成功,不设置会自动回滚不提交
            writableDatabase.setTransactionSuccessful();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            // TODO 只有执行了endTransaction方法,事务操作才会真正提交到数据库
            writableDatabase.endTransaction();
            ... ...
        }
        return isSuccess;
    }

使用 SQLiteDatabase 的 beginTransaction() 方法可以开启一个事务,程序执行到 endTransaction() 方法时会检查事务的标志是否为成功,如果程序执行到 endTransaction() 之前调用了 setTransactionSuccessful() 方法设置事务的标志为成功则提交事务,如果没有调用 setTransactionSuccessful() 方法则回滚事务。

(1)插入1000个数据:
① 不采用事务且每次使用完马上 close WritableDatabase:大约 13s
② 不采用事务且每次使用完也不 close WritableDatabase:大约 2s
③ 采用事务(关不关闭数据库连接都无所谓):大约 200ms

    public void transaction() {
        ArrayList<NumberBean> numbers = new ArrayList<>();
        for (int i = 0; i < 1000; i++) {
            NumberBean number = new NumberBean();
            number.setNumName("张" + i);
            number.setDescription("zhang" + i);
            numbers.add(number);
        }
        for (NumberBean number : numbers) {
            DaoManager.getNumbersDao(this).insert(number);
        }
    }

(2)插入10000个数据:
① 不采用事务且每次使用完马上 close WritableDatabase:大约 2m18s
② 不采用事务且每次使用完也不 close WritableDatabase:大约 13s
③ 采用事务(关不关闭数据库连接都无所谓):大约 600ms

    public void transaction() {
        ArrayList<NumberBean> numbers = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            NumberBean number = new NumberBean();
            number.setNumName("张" + i);
            number.setDescription("zhang" + i);
            numbers.add(number);
        }
        for (NumberBean number : numbers) {
            DaoManager.getNumbersDao(this).insert(number);
        }
    }

三、其他

1、通过 Device File Explorer 或者 DDMS 导出的 db 文件打开后,数据并未发生变化:

[随手记系列]SQLite数据库.db | .db-wal | .db-shw到底是个啥,为什么我导出.db里面没数据呢?

SQLite3.7.0 新特性:预写日志(WAL)

WAL机制的原理是:修改并不直接写入到数据库文件中,而是写入到另外一个称为WAL的文件中;如果事务失败,WAL中的记录会被忽略,撤销修改;如果事务成功,它将在随后的某个时间被写回到数据库文件中,提交修改。

如果我想要看数据咋办?
当 .db-shw 和 .db-wal 缓存到一定的大小后,就会写入到 .db 里面去。

如果我现在就要看数据咋办?
打开 Device File Explorer,把 data/data/[应用包名]/databases 文件夹中 3个文件全部 Save As 出来,打开 .db,就可以看到所有数据了。

Android 数据库操作之:SQLiteOpenHelper 与 SQLiteDatabase(整理)


参考文章:
1、《记录:Android SQLite的使用
2、《Android-SQLite常见问题总结
3、《android sqlite数据库并发问题的详细描述和解决方案
4、《Android 中 SQLite 性能优化
5、《Android数据库Sqlite的基本用法及升级策略

本文地址:https://blog.csdn.net/zeqiao/article/details/108808175