Room 使用及初步分析

一、概述

Room是对SQLite数据库的抽象,它提供了很多便利的API和注解等,让使用者对数据库的访问更加方便,同时可以减少很多的模板代码。另外 Room提供的单元测试相关方法,本文也会着重介绍一下。

二、基本用法

1.主要组件

DataBase:实际数据库的访问入口

@Database(
        entities = {
                User.class
        },
        version = 1)

public abstract class AppDatabase extends RoomDatabase {
    //用变量,避免不同分支同时升级数据库导致出错
    public static class DatabaseVersion {
        public static final int VERSION_1 = 1;
        public static final int VERSION_2 = 2;
    }
    public abstract UserDao getUerDao();
}

Entity:数据库里的表,用@Entity注解标注一个数据类,就可以把它变成一个表

@Entity
public class User {

    @PrimaryKey
    @NonNull
    private String id;


    @ColumnInfo
    private String name;

    public User(@NonNull String id, String name) {
        this.id = id;
        this.name = name;
    }


    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

DAO:数据库访问者,用@Dao注解标注接口,配合 sql 相关的注解,在编译时,Room会自动实现这个接口,并生成相应的代码

@Dao
public interface UserDao {
    // 如果已有同样主键的记录,则替换
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertUserRecord(User user);


    @Query("select *from User where id = (:id)")
    User geUerById(String id);
}

2.使用方法

一般通过构造一个Database的单例出来使用,构造的时候可以配置升级的方案等。

public class AppDatabaseFactory {
    public static final String DATABASE_NAME = "My_Database";
    private static volatile AppDatabase sInstance;

    private AppDatabaseFactory() {
    }

    public static AppDatabase create(Context context) {
        if (sInstance == null) {
            synchronized (AppDatabaseFactory.class) {
                if (sInstance == null) {
                    sInstance = Room.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
                            .enableMultiInstanceInvalidation()
                            .addMigrations(MIGRATION_1_2)
                            .build();
                }
            }
        }
        return sInstance;
    }

}

3.数据库升级

升级方案:
提供了一个Migration的类,每次升级版本都要新增一个Migration,也就是说每次版本升级要执行的操作是写在一起的,这跟SQLite是完全不同的,这样可以避免两个不同的分支,同时升级版本号,但是代码不冲突,导致没有发现升级配置出错情况。

//数据库升级配置
static final Migration MIGRATION_1_2 = new Migration(AppDatabase.DatabaseVersion.VERSION_1,
        AppDatabase.DatabaseVersion.VERSION_2) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL("CREATE TABLE `Book` (`id` TEXT NOT NULL, `name` TEXT, PRIMARY KEY(`id`))");
    }
};

单元测试:
Room 还提供了对数据库升级做单元测试的解决方案,通过MigrationTestHelper 可以实现真实的在手机里面安装一个APP去做数据库所有版本之间升级的测试,大大减少了项目数据库升级可能存在的风险性。
注意这里要用单元测试去验证的话,需要在 build.gradle 里面做schemas的相应配置,这是一个描述数据库版本信息的 json 文件,每一个版本会有一个。

@RunWith(AndroidJUnit4.class)
public class MigrationTest {
    protected static final String TEST_DB = "migration-test";

    @Rule
    public MigrationTestHelper helper =
            new MigrationTestHelper(InstrumentationRegistry.getInstrumentation(),
                    AppDatabase.class.getCanonicalName(),
                    new FrameworkSQLiteOpenHelperFactory());

    @Test
    public void migrateAll() throws IOException {
        SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);
        db.close();
        helper.runMigrationsAndValidate(TEST_DB, 2, true, ALL_MIGRATIONS);
        AppDatabase appDb = getMigratedRoomDatabase();
        appDb.getOpenHelper().getWritableDatabase();
        appDb.close();
    }


    private AppDatabase getMigratedRoomDatabase() {
        AppDatabase database = Room.databaseBuilder(ApplicationProvider.getApplicationContext(),
                AppDatabase.class, TEST_DB)
                .addMigrations(ALL_MIGRATIONS)
                .build();
        helper.closeWhenFinished(database);
        return database;
    }

    private static final Migration[] ALL_MIGRATIONS = new Migration[]{
            MIGRATION_1_2};

}


三、源码分析

从上面的Demo代码可以看出,Room有很多的注解,实际上Room正是通过APT注解处理器,自动生成了许多代码,避免使用者在为了使用数据库编写重复的模板代码。

1.Room.databaseBuilder.build()

数据库使用的入口,也就是构造RoomDatabase的实例,它里面会根据buider中做的配置,进行一系列赋值操作,生成一个 DatabaseConfiguration对象,然后传入的 class 对象,调用Class.newInstance()方法,获取到RoomDatabase的实例。
接着就是调用RoomDatabase.init进行初始化操作。

public T build() {
    //...省略
    DatabaseConfiguration configuration =
                    new DatabaseConfiguration(
                            mContext,
                            mName,
                            factory,
                            mMigrationContainer,
                            mCallbacks,
                            mAllowMainThreadQueries,
                            mJournalMode.resolve(mContext),
                            mQueryExecutor,
                            mTransactionExecutor,
                            mMultiInstanceInvalidation,
                            mRequireMigration,
                            mAllowDestructiveMigrationOnDowngrade,
                            mMigrationsNotRequiredFrom,
                            mCopyFromAssetPath,
                            mCopyFromFile,
                            mCopyFromInputStream,
                            mPrepackagedDatabaseCallback,
                     mTypeConverters);
     T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
     db.init(configuration);
     return db;
}

2.RoomDatabase.init()

调用了createOpenHelper方法,createOpenHelper方法实现在AppDatabase_Impl中,创建了RoomOpenHelperRoomOpenHelper继承SupportSQLiteOpenHelper.Callback
引申:注意这里的 AutoCloser对象,这里没有仔细研究它的代码,但是它应该是代理持有了一个 SupportSQLiteOpenHelper对象,可以实现在提交数据库事务之后,自动的判断并close数据库。
从这里就可以看出,Room实际上是对 SQLite的再次封装,但是通过 APT 以及其他辅助类,使得Room的比直接用SQLite要简便很多。

@Override
  protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
    final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(configuration,
                                                                              new RoomOpenHelper.Delegate(2) {
       //...省略
      }
}

@CallSuper
public void init(@NonNull DatabaseConfiguration configuration) {
    mOpenHelper = createOpenHelper(configuration);

    // Configure SqliteCopyOpenHelper if it is available:
    SQLiteCopyOpenHelper copyOpenHelper = unwrapOpenHelper(SQLiteCopyOpenHelper.class,
            mOpenHelper);
    if (copyOpenHelper != null) {
        copyOpenHelper.setDatabaseConfiguration(configuration);
    }

    AutoClosingRoomOpenHelper autoClosingRoomOpenHelper =
            unwrapOpenHelper(AutoClosingRoomOpenHelper.class, mOpenHelper);

    if (autoClosingRoomOpenHelper != null) {
        mAutoCloser = autoClosingRoomOpenHelper.getAutoCloser();
        mInvalidationTracker.setAutoCloser(mAutoCloser);
    }  
    
    // ... 省略
}
                                                                              

3.AppDatabase.getUserDao()

AppDatabase是我们继承自RoomDatabase的抽象类,我们在里面添加了获取UserDao的方法,Room 会通过APT技术,在编译之后自动帮我们添加对应的实现。

@Override
  public UserDao getUerDao() {
    if (_userDao != null) {
      return _userDao;
    } else {
      synchronized(this) {
        if(_userDao == null) {
          _userDao = new UserDao_Impl(this);
        }
        return _userDao;
      }
    }
  }

4.UserDao_Impl

可以看出来,UserDao_Impl根据注解,具体的实现了我们在 UserDao接口中添加的方法,其中的关键点就在于,根据我们添加在注解中的 sql 语句,生成了对数据库的访问代码。

@Override
public void insertUserRecord(final User user) {
  __db.assertNotSuspendingTransaction();
  __db.beginTransaction();
  try {
    __insertionAdapterOfUser.insert(user);
    __db.setTransactionSuccessful();
  } finally {
    __db.endTransaction();
  }
}

@Override
public User geUerById(final String id) {
  final String _sql = "select *from User where id = (?)";
  final RoomSQLiteQuery _statement = RoomSQLiteQuery.acquire(_sql, 1);
  int _argIndex = 1;
  if (id == null) {
    _statement.bindNull(_argIndex);
  } else {
    _statement.bindString(_argIndex, id);
  }
  __db.assertNotSuspendingTransaction();
  final Cursor _cursor = DBUtil.query(__db, _statement, false, null);
  try {
    final int _cursorIndexOfId = CursorUtil.getColumnIndexOrThrow(_cursor, "id");
    final int _cursorIndexOfName = CursorUtil.getColumnIndexOrThrow(_cursor, "name");
    final User _result;
    if(_cursor.moveToFirst()) {
      final String _tmpId;
      if (_cursor.isNull(_cursorIndexOfId)) {
        _tmpId = null;
      } else {
        _tmpId = _cursor.getString(_cursorIndexOfId);
      }
      final String _tmpName;
      if (_cursor.isNull(_cursorIndexOfName)) {
        _tmpName = null;
      } else {
        _tmpName = _cursor.getString(_cursorIndexOfName);
      }
      _result = new User(_tmpId,_tmpName);
    } else {
      _result = null;
    }
    return _result;
  } finally {
    _cursor.close();
    _statement.release();
  }
}

四、总结

本文只是对Room的一个简单分析,正如在一开始的概述里面说的那样:Room是对SQLite数据库的抽象,它提供了很多便利的API和注解等,简化了使用者使用数据库的方式。本文没有分析 RoomLiveData结合使用的情况,因为笔者公司的项目还没能引入 LiveData。

抛开这一点不谈,个人认为它有两个比较显著的优点:
1、当然就是通过APT技术,TypeConverter 等,简化了使用,减少了大量的访问数据库的模板代码。
2、是文中也有提到的,它对数据库升级的优化,除了可以避免两个分支同时升级数据库但是合并不冲突导致的错误之外,它还提供了对数据库升级做单元测试的工具类,安全性提升很多。

五、引用

Jetpack-Room框架使用与源码分析


版权声明:本文为tulensa原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。