Android 数据库操作:Room

29 篇文章 0 订阅
28 篇文章 0 订阅

Room 是 Google 官方对 SqliteDatabase 的封装库,本文列举了 Room 数据库组件的基本操作。
Room 官方文档:https://developer.android.google.cn/training/data-storage/room

可与 Sqlite 的操作对比来看:https://blog.csdn.net/Gdeer/article/details/88869051

一、数据库

1.1 定义

定义一个数据库需要继承 RoomDatabase,并在注解中提供 entities 和 version。它相当于定义了一个 SQLiteOpenHelper,并在其 onCreate 回调中执行创表语句。

entities 即数据库中数据的实体类,用来生成表。
version 即数据库的版本号。

@Database(entities = {City.class}, version = 1)
public abstract class ChinaDatabase extends RoomDatabase {
    public abstract CityDao cityDao();
}

在编译过后,Room 会自动生成 ChinaDatabase 的实现类 ChinaDatabase_Impl。

1.2 获取

通过 Room.databaseBuilder() 来生成一个 RoomDatabase,同时对它进行一些配置,如初始化处理、升降级处理等。

build 不会阻塞主线程,可以在主线程执行。

public static void accessDb(Context context) {
    chinaDb = Room.databaseBuilder(context, ChinaDatabase.class, "china-room")
        .build();
}

1.2.1 初始化

在 build 时,可以添加 Callback,相当于 SQLiteOpenHelper 中的 onCreate 回调。可以在这里进行数据库的初始化操作,如添加一些数据等(SQLiteDatabase 中 onCreate 经常用来建表,但在 Room 中,已经在定义 RoomDatabase 时通过注解生成了)。

这里不能操作 chinaDb(还没初始化),但可以操作 SQLiteDatabase 的封装 db,通过 db 来 execSQL()。

public static void accessDb(Context context) {
    chinaDb = Room.databaseBuilder(context, ChinaDatabase.class, "china-room")
        .addCallback(new RoomDatabase.Callback() {
            @Override
            public void onCreate(@NonNull SupportSQLiteDatabase db) {
                db.execSQL("insert into city (id, name) values (1, '上海')");
            }
        })
        .build();
}

1.2.2 升降级

Room 数据库的升降级通过 Migration 来实现,在 build 时,给 RoomDatabase 加入一个 Migration。

Migration 指明了负责的版本变化路径,即从 startVersion 到 endVersion。当版本号的变化符合该 Migration,那 migrate 就会执行。如版本号 1->2,那 Migration(1, 2) 会执行;版本号 2->1,那 Migration(2, 1) 会执行。

如果当前的版本变化路径没有设置相应的 Migration,那就会抛出一个异常。可以通过 build 时 设置 fallbackToDestructiveMigration 来避免,但这会清除当前数据库的所有数据来重新创建。
如果修改了数据库的模式(修改了 RoomDatabase 的定义),但没有修改版本号,也会抛出一个异常。

public static void accessDb(Context context) {
    chinaDb = Room.databaseBuilder(context, ChinaDatabase.class, "china-room.db")
        .addMigrations(new Migration(1, 2) {
            @Override
            public void migrate(@NonNull SupportSQLiteDatabase database) {
                database.execSQL("insert into city (city_id, city_name) values ('4', '杭州')");
            }
        })
        .build();
}

onCreate 回调、migrate 回调在通过 Dao 执行数据操作时才会执行,所以默认必须在子线程执行。

RoomDatabase 是对 SQLiteOpenHelper 的封装。在 build 时,相当于 new 了一个 SQLiteOpenHelper,但没有调 SQLiteOpenHelper 的 getWritableDatabase 方法,即没有生成一个 SQLiteDatabase 对象。所以 SQLiteOpenHelper 的 onCreate、onUpgrade 方法不会执行,RoomDatabase 的 onCreate、migrate 回调也就没有执行。

二、表

2.1 定义

表的定义与数据的定义合到了一起。定义一个数据,就相当于定义了一个表。

@Entity(tableName = "city")
public class City {
    @PrimaryKey(autoGenerate = true)
    public int id;

    @ColumnInfo(name = "city_id")
    public String cityId;
    
    @ColumnInfo(name = "city_name")
    public String cityName;
}

2.2 操作

表的创建、修改、删除都只能在定义阶段进行,在编码时修改数据实体类、数据库类和它们的注解来实现。

@Database(entities = {City.class}, version = 1)
public abstract class ChinaDatabase extends RoomDatabase {
    public abstract CityDao cityDao();
}

也可以通过 usaDatabase.getOpenHelper().getWritableDatabase() 获得 SupportSQLiteDatabase 对象,或在 RoomDatabase build 时通过 onCreate、migrate 回调中的 SupportSQLiteDatabase 对象进行 db.execSQL("") 操作来修改表,但这会打乱所有的东西,产生异常。

三、数据

3.1 定义

如上节所述,与表的定义合在一起。

3.2 操作

Room 对数据的操作通过 Dao (data access objects)来实现。

Dao 可以是接口,也可以是抽象类。

@Dao
public interface CityDao {
    @Insert
    void insertCity(City city);

    @Delete
    void deleteCity(City city);

    @Delete
    void deleteAllCity(City...city);
    
    @Update
    void updateCity(City city);

    @Query("select * from city")
    List<City> getAllCity();

    @Query("select * from city where city_id = (:cityId)")
    City getCity(String cityId);
}

@Insert、@Delete、@Update 方法都可以选择返回一个数值,用来标识执行成功的条数。

@Query 中的 sql 语句会在编译时检测,如果不正确会编译失败。

在编译过后,Room 会自动生成 CityDao 的实现类 CityDao_Impl。

注意:sql 表名称是纯小写的,如果类是 UsaCity,sql 语句中应该是 usa_city。

四、运行

4.1 普通运行

Room 默认不支持在主线程进行数据访问,因为有可能阻塞线程。通过在 build 时调用 allowMainThreadQueries() 可去除这个限制。

Dao 的操作最终都是通过 getWritableDatabase.xxx() 来实现的。

//DbActivity.java
public void onCreate(Bundle savedInstanceState) {
	...
    new Thread(new Runnable() {
        @Override
        public void run() {
			SqliteBehavior.behave(DbActivity.this);
        }
    }).start();
}

//SqliteBehavior.java
public static void behave(Context context) {
    accessDb(context);

    CityDao cityDao = chinaDb.cityDao();
    cityDao.deleteAllCity(cityDao.getAllCity().toArray(new City[0]));
    Log.d(TAG, "after deleteAll: " + cityDao.getAllCity());
        
    cityDao.insertCity(new City("1", "北京"));
    cityDao.insertCity(new City("2", "上海"));
    cityDao.insertCity(new City("3", "广州"));
    Log.d(TAG, "after insert: " + cityDao.getAllCity());

    City city = cityDao.getCity("3");
    city.setCityName("深圳");
    cityDao.updateCity(city);
    Log.d(TAG, "after update: " + cityDao.getAllCity());

    cityDao.deleteCity(city);
    Log.d(TAG, "after delete: " + cityDao.getAllCity());
}

输出

D/database-room: after deleteAll: []
D/database-room: after insert: [City{id=4, cityId='1', cityName='北京'}, City{id=5, cityId='2', cityName='上海'}, City{id=6, cityId='3', cityName='广州'}]
D/database-room: after update: [City{id=4, cityId='1', cityName='北京'}, City{id=5, cityId='2', cityName='上海'}, City{id=6, cityId='3', cityName='深圳'}]
D/database-room: after delete: [City{id=4, cityId='1', cityName='北京'}, City{id=5, cityId='2', cityName='上海'}]

4.2 RxJava 运行

todo

五、SQLite 替换为 Room

将原有的 SQLite 替换为 Room,只需创建 Room 所需类: RoomDatabase、CityDao、City,替换即可。

注意点:

  • 给 RoomDatabase 的版本号 +1
    否则会报错:
java.lang.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.
  • 添加 Migration(old, old + 1)
    否则会报错,缺少 Migration。

  • 建表语句要完全一致,即字段的名称、约束都要完全一致
    Room 中数据的实体类中,int 这样的基本类型,在创表时,会添加 not null 约束。
    而如果之前的 SQLite 数据库的建表语句中没有 not null 约束,那迁移时就会报错:

java.lang.IllegalStateException: Migration didn't properly handle city(com.gdeer.gdtesthub.db.room.City).
 Expected:
TableInfo{name='city', columns={city_name=Column{name='city_name', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}, id=Column{name='id', type='INTEGER', affinity='3', notNull=true, primaryKeyPosition=1}, city_id=Column{name='city_id', type='TEXT', affinity='2', notNull=false, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}
 Found:
TableInfo{name='city', columns={city_name=Column{name='city_name', type='text', affinity='2', notNull=false, primaryKeyPosition=0}, id=Column{name='id', type='integer', affinity='3', notNull=false, primaryKeyPosition=1}, city_id=Column{name='city_id', type='text', affinity='2', notNull=false, primaryKeyPosition=0}}, foreignKeys=[], indices=[]}

可以看到 Expected 的 id 的 notNull 是 ture,Found 的 id 的 notNull 是 false。

要解决这个问题,将 Room 的 City 实体类的 id 改为 Integer 类型,这样建表时就不会添加 not null 约束了。

如果有的列需要 not null 约束,那给那个字段加上 @NonNULL 的注解就好。

六、db 文件覆盖升级

通过 db 文件覆盖升级时,不用修改 RoomDatabase 的版本号,在 build 之前替换掉文件即可。

修改版本号,添加 Migration,在 migrate 中覆盖反而有问题,会抛异常 SQLiteException: attempt to write a readonly database

七、其他

  • setJournalMode()

RoomDatabase 在 build 时可以通过 setJournalMode() 来设置读写模式。读写模式分为 TRUNCATEWRITE_AHEAD_LOGGINGAUTOMATIC。默认是 AUTOMATIC,它会判断手机版本号,16及以上会使用 WRITE_AHEAD_LOGGING,以下会使用 TRUNCATE。

当使用 TRUNCATE,数据库读写都不能并发。数据会实时写入 db。

当使用 WRITE_AHEAD_LOGGING,数据库读可并发,写不能并发。数据不会实时写入 db(可能在 wal 文件内)。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值