深入学习 Jetpack 系列的 Android Architecture Components 中的一些列组件,记录一下学习过程,本文是 Room 的使用及原理解析,通过一个实际的例子,来体验 Room 能给我们带来哪些不一样的功能?最后通过阅读 Room 的源码,由浅入深一步一步探索其原理!

Room 简介

Room 是 Google 官方推出的数据库 ORM 框架。ORM:即 Object Relational Mapping,即对象关系映射,也就是将关系型数据库映射为面向对象的语言。使用 ORM 框架,我们就可以用面向对象的思想操作关系型数据库,不再需要编写 SQL 语句。

Room 是在 SQLite 的基础上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够更简便的访问数据库。

Room 包含三个组件:EntityDAODatabase
Entity:实体类,数据库中的表(table),保存数据库并作为应用持久性数据底层连接的主要访问点。
DAO:数据库访问对象,提供访问 DB 的 API,如增删改查等方法。
Database:访问底层数据库的入口,管理着真正的数据库文件。

Room 库架构的示意图:
Room 库架构的示意图
相比于 SQLiteOpenHelper 等传统方法,Room 在操作 SQLite 有哪些优势?

  1. 针对 SQL 查询的编译期验证
  2. 开发效率提高,可最大限度减少重复和容易出错的样板代码
  3. 友好地 API 设计,使用简介且容易理解
  4. 简化了数据库迁移路径
  5. 可以与 RxJavaLiveDataKotlin Coroutines 等进行集成桥接

1. Room 的使用

1.1 创建数据库管理 Database

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,避免反复创建实例带来的开销。

1.2 新建 Entity

实体类,使用 @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?
)
1.3 创建 Data Access Object (DAO)

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 线程直接访问。

1.4 数据库管理工具
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)
        }
    }
}
1.5 Activity 中使用
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))
            }
        }
    }
}
1.6 小结

Room 的使用还有很多东西需要学习,比如:连表查询、预填充数据库、数据库版本升级和迁移、类型转换器 TypeConverter、LiveData 集成监听数据库变化等等,更多功能可以参考Android 开发者 Jetpack Room篇

2. 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 的实现类、数据库名称的常规设置外,还包含了数据库的升级信息等,下面是一些常用的介绍如下:

  1. createFromAsset()/createFromFile():从 SD 卡或者 Assets 的 DB 文件创建 RoomDatabase 实例
  2. addMigrations():添加一个数据库迁移(migration),当进行数据版本升级时需要
  3. allowMainThreadQueries():允许在 UI 线程进行数据库查询,默认是不允许的
  4. fallbackToDestructiveMigration():如果找不到 migration 则重建数据库表(会造成数据丢失)

接下来看一下 RoomDatabase ## build() 方法,我们跟进去探索一下:

2.1 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 实例,然后初始化数据库。

2.2 Room ## getGeneratedImplementation() 方法
@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,然后通过反射创建一个实例化对象。

2.3 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。此外还有几个方法如下:

  1. createOpenHelper():Room.databaseBuilder().build() 创建 Database 时,会调用生成的实现类的 createOpenHelper() 方法创建SupportSQLiteOpenHelper,此 Helper 用来创建 DB 以及管理版本,这里创建了 RoomOpenHelper,RoomOpenHelper 继承 SupportSQLiteOpenHelper.Callback,这里可以看出,Room 就是对原生 SQLite 的封装,里面有初始化和数据库升级等操作。
  2. createInvalidationTracker():创建跟踪器,确保 table 的记录修改时能通知到相关回调方
  3. clearAllTables():清空数据库中 table 的实现
2.4 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 语句。

  1. __db:RoomDatabase 的实例
  2. __insertionAdapterOfUser:EntityInsertionAdapterd 实例,用于数据插入 insert。
  3. __deletionAdapterOfUser:EntityDeletionOrUpdateAdapter 实例,用于数据的更新、删除 update/delete。
    上述实例,用来真正地操作底层数据库文件。
2.5 RoomDataBase ## init() 方法
@CallSuper
    public void init(@NonNull DatabaseConfiguration configuration) {
        mOpenHelper = createOpenHelper(configuration);
    }

分析1 – 在 db.init() 方法中会调用 RoomDataBase 中的抽象方法createOpenHelper(),TestRoomDataBase_Impl 类实现了该方法,在方法中创建继承自 SupportSQLiteOpenHelper.Callback 的 RoomOpenHelper 的实例,构造方法中需传入 RoomOpenHelper 的 Delegate 抽象类的具体实现类,在该实现类实现的方法中收到并处理 mDelegate 的各种回调,看一下其中的几个方法:

  1. createAllTables():创建数据库
  2. onCreate():创建数据库,判断数据库是否为空,如不为空则是预填充的,这时须验证是否可用
  3. dropAllTables():删除数据库中的表
  4. onValidateSchema():迁移或预打包数据库后,验证数据库完整性

3. 总结

通过对 Room 的简单使用及源码执行流程的分析,可以加深我们对 Room 组件的理解。上文我们说过 Room 是对原生 SQLite 数据库的封装,这里没有对 SQLite 数据库做进一步的分析,感兴趣的可以自行查阅源码。

4. 遇到的问题

4.1 编译时 Java 8 的 bug 导致的异常,日志信息如下:
 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 然后编译通过并运行的。
在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐