深入学习 Jetpack 系列的 Android Architecture Components 中的一些列组件,记录一下学习过程,本文是 Room 的使用及原理解析,通过一个实际的例子,来体验 Room 能给我们带来哪些不一样的功能?最后通过阅读 Room 的源码,由浅入深一步一步探索其原理!
Room 是 Google 官方推出的数据库 ORM 框架。ORM:即 Object Relational Mapping,即对象关系映射,也就是将关系型数据库映射为面向对象的语言。使用 ORM 框架,我们就可以用面向对象的思想操作关系型数据库,不再需要编写 SQL 语句。
Room 是在 SQLite 的基础上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够更简便的访问数据库。
Room 包含三个组件:Entity、DAO 和 Database。
Entity:实体类,数据库中的表(table),保存数据库并作为应用持久性数据底层连接的主要访问点。
DAO:数据库访问对象,提供访问 DB 的 API,如增删改查等方法。
Database:访问底层数据库的入口,管理着真正的数据库文件。
Room 库架构的示意图:
相比于 SQLiteOpenHelper 等传统方法,Room 在操作 SQLite 有哪些优势?
Database 是我们访问底层数据库的入口,管理着真正的数据库文件。定义数据库中包含的表、数据库的版本、包含的 DAO 类,以及数据库升级逻辑。
我们使用 @Database 定义一个 TestRoomDataBase 抽象类,继承自 RoomDatabase,用来关联其内部数据库表对应的 entities,内部有一个获取 DAO 的抽象方法,注意不能有参数。
@Database(
// 指定该数据库有哪些表,若需建立多张表,以逗号相隔开
entities = [User::class],
// 指定数据库版本号,后续数据库的升级需依据版本号来判断
version = 1,
// 当设置变量 room.schemaLocation="XXX“时,且 exportSchema 为 true 时将数据库模式导出到给定的文件夹
exportSchema = false
)
abstract class TestRoomDataBase : RoomDatabase() {
abstract fun userDao(): UserDao
// 伴生对象实现单例
companion object {
private const val DB_NAME = "user.db"
@Volatile
private var INSTANCE: TestRoomDataBase? = null
fun getDatabase(context: Context): TestRoomDataBase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
TestRoomDataBase::class.java,
DB_NAME
)
// 开发阶段可使用,不可在正式环境中使用,因为直接丢弃旧版本数据库,
// 然后创建最新的数据库会导致旧版本数据库丢失
//.fallbackToDestructiveMigration()
.build()
INSTANCE = instance
instance
}
}
}
}
注意: 创建 Databsse 的成本较高,推荐使用单例的 Database,避免反复创建实例带来的开销。
实体类,使用 @Entity 定义一个 Entiry 类,每个 Entity 代表数据库中的一张表(table),使用 @ColumnInfo 定义类中的属性字段,每个属性字段都对应表中的一个 Column。所有的属性必须是 public、或者有 get、set 方法。属性中至少有一个主键,用于唯一标识相应数据库表中的每一行,使用 @PrimaryKey 表示单个主键,也可以定义多主键,当主键值为 null 时,autoGenerate 可以帮助自动生成键值。可以使用 indices 指定数据库索引,unique设置其为唯一索引。
// 该注解代表数据库一张表,tableName为该表名字,不设置则默认类名
// 注解必须有!!tableName可以不设置
@Entity(
tableName = "User",
indices = [Index(
value = ["userName"],
unique = true
)]
)
data class User(
// 该注解指定该字段作为表的主键, 自增长。注解必须有!!
@PrimaryKey(autoGenerate = true)
val id: Int? = null,
// 该注解设置当前属性在数据库表中的列名和类型,注解可以不设置,不设置默认列名和属性名相同
@ColumnInfo(name = "user_name", typeAffinity = ColumnInfo.TEXT)
val userName: String?,
// Entity 中的所有属性都会被持久化到数据库,除非使用 @Ignore
@ColumnInfo(name = "user_gender", typeAffinity = ColumnInfo.TEXT)
@Ignore val userGender: String?
)
DAO 是指 Data Access Object,即数据访问对象,通常定义成为一个接口或抽象类,一个 Entity 代表着一张表,而每张表都需要一个 DAO 对象。其提供了访问 DB 的 API,我们使用 @Dao 定义 DAO 接口或类,使用 @Query 、@Insert、 @Delete 定义 CRUD 方法,方便对这张表进行增删改查,这样逻辑层就不需要和数据库直接交互,只需要使用 DAO 类即可。
Room 在编译期会基于定义的 DAO 生成具体实现类,实现具体的 CURD 方法。
@Dao
interface UserDao {
// 查询操作,@Query 中的 SQL 语句以及返回值类型等会在编译期进行检查,更早的暴露问题
@Query("SELECT * FROM user")
fun getAll(): List<User>
// 查询操作,@Query 中的 SQL 语句可以用参数指定 @Query 中的 where 条件
@Query("SELECT * FROM user WHERE id IN (:userIds)")
fun loadAllByIds(userIds: IntArray): List<User>
// 插入操作,编译期生成的代码会将所有的参数以单独的事务更新到 DB
@Insert
fun insertAll(vararg users: User)
// 更新操作,根据参数对象的主键更新指定 row 的数据
// onConflict 设置当事务中遇到冲突时的策略
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(vararg user: User)
// 删除操作,根据参数对象的主键删除指定 row
@Delete
fun delete(user: User)
}
注意: DAO 的方法调用都在当前线程进行,所以要避免在 UI 线程直接访问。
class RoomDatabaseHelper {
companion object {
@Volatile
private var instance: RoomDatabaseHelper? = null
@Synchronized
fun getInstance(): RoomDatabaseHelper {
if (instance == null) {
instance = RoomDatabaseHelper()
}
return instance!!
}
}
/**
* 查询用户信息
*/
fun getUser(context: Context): List<User>? {
val userList = TestRoomDataBase.getDatabase(context).userDao().getAll()
if (userList.isEmpty()) {
return null
}
return userList
}
/**
* 储存用户信息
*
* @param user 用户
*/
fun saveUser(context: Context, user: User?) {
user?.let {
TestRoomDataBase.getDatabase(context).userDao().insertAll(it)
}
}
/**
* 删除用户信息
*
* @param user 用户
*/
fun deleteUser(context: Context, user: User?) {
user?.let {
TestRoomDataBase.getDatabase(context).userDao().delete(it)
}
}
}
class TestRoomActivity : BaseActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_live_data)
val user = User(null, "张三", "男")
btnUpdate.setOnClickListener {
// 点击按钮后删除
RoomDatabaseHelper.getInstance().deleteUser(this@TestLiveDataActivity, user)
}
GlobalScope.launch(Dispatchers.IO) {
RoomDatabaseHelper.getInstance().saveUser(this@TestLiveDataActivity, user)
val userList = RoomDatabaseHelper.getInstance().getUser(this@TestLiveDataActivity)
withContext(Dispatchers.Main) {
println("查询:" + Gson().toJson(userList))
}
}
}
}
Room 的使用还有很多东西需要学习,比如:连表查询、预填充数据库、数据库版本升级和迁移、类型转换器 TypeConverter、LiveData 集成监听数据库变化等等,更多功能可以参考Android 开发者 Jetpack Room篇。
Room 在编译期通过 kapt 处理 @Dao 和 @Database 注解,通过注解在运行时生成代码和SQL语句,并生成 DAO 和 Database 的实现类:TestRoomDataBase_Impl 和 UserDao_Impl。
下面来深入源码,一步步解读,分析 Room 执行流程:
以下面这一段代码作为入口来分析:
val instance = Room.databaseBuilder(
context.applicationContext,
TestRoomDataBase::class.java,
DB_NAME
).build()
Room.databaseBuilder() 方法使用 Builder 模式创建 RoomDataBase,内部除了包含 Room 的实现类、数据库名称的常规设置外,还包含了数据库的升级信息等,下面是一些常用的介绍如下:
接下来看一下 RoomDatabase ## build() 方法,我们跟进去探索一下:
@SuppressLint("RestrictedApi")
@NonNull
public T build() {
......
if (mQueryExecutor == null && mTransactionExecutor == null) {
mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor();
} else if (mQueryExecutor != null && mTransactionExecutor == null) {
mTransactionExecutor = mQueryExecutor;
} else if (mQueryExecutor == null && mTransactionExecutor != null) {
mQueryExecutor = mTransactionExecutor;
}
......
SupportSQLiteOpenHelper.Factory factory;
......
if (mFactory == null) {
factory = new FrameworkSQLiteOpenHelperFactory();
} else {
factory = mFactory;
}
if (mAutoCloseTimeout > 0) {
......
factory = new AutoClosingRoomOpenHelperFactory(factory, autoCloser);
}
if (mCopyFromAssetPath != null
|| mCopyFromFile != null
|| mCopyFromInputStream != null) {
......
factory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile,
mCopyFromInputStream, factory);
}
if (mQueryCallback != null) {
factory = new QueryInterceptorOpenHelperFactory(factory, mQueryCallback,
mQueryCallbackExecutor);
}
// 初始化 Database 的配置信息
DatabaseConfiguration configuration =
new DatabaseConfiguration(
mContext,
mName,
factory,
mMigrationContainer,
mCallbacks,
mAllowMainThreadQueries,
mJournalMode.resolve(mContext),
mQueryExecutor,
mTransactionExecutor,
mMultiInstanceInvalidation,
mRequireMigration,
mAllowDestructiveMigrationOnDowngrade,
mMigrationsNotRequiredFrom,
mCopyFromAssetPath,
mCopyFromFile,
mCopyFromInputStream,
mPrepackagedDatabaseCallback,
mTypeConverters);
// 创建 Database 类的实例
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
// 分析1 -- 初始化数据库
db.init(configuration);
return db;
}
RoomDatabase ## build() 方法,通过初始化不同的工厂、Database的配置信息,最后由 Room ## getGeneratedImplementation() 方法
创建 Database 实例,然后初始化数据库。
@SuppressWarnings({"TypeParameterUnusedInFormals", "ClassNewInstance"})
@NonNull
static <T, C> T getGeneratedImplementation(Class<C> klass, String suffix) {
final String fullPackage = klass.getPackage().getName();
String name = klass.getCanonicalName();
// 获取类名
final String postPackageName = fullPackage.isEmpty()
? name
: name.substring(fullPackage.length() + 1);
/// 拼接类名,加上 _Impl 后缀
final String implName = postPackageName.replace('.', '_') + suffix;
//noinspection TryWithIdenticalCatches
try {
final String fullClassName = fullPackage.isEmpty()
? implName
: fullPackage + "." + implName;
@SuppressWarnings("unchecked")
// 通过 ClassLoader 获取生成的类的字节码文件,即 TestRoomDataBase_Impl
final Class<T> aClass = (Class<T>) Class.forName(
fullClassName, true, klass.getClassLoader());
// 通过反射的方式创建一个实例化对象
return aClass.newInstance();
} catch (ClassNotFoundException e) {
throw new RuntimeException("cannot find implementation for "
+ klass.getCanonicalName() + ". " + implName + " does not exist");
} catch (IllegalAccessException e) {
throw new RuntimeException("Cannot access the constructor"
+ klass.getCanonicalName());
} catch (InstantiationException e) {
throw new RuntimeException("Failed to create an instance of "
+ klass.getCanonicalName());
}
}
获取全路径类名并在尾部添加 _Impl 后缀,通过 ClassLoader 获取生成类的字节码文件,即 TestRoomDataBase_Impl,然后通过反射创建一个实例化对象。
@SuppressWarnings({"unchecked", "deprecation"})
public final class TestRoomDataBase_Impl extends TestRoomDataBase {
private volatile UserDao _userDao;
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration, new RoomOpenHelper.Delegate(1) {
......
}
@Override
protected InvalidationTracker createInvalidationTracker() {
final HashMap<String, String> _shadowTablesMap = new HashMap<String, String>(0);
HashMap<String, Set<String>> _viewTables = new HashMap<String, Set<String>>(0);
return new InvalidationTracker(this, _shadowTablesMap, _viewTables, "User");
}
// 清空 table 的实现
@Override
public void clearAllTables() {
super.assertNotMainThread();
......
}
@Override
protected Map<Class<?>, List<Class<?>>> getRequiredTypeConverters() {
final HashMap<Class<?>, List<Class<?>>> _typeConvertersMap = new HashMap<Class<?>, List<Class<?>>>();
_typeConvertersMap.put(UserDao.class, UserDao_Impl.getRequiredConverters());
return _typeConvertersMap;
}
@Override
public UserDao userDao() {
if (_userDao != null) {
return _userDao;
} else {
synchronized(this) {
if(_userDao == null) {
// 创建 UserDao 的实例并返回
_userDao = new UserDao_Impl(this);
}
return _userDao;
}
}
}
}
TestRoomDataBase_Impl 是系统根据注解自动创建的实现类,并重写了抽象方法 userDao(),在 userDao() 方法中使用单例的方式提供 UserDao 的实现类 UserDao_Impl。此外还有几个方法如下:
@SuppressWarnings({"unchecked", "deprecation"})
public final class UserDao_Impl implements UserDao {
private final RoomDatabase __db;
private final EntityInsertionAdapter<User> __insertionAdapterOfUser;
private final EntityDeletionOrUpdateAdapter<User> __deletionAdapterOfUser;
private final EntityDeletionOrUpdateAdapter<User> __updateAdapterOfUser;
public UserDao_Impl(RoomDatabase __db) {
this.__db = __db;
this.__insertionAdapterOfUser = new EntityInsertionAdapter<User>(__db) {};
this.__deletionAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {};
this.__updateAdapterOfUser = new EntityDeletionOrUpdateAdapter<User>(__db) {};
}
@Override
public void insertAll(final User... users) {}
@Override
public void delete(final User user) {}
@Override
public void update(final User... user) {}
@Override
public List<User> getAll() {}
@Override
public List<User> loadAllByIds(final int[] userIds) {}
}
UserDao_Impl 真正实现了我们在 DAO 中定义的所有添加了注解的方法,并生成对应的 SQL 语句。
@CallSuper
public void init(@NonNull DatabaseConfiguration configuration) {
mOpenHelper = createOpenHelper(configuration);
}
分析1 – 在 db.init() 方法中会调用 RoomDataBase 中的抽象方法createOpenHelper(),TestRoomDataBase_Impl 类实现了该方法,在方法中创建继承自 SupportSQLiteOpenHelper.Callback 的 RoomOpenHelper 的实例,构造方法中需传入 RoomOpenHelper 的 Delegate 抽象类的具体实现类,在该实现类实现的方法中收到并处理 mDelegate 的各种回调,看一下其中的几个方法:
通过对 Room 的简单使用及源码执行流程的分析,可以加深我们对 Room 组件的理解。上文我们说过 Room 是对原生 SQLite 数据库的封装,这里没有对 SQLite 数据库做进一步的分析,感兴趣的可以自行查阅源码。
Current JDK version 1.8.0_201 has a bug (https://bugs.openjdk.java.net/browse/JDK-8007720) that prevents Room from being incremental. Consider using JDK 11+ or the embedded JDK shipped with Android Studio 3.5+.
这里我们可以按照日志的提示使用 JDK 11+ 或者 Android Studio 内嵌的 JDK(11+),我这边改为使用内嵌的 JDK 然后编译通过并运行的。