Room数据库基础使用以及新增表,不增加版本号以及Migration的方法升级

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<TestModel> 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<CqmDatabase> 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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
对于 Room 数据库升级,您需要遵循以下步骤: 1. 在新版本的数据库中定义新结构或对旧结构进行更改,例如添加、删除或修改的列。 2. 在您的 `AppDatabase` 类中增加数据库版本号,可以在类上使用 `@Database` 注解指定版本号,例如: ```kotlin @Database(entities = [User::class], version = 2) abstract class AppDatabase : RoomDatabase() { //... } ``` 3. 创建一个实现 `Migration` 接口的类,该类将包含从旧版本升级到新版本所需的所有更改。例如: ```kotlin val migration_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0") } } ``` 该示例代码示,从版本 1 升级到版本 2,需要在 `users` 中添加一个名为 `age` 的整数类型的列。 4. 在 `AppDatabase` 类中,使用 `addMigrations()` 方法将 `Migration` 对象添加到数据库中,例如: ```kotlin @Database(entities = [User::class], version = 2) abstract class AppDatabase : RoomDatabase() { //... companion object { val migration_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0") } } } //... init { if (BuildConfig.DEBUG) { // 在调试模式下,如果发现数据结构变化,将会清空数据 fallbackToDestructiveMigration() } else { // 在正式发布模式下,如果发现数据结构变化,将会执行升级脚本 addMigrations(migration_1_2) } } } ``` 在上述示例代码中,我们将 `migration_1_2` 对象添加到 `AppDatabase` 类的伴生对象中,并在 `init` 块中进行了初始化。我们还使用了 `fallbackToDestructiveMigration()` 方法,如果在调试模式下发现数据结构变化,将会清空数据。在正式发布模式下,我们使用了 `addMigrations()` 方法,将 `migration_1_2` 对象添加到数据库中,以执行升级脚本。 这样,在您的应用程序使用新版本的数据库时,将自动执行升级脚本,以将旧数据结构转换为新数据结构。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值