android room数据库的基础使用
room数据库的基础使用
最近在工作中要使用到数据库,但是又不能引入greendao框架,没办法只能自己动手写最基本的增删改查的代码。 完成了功能后,实在看不下去各种数据库的操作。 于是想,肯定有更好的解决方法。 想起了之前google很早就提出的room数据库了,然后在官网上看了一下用法,真是惊讶到了,真的很方便快捷。感觉少许的代码就完成了最基础的功能。 此处记录一下room数据库的基本用法与遇到的问题等。
下图是google codelab中的示意图,通过此图可以大概了解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)
转载自:https://juejin.cn/post/7143200793707937799