SQLite数据库的使用

SQLite数据库的使用

app通常需要保存一些有用的数据在本地,如果数据量小,比如app的一些配置信息,可以考虑使用轻量级的SharedPreferences来保存。如果数据量大,且要进行复杂的操作,则可能需要使用数据库来保存数据,而SQLite是Android系统默认支持的一款数据库。

业务描述:这篇文章里我们试图来完成这样一个业务,app支持多账号切换,且每个账号都会产生一些训练(比如跳绳)数据,当app没有联网时,把训练数据暂时保存到本地,当连上网络时,就上传训练数据然后清空本地训练数据,这样即使没有网络,也不至于造成训练数据的丢失,且可保证app正常使用。这个业务使用SharedPreferences来保存数据显然不太合适,因为每个账号都会产生数据,且数据量可能比较大,而且要把账号资料和训练数据对应起来,使用关系型数据库最合适不过了。
业务分析:根据以上业务,可以知道这个数据库里至少需要两个表,一个是用户表,一个是训练表。
表结构设计:
- 用户表:用户id,用户昵称、性别 等等
- 训练表: 用户id, 训练时间,训练个数 等等

根据以上分析,就可以着手建立数据库了。

1. 基础使用

1.1 SQLiteOpenHelper

SQLiteOpenHelper是一个用来创建数据库,及对数据库进行版本管理的类。
我们首先要做的就是自定义一个类来继承它,然后实现它的构造方法,数据库创建方法,及版本升级方法。如下:

public class MyDataBaseHelper extends SQLiteOpenHelper {
    private static final String TAG = "MyDataBaseHelper";

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


    public MyDataBaseHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        Log.e(TAG, "onCreate SQLite");
        sqLiteDatabase.execSQL(UserTable.SQL_CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        Log.e(TAG, "onUpgrade SQLite -->> oldVersion = " + oldVersion + " ; newVersion = " + newVersion);
    }

}

这里我们定义了一个MyDataBaseHelper 类来继承SQLiteOpenHelper类,并在这个类里定义了数据库的名称 DB_NAME ,定义了数据库版本号 DB_VERSION ,实现了构造方法,并重写了数据库的创建方法 onCreate 和版本升级方法 onUpgrade。数据表生成语句就在在 onCreate 方法里执行。这里只生成了一个用户表,训练表等后面版本升级的时候再添加,以模拟版本升级时添加数据表的操作。

1.2 定义数据及数据表

我们把数据表的定义全放在同一个类里,以方便管理。如下:

public final class TablesContract {

    private TablesContract(){
        //防止被初始化
    }

    public static class UserTable implements BaseColumns{
        //user表名 字段名 及 建表语句
        public static final String TABLE_NAME = "table_user";
        public static final String COLUMN_ID = "user_id";
        public static final String COLUMN_NAME = "user_name";
        public static final String COLUMN_GENDER = "user_gender";
        public static final String COLUMN_AGE = "user_age";
        public static final String SQL_CREATE_TABLE = "create table if not exists " + TABLE_NAME + " ( "
                + COLUMN_ID + " text not null,"
                + COLUMN_NAME + " text not null,"
                + COLUMN_GENDER + " integer,"
                + COLUMN_AGE + " integer)";
    }
}

自定义了一个 TablesContract 类,在里面定义了 用户资料表的 表名字段名SQL建表语句,目前用户表的字段有:用户id、用户名、性别、年龄 。如果数据库还需要添加其它表,也可以同样的方式定义在这个类里。
同时,对应用户表的字段定义一个用户对象类,如下:

public class UserBean {
    private String id;
    private String name;
    private int gender;
    private int age;

    public UserBean(String id, String name, int gender, int age) {
        this.id = id;
        this.name = name;
        this.gender = gender;
        this.age = age;
    }

    public String getId() {
        return id;
    }

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

    ... ...

    @Override
    public String toString() {
        return "UserBean{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                ", gender=" + gender +
                ", age=" + age +
                '}';
    }
}

示例代码省略了一些get、set方法,就不全部放上来了。

1.3 定义一个数据库操作类

定义这个类,主要是为了把数据库与上层应用分离开,使用的时候不必考虑数据库操作详情,只需要调用相应的方法就可以存取、删改数据。
如下:

public class MyDatabaseOperator {
    private static SQLiteOpenHelper openHelper;

    public MyDatabaseOperator(Context context){
        openHelper = new MyDataBaseHelper(context);
    }

    //保存用户资料
    public boolean saveUser(UserBean userBean){
        SQLiteDatabase database = openHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(UserTable.COLUMN_ID, userBean.getId());
        values.put(UserTable.COLUMN_NAME, userBean.getName());
        values.put(UserTable.COLUMN_GENDER, userBean.getGender());
        values.put(UserTable.COLUMN_AGE, userBean.getAge());
        long result = database.insert(UserTable.TABLE_NAME, null, values);
        database.close();
        if(result == -1){
            return false;
        }else {
            return true;
        }
    }

    //获取全部用户资料
    public List<UserBean> getUserList(){
        SQLiteDatabase database = openHelper.getReadableDatabase();
        final String querySQL = "select * from " + UserTable.TABLE_NAME;
        Cursor cursor = database.rawQuery(querySQL, null);
        List<UserBean> userList = new ArrayList<>();
        if(cursor != null && cursor.getCount() > 0){
            while (cursor.moveToNext()){
                UserBean userBean = new UserBean(
                        cursor.getString(cursor.getColumnIndex(UserTable.COLUMN_ID)),
                        cursor.getString(cursor.getColumnIndex(UserTable.COLUMN_NAME)),
                        cursor.getInt(cursor.getColumnIndex(UserTable.COLUMN_GENDER)),
                        cursor.getInt(cursor.getColumnIndex(UserTable.COLUMN_AGE))
                );
                userList.add(userBean);
            }
        }
        cursor.close();
        database.close();
        return userList;
    }

}

在这个类里定义了两个方法,一个用来保存用户资料,一个用来取出所有用户资料。这里需要说明一个问题,在这两个方法的结尾,都有关闭数据库的操作。但是如果有多个线程同时操作数据库,当一个线程关闭了数据库,另一个线程还在操作时,就会导致bug。因此如果有多个线程频繁使用数据库,是不建议这样直接关闭的。但是我们这里只需要简单的操作,这样写并没有问题,至于多线程操作数据库的情况,我们后面再讨论。

1.4 在Activity里使用数据库

    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tvDatabase = findViewById(R.id.tv_database);
        Button btnScan = findViewById(R.id.btn_scan_database);
        btnScan.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                scanDatabase();
            }
        });

        Button btnSave = findViewById(R.id.btn_save_database);
        btnSave.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                saveData();
            }
        });
    }

    private void scanDatabase(){
        new ScanDataTask().execute();
    }


    private void saveData(){
        new Thread(new Runnable() {
            @Override
            public void run() {
                MyDatabaseOperator operator = new MyDatabaseOperator(getApplicationContext());
                boolean result = operator.saveUser(new UserBean("123", "chenrenxiang", 1, 26));
                if(result){
                    runOnUiThread(new Runnable() {
                        @Override
                        public void run() {
                            Toast.makeText(MainActivity.this, "data saved successful", Toast.LENGTH_SHORT).show();
                        }
                    });
                }
            }
        }).start();

    }

    private class ScanDataTask extends AsyncTask<Void, Void, List<UserBean>>{
        @Override
        protected List<UserBean> doInBackground(Void... voids) {
            MyDatabaseOperator operator = new MyDatabaseOperator(getApplicationContext());
            return operator.getUserList();
        }

        @Override
        protected void onPostExecute(List<UserBean> userBeans) {
            String result = "";
            for(UserBean userBean : userBeans){
                result += userBean.toString() + "\n";
            }
            tvDatabase.setText(result);
        }
    }

在Activity里模拟了插入一条用户资料和取出用户资料的操作。数据库操作是耗时操作,最好写在子线程里。
插入之后,使用Stetho查看新建的数据库、数据表,及添加的用户资料。可以看到,数据已经成功插入了数据表。
这里写图片描述


2. 数据库升级

在上面的操作中,我们建立了一个数据库,并创建了一个 用户表,定义了一些基本的操作。现在,随着app的升级,我们需要更新数据库。比如,我们需要给用户表添加一个字段,以存储用户的头像url地址,然后要新加一个 训练表 ,以存储用户的训练数据。

2.1 修改已有数据表

首先,我们给用户对象类 UserBean 添加一个 avatar 字段,对 UserBean 类作一些相应的修改。

    private String id;
    private String name;
    private int gender;
    private int age;
    private String avatar;  //新加字段

    public UserBean(String id, String name, int gender, int age, String avatar) {
        this.id = id;
        this.name = name;
        this.gender = gender;
        this.age = age;
        this.avatar = avatar;
    }

    public String getAvatar() {
        return avatar;
    }

    public void setAvatar(String avatar) {
        this.avatar = avatar;
    }

然后在 TablesContract 类中把 avatar 这个字段添加给用户表,建表语句也不要忘了修改

    public static class UserTable implements BaseColumns{
        //user表名 字段名 及 建表语句
        public static final String TABLE_NAME = "table_user";
        public static final String COLUMN_ID = "user_id";
        public static final String COLUMN_NAME = "user_name";
        public static final String COLUMN_GENDER = "user_gender";
        public static final String COLUMN_AGE = "user_age";
        public static final String COLUMN_AVATAR = "user_avatar"; //新加字段
        public static final String SQL_CREATE_TABLE = "create table if not exists " + TABLE_NAME + " ( "
                + COLUMN_ID + " text not null,"
                + COLUMN_NAME + " text not null,"
                + COLUMN_GENDER + " integer,"
                + COLUMN_AVATAR + " text,"
                + COLUMN_AGE + " integer)";
    }

2.2 新增数据表

首先新建一个训练对象类 TrainBean,如下:

public class TrainBean {
    private String id;      //用户id
    private long time;     //训练时间,用时间戳记录  
    private int duration;  //训练时长,以秒为单位记录 
    private int jumps;     //跳绳个数

    public TrainBean(String id, long time, int duration, int jumps) {
        this.id = id;
        this.time = time;
        this.duration = duration;
        this.jumps = jumps;
    }

    ... ... 省略了getset方法和toString方法

 }

然后在 TablesContract 类中定义训练表 TrainTable, 如下:

    public static class TrainTable implements BaseColumns{
        //表名
        public static final String TABLE_NAME = "table_train";
        //字段名
        public static final String COLUMN_ID = "user_id";
        public static final String COLUMN_TIME = "train_time";
        public static final String COLUMN_DURATION = "train_duration";
        public static final String COLUMN_JUMPS = "train_jumps";
        //建表语句
        public static final String SQL_CREATE_TRAIN_TABLE = "create table if not exists " + TABLE_NAME + " ( "
                + COLUMN_ID + " text not null,"
                + COLUMN_TIME + " long not null,"
                + COLUMN_DURATION + " integer,"
                + COLUMN_JUMPS + " integer)";
    }

2.3 数据库升级管理

数据库升级有两种情况:
1. 用户没有装过以前版本的app,或者卸载重装新版本的app
2. 用户手机里已经有以前版本的app,现在覆盖安装新版本app

对于情况1,需要新建数据库,程序会进入到 SQLiteOpenHelper 类里面的 onCreate 方法,不会进入 onUpgrade 方法。 而情况2,需要升级数据库,程序会进入 onUpgrade 方法,不会进入onCreate 方法。

分析完这两种情况,就可以开始对 MyDataBaseHelper 类行进修改了:

    private static final int DB_VERSION = 1;
    private static final int DB_VERSION_2 = 2;

    private static final String DB_NAME = "mydb.db";


    public MyDataBaseHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION_2);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        Log.e(TAG, "onCreate SQLite");
        sqLiteDatabase.execSQL(UserTable.SQL_CREATE_USER_TABLE);
        //新增训练表
        sqLiteDatabase.execSQL(TrainTable.SQL_CREATE_TRAIN_TABLE); 
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        Log.e(TAG, "onUpgrade SQLite -->> oldVersion = " + oldVersion + " ; newVersion = " + newVersion);
        //如果是从版本1升级到版本2,先向用户表添加avatar字段,然后新建训练表
        if(oldVersion == DB_VERSION && newVersion == DB_VERSION_2){
            //向用户表插入avatar字段的SQL语句
            String upgradeUserSQL = "alter table " + UserTable.TABLE_NAME + " add " + UserTable.COLUMN_AVATAR + " text";
            sqLiteDatabase.execSQL(upgradeUserSQL);
            sqLiteDatabase.execSQL(TrainTable.SQL_CREATE_TRAIN_TABLE);
        }
    }
  1. 数据库升级的时候,新的版本号必须比之前的版本号大,这里我没有直接把 DB_VERSION 改大,而是新建了一个 DB_VERSION_2 来保存新的版本号,主要是为了保存以前的版本号。因为随着数据库的升级,会出现各种覆盖安装的情况。比如说可能重版本1升到版本4,也可能从版本2升到版本4,而不同的升级情况数据库变动的内容通常也是不同的,需要进行判断然后分别处理。
  2. 以上是考虑到需要保存以前版本的本地数据,如果以前版本的本地数据不重要,那么可以直接在 onUpgrade 方法里删除以前的数据表,再调用 onCreate 方法新建数据表就行。如下:
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        Log.e(TAG, "onUpgrade SQLite -->> oldVersion = " + oldVersion + " ; newVersion = " + newVersion);
        String deleteTableSQL = "drop table " + UserTable.TABLE_NAME;
        sqLiteDatabase.execSQL(deleteTableSQL);
        onCreate(sqLiteDatabase);
    }

2.4 测试升级

在数据库操作类 MyDatabaseOperator 中添加一个向训练表 TrainTable 插入一条训练数据的方法,在Activity中调用此方法向训练表插入一条数据,使用Stetho查看数据是否插入成功。

    //插入训练数据
    public boolean saveTrainData(TrainBean trainBean){
        SQLiteDatabase database = openHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(TrainTable.COLUMN_ID, trainBean.getId());
        values.put(TrainTable.COLUMN_TIME, trainBean.getTime());
        values.put(TrainTable.COLUMN_DURATION, trainBean.getDuration());
        values.put(TrainTable.COLUMN_JUMPS, trainBean.getJumps());
        long result = database.insert(TrainTable.TABLE_NAME, null, values);
        database.close();
        if(result == -1){
            return false;
        }else {
            return true;
        }
    }

1. 测试覆盖升级: 可以看到程序只进入了onUpgrage方法,如下:
这里写图片描述
插入完成后查看数据库,可以看到新建的训练表已经存在,且用户表新增了avatar字段:
这里写图片描述

2. 测试重新安装: 可以看到程序只进入了 onCreate方法,如下:
这里写图片描述


3. 数据库的多线程操作

前面提到过当多个线程同时操作数据库时可能会导致bug,下面我们来模拟一下多线程操作数据库,当一个线程工作完成之后关闭了数据库,另一个线程仍然在运行的情况。

首先我们把 保存训练数据 的方法稍微改动一下,让它接受一个count参数,然后重复插入count条数据。

    //插入训练数据
    public boolean saveTrainData(TrainBean trainBean, int count){
        SQLiteDatabase database = openHelper.getWritableDatabase();
        ContentValues values = new ContentValues();
        long result = 0;
        for(int i=0; i<count; i++){
            values.put(TrainTable.COLUMN_ID, trainBean.getId());
            values.put(TrainTable.COLUMN_TIME, trainBean.getTime());
            values.put(TrainTable.COLUMN_DURATION, trainBean.getDuration());
            values.put(TrainTable.COLUMN_JUMPS, trainBean.getJumps());
            result = database.insert(TrainTable.TABLE_NAME, null, values);
        }
        database.close();
        if(result == -1){
            return false;
        }else {
            return true;
        }
    }

然后把Activity里保存训练数据的方法也稍微改一下。当点击按钮时,同时开两个线程保存数据,一个线程保存2条,一个线程保存10条,同时把保存的结果打印出来。

    ... ...
        btnSave.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                saveData(2);
                saveData(10);
            }
        });
    }

    private void saveData(final int count){
        new Thread(new Runnable() {
            @Override
            public void run() {
                MyDatabaseOperator operator = new MyDatabaseOperator(getApplicationContext());
                boolean result = operator.saveTrainData(new TrainBean("123", 123454, 60, 144), count);
//                boolean result = operator.saveUser(new UserBean("123", "chenrenxiang", 1, 26));
                if(result){
                    Log.e(TAG, "save" + count + "data successful");
                }
            }
        }).start();

    }

运行程序,点击按钮后立即奔溃,log如下:

com.xiaoqiang.sqlitelearn E/MainActivity: save 2 data successful
com.xiaoqiang.sqlitelearn E/AndroidRuntime: FATAL EXCEPTION: Thread-4213
    Process: com.xiaoqiang.sqlitelearn, PID: 18405
    java.lang.IllegalStateException: attempt to re-open an already-closed object: SQLiteDatabase: /data/data/com.xiaoqiang.sqlitelearn/databases/mydb.db
    at android.database.sqlite.SQLiteClosable.acquireReference(SQLiteClosable.java:55)
    at android.database.sqlite.SQLiteDatabase.insertWithOnConflict(SQLiteDatabase.java:1439)
    at android.database.sqlite.SQLiteDatabase.insert(SQLiteDatabase.java:1341)
    at com.xiaoqiang.sqlitelearn.sqlite.MyDatabaseOperator.saveTrainData(MyDatabaseOperator.java:76)
    at com.xiaoqiang.sqlitelearn.MainActivity$3.run(MainActivity.java:52)
    at java.lang.Thread.run(Thread.java:818)

可以看到,保存2条数据的线程运行完成,并打印出了 “save 2 data successful”,而保存10条数据的线程出错,原因是 “attempt to re-open an already-closed object: SQLiteDatabase: /data/data/com.xiaoqiang.sqlitelearn/databases/mydb.db” ,即试图重新打开已经关闭的数据库。

3.1 多线程操作的可行方案

那么如何解决这个bug呢,目前我看到的最佳解决方案是这篇博客:Concurrent database access
写得很清楚,很详细,我就不复述了。

接下来我要做的就是参考这篇博客,修复当前的bug。

  1. 首先新建 DatabaseManager 类,完全复制它的代码。
public class DatabaseManager {

    private AtomicInteger mOpenCounter = new AtomicInteger();

    private static DatabaseManager instance;
    private static SQLiteOpenHelper mDatabaseHelper;
    private SQLiteDatabase mDatabase;

    public static synchronized void initializeInstance(SQLiteOpenHelper helper) {
        if (instance == null) {
            instance = new DatabaseManager();
            mDatabaseHelper = helper;
        }
    }

    public static synchronized DatabaseManager getInstance() {
        if (instance == null) {
            throw new IllegalStateException(DatabaseManager.class.getSimpleName() +
                    " is not initialized, call initializeInstance(..) 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();

        }
    }
}
  1. 在Application中初始化 DatabaseManager ,并传入自定义的 DatbaseOpenHelper。
    private void initDatabase(){
        DatabaseManager.initializeInstance(new MyDataBaseHelper(this));
    }
  1. 修改数据库操作类 MyDatabaseOperator
public class MyDatabaseOperator {
    private static DatabaseManager manager;

    public MyDatabaseOperator(){
        manager = DatabaseManager.getInstance();
    }

    ... ...

    //插入训练数据
    public boolean saveTrainData(TrainBean trainBean, int count){
        SQLiteDatabase database = manager.openDatabase();
        ContentValues values = new ContentValues();
        long result = 0;
        for(int i=0; i<count; i++){
            values.put(TrainTable.COLUMN_ID, trainBean.getId());
            values.put(TrainTable.COLUMN_TIME, trainBean.getTime());
            values.put(TrainTable.COLUMN_DURATION, trainBean.getDuration());
            values.put(TrainTable.COLUMN_JUMPS, trainBean.getJumps());
            result = database.insert(TrainTable.TABLE_NAME, null, values);
        }
        manager.closeDatabase();
        if(result == -1){
            return false;
        }else {
            return true;
        }
    }


}

获取数据库使用 DatabaseManager.openDatabase() 方法,关闭数据库用DatabaseManager.closeDatabase() 方法。

修改完成之后运行程序,结果如下:

com.xiaoqiang.sqlitelearn E/MainActivity: save 2 data successful
com.xiaoqiang.sqlitelearn E/MainActivity: save 10 data successful

两次操作都运行成功!

放一下demo地址

难免bug,欢迎讨论指正。以上。

猜你喜欢

转载自blog.csdn.net/chenrenxiang/article/details/78771377