Room数据库的使用:新建数据库表类并且用@Entity注解,数据结构里有主键等;新建接口操作类并且用@Dao注解,里面方法可以用几个注解表达删除,插入,查询,更新等;在继承了RoomDatabase的文件中添加这两个类。网上教程很多,就简单写一下。
表类:
@Entity(tableName = "Test_Model")
public class TestModel {
@PrimaryKey
@NonNull
public String id;
public String name;
}
操作类:
@Dao
public interface TestDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) //id一样就是替换
void insert(TestModel...tests);
@Update
void update(TestModel...tests);
@Delete
void delete(TestModel...tests);
@Query("SELECT * FROM Test_Model")
List getAllTests();
}
数据库文件:
@Database(entities = {TestModel.class},
version = 1, exportSchema = false)
//
public abstract class CqmDatabase extends RoomDatabase {
private static CqmDatabase INSTANCE;
//因为在获取的时候,我在MVVM的model里使用,
//它不引入context,因此我分2个方法,和网上不一样
public static CqmDatabase getInstance() {
return INSTANCE;
}
//Application里onCreate调用就行
public static void init(Context context) {
if (INSTANCE == null) {
synchronized (CqmDatabase.class) {
INSTANCE = create(context);
}
}
}
private static CqmDatabase create(final Context context) {
return Room.databaseBuilder(
context,
CqmDatabase.class,
MyApplication.DB_NAME)
.allowMainThreadQueries()
.build();
}
public abstract TestDao getTestDao();
}
使用:
CqmDatabase.getInstance().getTestDao().delete(testModel);
大致是这样。
那么新增表的话,就是建和上面类似的文件,然后在Database里的新增注解内容以及底下的接口get就行了。
后续新增表里的字段的话则不一样,因为在数据库文件中,表已经生成好了,假设里面数据不重要的话,可以删除表再新建表,一般我们不会这样做。那么需要写数据库升级代码,比方在TestModel类中新增一个字段:public String time;假设新设备,那么可以安装不会有问题。老设备,即之前装过的设备会因为表结构变动而闪退。
升级数据库代码,写在CqmDatabase里:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Test_Model ADD COLUMN time TEXT");
}
};
private static CqmDatabase create(final Context context) {
return Room.databaseBuilder(
context,
CqmDatabase.class,
MyApplication.DB_NAME)
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2)
.build();
}
同时注解里的version = 2。
假如是新项目周期较短,从开始到开发完,一般数据库是只会新增表,而不会修改表结构的,为了图省事,也可以采用清除数据或者卸载重装的方法。等后续开发完之后,再添加Migration也没什么问题。但是我这边周期较长,导致数据库版本升上去后,还需要添加表。这个时候就不能只是新增文件了,也同样要写Migration,不然就会闪退。特别麻烦,而且又多,毕竟新需求来了添加表是很正常的,就会像这样:
static final Migration MIGRATION_6_7 = new Migration(6, 7) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE Customer_Model(id TEXT NOT NULL DEFAULT '', approved INTEGER NOT NULL," +
"code TEXT, customerId TEXT , deleted INTEGER NOT NULL, email TEXT, gmtCreate TEXT, " +
"gmtModified TEXT, industry TEXT, leader TEXT, mobile TEXT, name TEXT, sort TEXT, status INTEGER NOT NULL, " +
"PRIMARY KEY(id))");
}
};
这SQL你可以通过注解里的exportSchema=true生成的json文件里面找到。但是我就不喜欢那么多,有没有什么方法,在新增表的时候不报错,并且新增呢?
报错信息:
Room cannot verify the data integrity.
Looks like you've changed schema but forgot to update the version number.
You can simply fix this by increasing the version number.
只是为了基础使用的话,可以不往下看了,底下会给出新增表不需要写Migration的分析以及方法。
这闪退时机是打开App即闪退,那么就是只调用了init,即:
Room.databaseBuilder(
context,
CqmDatabase.class,
MyApplication.DB_NAME)
.allowMainThreadQueries()
.build();
典型的build模式,真正构建出我们用的实例就是在build()方法
public T build() {
//noinspection ConstantConditions
if (mContext == null) {
throw new IllegalArgumentException("Cannot provide null context for the database.");
}
//noinspection ConstantConditions
if (mDatabaseClass == null) {
throw new IllegalArgumentException("Must provide an abstract class that"
+ " extends RoomDatabase");
}
if (mQueryExecutor == null) {
mQueryExecutor = ArchTaskExecutor.getIOThreadExecutor();
}
if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) {
for (Integer version : mMigrationStartAndEndVersions) {
if (mMigrationsNotRequiredFrom.contains(version)) {
throw new IllegalArgumentException(
"Inconsistency detected. A Migration was supplied to "
+ "addMigration(Migration... migrations) that has a start "
+ "or end version equal to a start version supplied to "
+ "fallbackToDestructiveMigrationFrom(int... "
+ "startVersions). Start version: "
+ version);
}
}
}
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
DatabaseConfiguration configuration =
new DatabaseConfiguration(mContext, mName, mFactory, mMigrationContainer,
mCallbacks, mAllowMainThreadQueries,
mJournalMode.resolve(mContext),
mQueryExecutor,
mRequireMigration, mMigrationsNotRequiredFrom);
T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX);
db.init(configuration);
return db;
}
上面抛异常的地方,都不是提示的错误。
Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX); 这句话,是通过反射,获取到build里自动生成代码继承所写的dataBase的类生成实例。获取到实例后,调用init,那很明显是init导致的。
@CallSuper
public void init(@NonNull DatabaseConfiguration configuration) {
mOpenHelper = createOpenHelper(configuration);
boolean wal = false;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
wal = configuration.journalMode == JournalMode.WRITE_AHEAD_LOGGING;
mOpenHelper.setWriteAheadLoggingEnabled(wal);
}
mCallbacks = configuration.callbacks;
mQueryExecutor = configuration.queryExecutor;
mAllowMainThreadQueries = configuration.allowMainThreadQueries;
mWriteAheadLoggingEnabled = wal;
}
从这代码里可以看出,init里基本是赋值或者设置日志输出,除了第一行,createOpenHelper。
点进去看到其实现方法位于build自动生成的代码里CqmDatabase_Impl:
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper(..., ..., ..., ..., ...);
final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
.name(configuration.name)
.callback(_openCallback)
.build();
final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
return _helper;
}
点进RoomOpenHelper,能找到位于onOpen里的方法:
@Override
public void onOpen(SupportSQLiteDatabase db) {
super.onOpen(db);
checkIdentity(db);
mDelegate.onOpen(db);
// there might be too many configurations etc, just clear it.
mConfiguration = null;
}
private void checkIdentity(SupportSQLiteDatabase db) {
String identityHash = null;
if (hasRoomMasterTable(db)) {
Cursor cursor = db.query(new SimpleSQLiteQuery(RoomMasterTable.READ_QUERY));
//noinspection TryFinallyCanBeTryWithResources
try {
if (cursor.moveToFirst()) {
identityHash = cursor.getString(0);
}
} finally {
cursor.close();
}
}
if (!mIdentityHash.equals(identityHash) && !mLegacyHash.equals(identityHash)) {
throw new IllegalStateException("Room cannot verify the data integrity. Looks like"
+ " you've changed schema but forgot to update the version number. You can"
+ " simply fix this by increasing the version number.");
}
}
这个类应该是数据库运行时每个阶段的回调。可以看到checkIdentity就抛出了上面的异常。看代码可以看出,它是由于2个哈希值比较不上抛出的。就是说改动了数据库后,它生成的哈希值与之前存在room_master_table表里存的identity_hash对应不上。它要求修改了数据库结构就需要升级数据库版本。(咋生成的哈希值没有找到,它的值在上面RoomOpenHelper构造函数直接传过来的,应该是代码自动生成的时候就计算好直接写进去的)
这个哈希值功能可以看做是room的数据库保护机制。它用一个唯一id来判断数据库是否修改过。现在想要新增表的时候不抛异常就需要把这个判断去掉,或者把hash值设置成一致。hash值设置不了,现在考虑把这个判断去掉。
最简单的想法,当然是,拷贝一份这个代码,然后直接注释这个方法调用。在XxxDatabase_Impl这个自动生成的数据库实现类里,替换RoomOpenHelper。那么你就会发现,做不到。因为实现类是自动生成的,位于build文件夹,当然我们可以直接文件打开修改,但是下一次重新编译,就又变回去了。
如何修改呢?先要看看这个RoomOpenHelper,是在哪里调用的:
在上面的构造函数可以看到,它生成实例之后,
@Override
protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
final SupportSQLiteOpenHelper.Callback _openCallback = new RoomOpenHelper();
final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
.name(configuration.name)
.callback(_openCallback)
.build();
final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
return _helper;
}
如上图,可以看出它是_sqliteConfig里的参数,关键点还是底下的configuration.sqliteOpenHelperFactory.create(_sqliteConfig);这个configuration里的sqliteOpenHelperFactory是在
Room.databaseBuilder(
context,
CqmDatabase.class,
MyApplication.DB_NAME)
.allowMainThreadQueries()
.build();
build里面生成的,可以看到一句
if (mFactory == null) {
mFactory = new FrameworkSQLiteOpenHelperFactory();
}
查看这个类:
public final class FrameworkSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
return new FrameworkSQLiteOpenHelper(
configuration.context, configuration.name, configuration.callback);
}
}
对应代码来看,这个configuration.callback就是上面的RoomOpenHelper。也就是说我们把这构造函数的第三个参数,改成我们自定义注释掉那个判断方法的RoomOpenHelper就大功告成了。
注释:再往底层走,即它onOpen究竟哪里调用,就没必要了。room终究是对sqlite的封装,因此往下继续查看的话,会看到它走到了SQLiteOpenHelper,用原生写数据库自己操作的应该不陌生。这里面就有其中几个方法,onCreate onOpen等。
mFactory也是在build的时候可以配置的:openHelperFactory。那我们可以自己新建一个FrameworkSQLiteOpenHelperFactory,然后把create里的configuration.callback换成我们自己的。
首先拷贝整个RoomOpenHelper,注释掉,以及拷贝FrameworkSQLiteOpenHelperFactory替换里面的参数,这个时候,你就会发现一片爆红,以及有一些无法解决的问题。
第一:RoomOpenHelper的构造函数传进来的Delegate delegate,它是代码自动生成的,当然手写也写的出来,但是它是动态的,可以截个图放出一下代码量:
整整六百来行,不说手写,拷贝看着也不太行的样子。之所以这么多,它包含了数据库字段更新代码,数据库新建代码;
第二:其拷贝出来的Delegate delegate类是public的,但是里面的方法是protect的。拷贝的时候,也可以同样都拷贝出来,问题和一差不多,无法手写new一个。以及FrameworkSQLiteOpenHelperFactory 这里面的FrameworkSQLiteOpenHelper它不是public的,无法在外部新建。也许你会考虑,再拷贝FrameworkSQLiteOpenHelper这个代码,把它变成public的,你就会发现,它里面还有非公共类。越来越多。
因此,需要解决2个问题,一个是Delegate delegate这个东西怎么生成,另一个FrameworkSQLiteOpenHelperFactory里FrameworkSQLiteOpenHelper实例怎么弄出来。
从上面protect以及非公有类,很容易能联想到java的反射。我们通过反射,在拷贝的RoomOpenHelper调用 delegate的protect方法。通过反射去生成一个FrameworkSQLiteOpenHelper。
原理就是这个原理,上代码:
在拷贝里面,报错的有:
mDelegate.createAllTables(db);
mDelegate.onCreate(db);
mDelegate.validateMigration(db);
mDelegate.dropAllTables(db);
//这几个方法由于是protect的,拷贝出来无法使用,现换为反射调用
private void delegateCreateAllTables(SupportSQLiteDatabase db) {
Class cls = mDelegate.getClass();
try {
Method createAllTables= cls.getDeclaredMethod("createAllTables",SupportSQLiteDatabase.class);
createAllTables.setAccessible(true);
createAllTables.invoke(mDelegate, db);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
private void delegateOnCreate(SupportSQLiteDatabase db) {
Class cls = mDelegate.getClass();
try {
Method onCreate= cls.getDeclaredMethod("onCreate",SupportSQLiteDatabase.class);
onCreate.setAccessible(true);
onCreate.invoke(mDelegate, db);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
private void delegateValidateMigration(SupportSQLiteDatabase db) {
Class cls = mDelegate.getClass();
try {
Method validateMigration= cls.getDeclaredMethod("validateMigration",SupportSQLiteDatabase.class);
validateMigration.setAccessible(true);
validateMigration.invoke(mDelegate, db);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
private void delegateDropAllTables(SupportSQLiteDatabase db) {
Class cls = mDelegate.getClass();
try {
Method dropAllTables= cls.getDeclaredMethod("dropAllTables",SupportSQLiteDatabase.class);
dropAllTables.setAccessible(true);
dropAllTables.invoke(mDelegate, db);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
方法名都是一一对应的。
在生成这个拷贝出来的,我命名为CopyRoomHelper中,它的参数,其实都是从原有的RoomOpenHelper获取,这样我们就不需要去生成Delegate以及不需要考虑这个哈希值是怎么来的,同样的,RoomOpenHelper它里面值都是private的,也需要反射获取:
生成CopyRoomHelper:
private CopyRoomHelper getCopyRoomHelp(SupportSQLiteOpenHelper.Configuration configuration) {
RoomOpenHelper roomOpenHelper = (RoomOpenHelper) configuration.callback;
Class> roomHelperClz = roomOpenHelper.getClass();
Field configField = roomHelperClz.getDeclaredField("mConfiguration");
configField.setAccessible(true);
DatabaseConfiguration configuration1 = (DatabaseConfiguration)
configField.get(roomOpenHelper);
Field delegateField = roomHelperClz.getDeclaredField("mDelegate");
delegateField.setAccessible(true);
RoomOpenHelper.Delegate delegate2 = (RoomOpenHelper.Delegate) delegateField.get(roomOpenHelper);
Field identityField = roomHelperClz.getDeclaredField("mIdentityHash");
identityField.setAccessible(true);
String identityHash3 = (String) identityField.get(roomOpenHelper);
Field legacyField = roomHelperClz.getDeclaredField("mLegacyHash");
legacyField.setAccessible(true);
String legacyHash4 = (String) legacyField.get(roomOpenHelper);
CopyRoomHelper copyRoomHelper = new CopyRoomHelper(configuration1, delegate2, identityHash3, legacyHash4);
return copyRoomHelper;
}
生成FrameworkSQLiteOpenHelper同样是反射,放出完成的FrameworkSQLiteOpenHelperFactory:
public class SQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory {
@Override
public SupportSQLiteOpenHelper create(SupportSQLiteOpenHelper.Configuration configuration) {
try {
RoomOpenHelper roomOpenHelper = (RoomOpenHelper) configuration.callback;
Class> roomHelperClz = roomOpenHelper.getClass();
Field configField = roomHelperClz.getDeclaredField("mConfiguration");
configField.setAccessible(true);
DatabaseConfiguration configuration1 = (DatabaseConfiguration)
configField.get(roomOpenHelper);
Field delegateField = roomHelperClz.getDeclaredField("mDelegate");
delegateField.setAccessible(true);
RoomOpenHelper.Delegate delegate2 = (RoomOpenHelper.Delegate) delegateField.get(roomOpenHelper);
Field identityField = roomHelperClz.getDeclaredField("mIdentityHash");
identityField.setAccessible(true);
String identityHash3 = (String) identityField.get(roomOpenHelper);
Field legacyField = roomHelperClz.getDeclaredField("mLegacyHash");
legacyField.setAccessible(true);
String legacyHash4 = (String) legacyField.get(roomOpenHelper);
CopyRoomHelper copyRoomHelper = new CopyRoomHelper(configuration1, delegate2, identityHash3, legacyHash4);
Class> cls=Class.forName("androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper");
Constructor constructor = cls.getDeclaredConstructor(Context.class, String.class,
SupportSQLiteOpenHelper.Callback.class);
constructor.setAccessible(true);
return (SupportSQLiteOpenHelper) constructor.newInstance(configuration.context, configuration.name, copyRoomHelper);
} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException | NoSuchFieldException e) {
e.printStackTrace();
return null;
}
}
}
到这里其实就算完成了,让我们回到最开始,写room的时候必须建立的继承RoomDatabase的那个类。修改一下它的init:
public static void init(Context context) {
if (INSTANCE == null) {
synchronized (CqmDatabase.class) {
INSTANCE = create(context);
}
}
}
private static CqmDatabase create(final Context context) {
RoomDatabase.Builder builder = Room.databaseBuilder(
context,
CqmDatabase.class,
MyApplication.DB_NAME)
.openHelperFactory(new SQLiteOpenHelperFactory())
.allowMainThreadQueries()
.addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7);
return builder
.build();
}
.openHelperFactory(new SQLiteOpenHelperFactory()) 这句话就是应用了我们上面写的所有东西。还有个要点就是,做好后,启动程序,确实不会闪退了,但是它新增的表却也没有在数据库里。也就是说,闪退修好了,表没了。这是因为新装设备,或者说版本是1的时候,没有数据库新增代码,它都会走RoomOpenHelper里的,onCreate方法,对应会走Delegate里的createAllTables,它是自动生成代码的,里面有新建表的所有代码:
因此在数据库升级后,这代码就不走了,我们需要在拷贝RoomOpenHelper的CopyRoomHelper中的onOpen里调用delegateCreateAllTables:
大功告成!!!!!
————————————————
版权声明:本文为CSDN博主「咳咳涯」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_27454233/article/details/127249075