likes
comments
collection
share

android room数据库的基础使用

作者站长头像
站长
· 阅读数 20

room数据库的基础使用

最近在工作中要使用到数据库,但是又不能引入greendao框架,没办法只能自己动手写最基本的增删改查的代码。 完成了功能后,实在看不下去各种数据库的操作。 于是想,肯定有更好的解决方法。 想起了之前google很早就提出的room数据库了,然后在官网上看了一下用法,真是惊讶到了,真的很方便快捷。感觉少许的代码就完成了最基础的功能。 此处记录一下room数据库的基本用法与遇到的问题等。

下图是google codelab中的示意图,通过此图可以大概了解room的结构。

android room数据库的基础使用

基本使用

1、加入依赖

// Room
implementation "androidx.room:room-runtime:$room_version"
// kotlin版的注解处理器
kapt "androidx.room:room-compiler:$room_version"
// 对livedata和协程的支持
implementation "androidx.room:room-ktx:$room_version"
// 还有一些其他的库依赖可用,根据需要添加。 

2、申明实体

首先,定义一个javabean,其中字段对应数据库表中的字段。 示例如下

// 使用tableName自定义表名, primariyKeys可以指定联合主键,也可以通过@PrimaryKey注解
// 单独申明某个字段为主键。 
@Entity(tableName="xxx", primaryKeys = ["name", "age"])
public class User {
    public String name;
    public int age;
    @Ignore
    public int gender;
}

然后在类上添加@Entity注解,将该类与room关联起来。该注解中可以定义主键和外键,表名等。

需要注意的是,SQLite中的表名和列名不区分大小写。

针对一些不需要持久化的字段,可以通过@Ignore注解该字段,最后生成的数据库表中就不会有该字段对应的列了。

在官方示例中,使用的是kotlin中的data class。 在实际中可能并不适用,data class中的字段一经赋值便不可改变,但是在业务开发过程中,可能需要频繁更新字段,故可能不适用。在一些获取只读数据的界面可以尝试使用data class。

3、DAO类的申明

// 使用@Dao注解申明对数据库的访问操作
@Dao
interface UserDao {

    @Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
    public User[] loadAllUsersBetweenAges(int minAge, int maxAge);

    @Query("SELECT * FROM user WHERE first_name LIKE :search " +
           "OR last_name LIKE :search")
    public List<User> findUserWithName(String search);

    @Query("SELECT * FROM user WHERE region IN (:regions)")
    public List<User> loadUsersFromRegions(List<String> regions);

    // 其他的可选注解还有@Update @Delete @Insert等。 
		
}

在dao类中,使用注解的形式申明了sql语句,框架最后会生成sql语句对应的操作,而不用我们手动去操作,确实是很方便快捷。

@Insert和@Update语句在插入和更新时如果有冲突,可以在注解中定义冲突解决方式,是替换还是不执行相关sql语句。 其他的一些可选项,在开发时可点进源码看看。

同时,dao类中可以在参数或返回值中使用list来批量操作对象数据。

在开发过程中,遇到了一个问题。 提示参数必须是arg0、arg1的形式,暂时不知道是啥问题。看github上的一些项目和官方资料时,大部分用的是参数名,可能是版本的问题吧。注意即可。

4、创建数据库

// 其中version是数据库版本,如果数据库需要升级则需改变version值。 exportSchema标识是否将数据库创建wyth
@Database(entities = {User.class}, version = 1, exportSchema = false)
public abstract class UserInfoDataBase extends RoomDatabase {
    public static final String DATABASE_NAME = "userInfo.db";
    private static volatile UserInfoDataBase sInstance;

    public static UserInfoDataBase getInstance() {
        if (sInstance == null) {
            synchronized (UserInfoDataBase.class) {
                if (sInstance == null) {
		// context使用的是全局的context
                    sInstance = Room.databaseBuilder(mContext, UserInfoDataBase .class, DATABASE_NAME)
                            // 这个属性是允许多进程环境下操作数据库
                            .enableMultiInstanceInvalidation()
                            .build();
                }
            }
        }
        return sInstance;
    }

		// 定义一个抽象的dao方法
    public abstract UserInfoDao userInfoDao();
}

创建数据库主要是通过@DataBase注解,申明数据库的信息,并实例化Room的过程。

至此,我们就可以通过UserInfoDataBase实例获取dao对象,从而对数据库进行各种读写操作了。

5、与kotlin flow的结合使用

在之前的开发过程中,如果想监听数据库的变化,一般都是用ContentObserver。 但是ContentObserver监听的是URI,这就需要定义一个content provider,比较麻烦。 而room数据库则支持直接对数据库的观察,以flow或livedata的形式返回。 如在dao类中进行如下申明

@Query("SELECT * FROM user_info ORDER BY updateAt DESC")
fun getAllUserInfoFlow(): Flow<List<UserInfo>>

那么返回的是一个flow对象,而flow对象则会持续对数据库进行监听,一旦数据库有更新操作,就会发射相关flow事件。需要注意的是,使用flow时一般使用collect方法接收相关事件,该方法是一个阻塞方法,需要在协程中处理。 如下示例

mMainScope = MainScope()
mMainScope?.launch(Dispatchers.IO) {
    val userInfoFlow =
        UserInfoDataBase.getInstance().userInfoDao()
            .getAllUserInfoFlow()
			
		userInfoFlow.collect {
			// 数据库数据有更新即会执行该方法体。 
			// 隐藏的it参数即为返回数据list
		
			withContext(Dispatchers.Main) {
				// 切换到线程操作
			}
		}
}

使用livedata也可以实现类似效果。

6、结合recyclerview listadapter的使用

使用listadapter来显示数据可以避免使用notifyDataSetChanged方法刷新全部数据(一者方法可能造成界面闪动,二者导致部分item不必要的刷新)。 listadapter会使用Diff工具计算出前后两个list的差别,然后只刷新有变化的item。在实际开发中,可以多尝试使用listadapter。

在实现ListAdapter对象时也要求传入一个DiffUtil.ItemCallback对象,该对象告诉了Adapter对比item的依据。

如下为DiffUtil.ItemCallback的示例

private val DiffCallback = object : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User , newItem: User): Boolean {
        return oldItem.name == newItem.name && oldItem.age == newItem.age
    }

    override fun areContentsTheSame(oldItem: User , newItem: User): Boolean {
        return oldItem.name == newItem.name && oldItem.age == newItem.age
    }
}

从名称上可以很好的看出,第一个方法是比对两个item是否相同的item,而第二个方法比对的是两个item的内容是否相同。 如果内容有变动,那么会触发界面更新的。

在开发中,遇到了界面刷新不如预期的情况时,是不是这两个方法中写的比对逻辑有问题。 特别是有多个字段的情况下。

7、数据库的调试问题

7.1、Android 设备上运行的 JUnit 测试

7.2、使用as提供的Database inspector。在android studio新版本中已经更名叫做App Inspection了。

但是在实际开发过程中发现,业务中如果未显式调用数据库的close方法,数据不会实时写入db数据库文件(进程重启一次可以将数据写入)。

7.3、使用Sqlite3命令行转储数据文件。

7.4、使用python写个小脚本,将db文件导出到excel查看。

8、其他问题

1、数据库实体文件问题,关闭后才会写入数据库db文件中。

当我想将数据库db文件导出来放在navicat或excel等软件中查看时,突然发现db文件里面只有部分数据,但是在data/data/包名/database目录下生成了xxx.db-shm与xxx.db-wal。 于是猜想,这两个文件是临时文件,只有当数据库操作完成时才会写入。 后来查找了一下RoomDataBase对象方法,有一个close方法,尝试着在业务完成时调用一下,最后发现数据确实都会写入db文件,且不会产生这两个临时文件了。

但是在业务中这样处理也有一个弊端,当数据库close后,再尝试读写数据库时,会抛出数据库未打开的异常。 在业务中很多数据库操作都是异步的,就很容易出现这种问题。 如应用在子线程发起了一个网络请求,请求成功后会将数据插入到数据库。 但是请求还未完成时,应用就退出了,同时数据库也close,那么等请求完成时,就可能调用数据操作,从而报错。

其实从实际的编程经验而言,数据库用完就要关闭这已经是共识了。但是在官方的codelab关于room数据库的使用上也没有明确的调用close方法,我在实际使用过程中也没发现什么问题。

这一点还要在后续的学习中继续深入一下。

2、找不到dao_impl 实现类的问题

主要是因为使用了kotlin版本的,但是又没添加ksp的依赖,如下。

// To use Kotlin Symbol Processing (KSP)
ksp "androidx.room:room-compiler:$room_version"

3、list adapter的问题

每次提交给listadapter的list集合必须是新的对象,而不是将当前list修改一下数据然后提交给listadapter,这样是不会生效的。 因为系统源码中会有前后两个list是否相等的对比,如果发现是一样的,则直接返回,故界面上不会有任何反应。

参考

1、使用 Room 将数据保存到本地数据库  |  Android 开发者  |  Android Developers (google.cn)