irpas技术客

Android jetpack组件-Room_JeeZhong_android jetpack room

网络投稿 741

Room是什么?

Room 是Google为了简化旧式的SQLite操作专门提供的一个覆盖SQLite抽象层框架库 Room也是一个ORM框架,它在SQLite上提供了一个抽象层,屏蔽了部分底层的细节,使用对象对数据库进行操作,进行CRUD就像对象调用方法一样的简单。 Room 是一个对象关系映射(ORM)库。可以很容易将 SQLite 表数据转换为 Java 对象。Room 在编译时检查 SQLite 语句。

Room 为 SQLite 提供一个抽象层,以便在充分利用 SQLite 的同时,可以流畅地进行数据库访问。

作用: 实现SQLite的增、删、查、改功能。 特点: 1.使用简单(类似于Retrofit库),通过注解的方式实现相关功能。 2.拥有SQLite的所有操作功能(数据库表的所有操作、版本升级....)

Room 包含 3 个主要组件: Room Database 数据库:底层连接的主要接入点,创建数据库就靠它了。 Data Access Objects DAO:在DAO中会有一系列对数据库进行CRUD的方法声明 Entity 实体类:是对象与数据表的对应表现,设计实体类,并最后转化为对应的数据表

主要角色说明 ? Entities : 实体类,表示数据库表的数据。 ? Dao : 数据操作接口,在DAO中会有一系列对数据库进行CRUD的方法声明。 ? Database : 数据库持有者 & 数据库版本管理者。 ? Room : 数据库的创建者 & 负责数据库版本更新的具体实现者

注释说明

1.Bean(实体)

? @Entity : 数据表的实体类。 ? @PrimaryKey : 每一个实体类都需要一个唯一的标识。 ? @ColumnInfo : 数据表中字段名称。 ? @Ignore : 标注不需要添加到数据表中的属性。 ? @Embedded : 实体类中引用其他实体类。 ? @ForeignKey : 外键约束。

//有些时候, 数据库中的某些域或几组域必须是唯一的. 你可以通过将注解@Index的unique属性设置为true, 强制完成唯一的属性 @Entity(tableName = "user", indices = {@Index(value = {"name"}, unique = true)}) @NonNull// 表示参数,成员变量或者方法返回值从不为null //@ForeignKey注解定义它和实体User的关系 /* * 外键非常强大, 因为它允许你指定做什么操作, 在引用实体更新的时候. 比如, 你可以告诉SQLite为用户删除所有的书, * 在相应的User实例被删除时, 而该User被Book通过在@ForeignKey注解里面声明onDelete = CASCADE而关联. * 备注: SQLite将@Insert(onConflict = REPLACE)作为REMOVE和REPLACE的集合来操作, 而非单独的UPDATE操作. 这个取代冲突值的方法能够影响你的外键约束. * */ @Entity(foreignKeys = @ForeignKey(entity = User.class, parentColumns = "id", childColumns = "user_id"))

Room会利用@Entity注解的类的所有字段来创建表的列,如果某些字段不希望存储的话,使用@Ignore注解该字段即可

默认情况下,Room使用类名作为表名,使用字段名作为列名。我们可以通过@Entity的tableName属性定义自己的表名,通过@ColumnInfo的name属性定义自己的列名。

@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "_id") private int id;

Room 默认使用类名作为数据库的 Table 名称。可以通过 @Entity 的 tableName 属性设置 Table 的名称。(注意:在 SQLite 中,Table 名称是不区分大小写的。)

Room 使用字段(Filed)名称作为在数据库中的默认列名。可以通过给 Filed 添加 @ColumnInfo 注解设置列名。

主键 @PrimaryKey 每一个实体至少定义一个字段作为主键。可以将@PrimaryKey的autoGenerate属性设置为true来设置自动id。如果实体有一个复合的主键,可以使用 @Entity的primaryKeys属性来指定主键。

每个 Entity 必须设置至少一个 Field 作为主键(primary key)。即使只有 1 个 Field,也需要将其设置为主键。 有两种方法设置主键: 1.使用注解 @PrimaryKey,可以用来设置单个主键。 如果需要 Room 自动分配 IDs 给 Entity,可以设置 @PrimaryKey 的 autoGenerate 属性 2.使用注解 @Entity 的 primaryKeys 属性,可以用来设置单个主键和复合主键。

设置忽略字段(Ignore fields) @Ignore 默认情况下,Room 为 Entity 中每个 Field 创建一列。如果在 Entity 中存在不需要持久化的 Field,可以给它们添加 @Ignore 注解。 如果子类不需要持久化父类中的 Field,使用 @Entity 的 ignoredColumns 属性更为方便。 索引和唯一性 为数据库添加索引可以加快数据的查询。在Room中可以通过@Entity的indices属性添加索引。 有时候,需要确保某个字段或者多个字段形成的组唯一。可以通过将@Index的unique属性设置为true,来确保唯一性。在下面的例子中,防止first_name和last_name这两列同时具有相同的数据 关系 SQLite是关系型数据库,你可以指定不同对象之间的关系。尽管大多数ORM类库允许对象之间互相引用,但Room明确禁止这一点。 尽管不能使用直接关系,Room仍然两个实体之间定义外键

例如,有另外一个实体Book,你可以使用@ForeignKey注解定义和User之间的关系。 外键非常有用,因为当引用的实体发生改变时,你可以指定如何处理。例如,如果@ForeignKey的onDelete属性值为CASCADE,如果删除user,所有具有相同userId的book会被全部删除。 嵌套对象 Room提供了一个注解@Embedded,允许在一个实体中嵌入另外一个实体,创建的表使用的是当前实体和嵌入实体的所有字段,所以我们可以修改上面的User实体 当一个类中嵌套多个类,并且这些类具有相同的字段,则需要调用@Embedded的属性prefix 添加一个前缀,生成的列名为前缀+列名

2.Dao(数据库操作类) DAO(Data access object)

在 Room 持久化库中,使用数据访问对象(data access objects, DAOs)访问 App 的数据

DAO 可以是接口(interface),也可以是抽象类(abstract class)。如果是一个抽象类,可以有一个构造函数,其只接收一个 RoomDatabase 参数。在编译时,Room 为每个 DAO 创建具体实现。

注意:除非在构造器上调用 allowMainThreadQueries(),否则 Room 不支持在主线程上进行数据库访问,因为它可能会长时间锁定 UI。不过异步查询(返回 LiveData 或 Flowable 实例的查询)不受此规则约束,因为它们在需要时会在后台线程进行异步查询。

? @Dao : 标注数据库操作的类。 ? @Query : 包含所有Sqlite语句操作。 ? @Insert : 标注数据库的插入操作。 ? @Delete : 标注数据库的删除操作。 ? @Update : 标注数据库的更新操作。

数据访问对象(DAOs)

@Query 查询接受的参数是一个字符串,所以像删除或者更新我们也可以使用 @Query 注解来使用SQL语句来直接执行

@Query("delete from user where userId = :id ") fun deleteUserById(id:Long) @Query("update user set userName = :updateName where userID = :id") fun update(id: Long, updateName: String)

插入 当我们创建一个Dao方法,并使用@Insert注解,Room将把所有的参数在一次事物中插入到数据库中。 onConflict用来指定当发生冲突是的策略。比如将@Index的unique属性设置为true,当产生冲突时,默认情况下为OnConflictStrategy.ABORT会导致崩溃,这里设置为OnConflictStrategy.REPLACE,当发生冲突时替换老数据。关于其他的冲突策略可以阅读SQL As Understood By SQLite进行了解。

除了可以将@Insert注解的方法返回值设置为void外,还可以将方法的返回值设置为long, Long, Long[] 或者 List。如果参数是单个实体,返回long或者Long,该值是插入新条目的rowId。如果参数是集合或者多个参数时,则返回Long[]或者List

更新 使用@Update注解方法,可以使用参数实体的值更新主键值和参数实体的主键相同的行。 @Update注解的方法还可以返回int,表示受影响的行数。

删除 使用@Delete注解方法,可以删除主键值和参数实体的主键相同的行

查询(Query)

@Query 是 DAO 类中的重要注解。它允许在数据库上执行读写操作。每个 @Query 方法都是在编译时验证的;因此,如果存在查询问题,将出现编译错误而不是运行时错误

在编译时,Room 还验证查询的返回值,如果返回对象中的字段名称与查询中的相应列名称不匹配,将通过以下两种方式之一告知:(在下面 3.4.3 返回列的子集 会提到) ? 如果仅仅部分 Field 名称匹配,将显示 Warning。 ? 如果没有 Field 名称匹配,将显示 Error。

简单查询

@Query的值为SQL语句,可以被SQLite执行。@Query支持查询语句,删除语句和更新语句,不支持插入语句。 Room会在编译时进行检查,当代码中包含语法错误,或者表不存在,Room将在编译时出现错误信息

带参数的查询 大多数情况下,需要将参数传递到查询中以执行筛选操作,例如仅需要显示大于某一年龄的 User。这时,我们可以使用方法参数。 在编译时,Room 使用 minAge 方法参数匹配 :minAge 绑定参数。如果存在匹配错误,将出现编译错误。

还可以在查询中传递多个参数或者多次引用它们。 在查询时,传递的参数还可以是一个集合。Room 知道参数何时是一个集合,并根据提供的参数数量在运行时自动展开。

传入参数 如果我们想获取指定id的用户,该怎么办。@Query的value中支持添加绑定参数,该参数必须找到与之匹配的方法参数,并取得该方法参数的值。

传入参数 如果我们想获取指定id的用户,该怎么办。@Query的value中支持添加绑定参数,该参数必须找到与之匹配的方法参数,并取得该方法参数的值。 在这个例子中绑定参数:minAge与方法参数minAge相匹配 此外,Room还允许传入一个参数集合 返回列的子集 多数情况下,你只需要获取实体的少数几个字段。例如,你的ui可能只展示用户的名和姓,而不是每个用户的详细信息。通过只获取需要的列,可以节省资源,并且查询速度更快。

只要可以将查询的结果映射到返回的对象上,Room允许返回任何java对象。例如,可以创建如下java对象来获取用户的名和姓。

原文链接

只要结果列集合可以映射到返回的对象中,Room 允许返回任何基于 Java 的对象。例如,可以创建以下普通的 Java 对象(plain old Java-based object, POJO)来获取用户的 first name 和 last name: 注意:POJO 也可是使用 @Embedded 注解。

可观察的查询 如果希望 App 的 UI 在数据发生变化时自动更新 UI,可以在查询方法中返回一个 LiveData 类型的值。Room 会产生所有必须的代码,用于在数据库发生变化时更新这个 LivaData 对象

带通配符的模糊查找查询 两种做法,一种就是用双竖杠拼接的,还有一种就是在传参的时候,把%%给拼接好 @Query(“SELECT * FROM tb_use WHERE Name LIKE ‘%’ || :name” || ‘%’)

RxJava 的响应式查询

Room 支持返回一下 RxJava2 类型的值: ? @Query 方法:支持返回 Publisher、Flowable 和 Observable 类型的值。 ? @Insert、@Update 和 @Delete 方法:Room 2.1.0 及以上版本支持返回 Completable、Single 和 Maybe 类型的值。 需要在 App 的 build.gradle 文件中添加对最新 rxjava2 版本的依赖:

Room也可以返回RxJava2的Publisher和Flowable对象。要使用这个功能需要在gradle中添加android.arch.persistence.room:rxjava2

直接返回Cursor Room还可以直接返回Cursor对象

查询多个表

/** * 为了简便,我们只在表中存入1个用户信息 * 这个查询语句可以获得 所有 User 但我们只需要第一个即可 * @return */ @Query("SELECT * FROM Users LIMIT 1") Flowable<User> getUser(); /** * 想数据库中插入一条 User 对象 * 若数据库中已存在,则将其替换 * @param user * @return */ @Insert(onConflict = OnConflictStrategy.REPLACE) Completable insertUser(User user); /** * 清空所有数据 */ @Query("DELETE FROM Users") void deleteAllUsers(); /** * 从数据库中读取信息 * 由于读取速率可能 远大于 观察者处理速率,故使用背压 Flowable 模式 * Flowable:https://·/p/ff8167c1d191/ */ Flowable<User> getUser(); /** * 将数据写入数据库中 * 如果数据已经存在则进行更新 * Completable 可以看作是 RxJava 的 Runnale 接口 * 但他只能调用 onComplete 和 onError 方法,不能进行 map、flatMap 等操作 * Completable:https://·/p/45309538ad94 */ Completable insertOrUpdateUser(User user);

原文链接

3.Database(数据库持久化)

继承数据库的创建和升级在这个类里面处理 ? @Database : 标注数据库持久化的类

defaultConfig { ... //指定room.schemaLocation生成的文件路径 输出sql语句,方便写migration javaCompileOptions { ... annotationProcessorOptions { arguments += ["room.schemaLocation": "$projectDir/schemas".toString()] } } }

实例 代码:

//管理数据创建和升级的类 @Database(entities = {RecordMarksBean.class},version = 2) public abstract class RecordMarksBeanRoomDatabase extends RoomDatabase { /** * 数据库名 */ private static final String DB_NAME = "room_sound_recorder.db"; public abstract RecordMarksBeanDao getRecordMarksBeanDao(); private static volatile RecordMarksBeanRoomDatabase INSTANCE; public static RecordMarksBeanRoomDatabase getDatabase(final Context context) { if (INSTANCE == null) { synchronized (RecordMarksBeanRoomDatabase.class) { if (INSTANCE == null) { // Create database here //用上下文context中创建RoomDatabase对象,并将数据库全名为"word_database"。 INSTANCE = Room.databaseBuilder(context.getApplicationContext(), RecordMarksBeanRoomDatabase.class, DB_NAME) .addMigrations(MIGRATION_1_2) .build(); } } } return INSTANCE; } /** * 数据库版本升级 Migration12 升级到第二个版本 */ static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { // //数据库迁移,新建一个表 // database.execSQL("CREATE TABLE room_mark_table (_id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,"+ // "file_id INTEGER,filepath TEXT,marks TEXT,uripath TEXT)"); // 为旧表添加新的字段 database.execSQL("ALTER TABLE room_mark_table " + " ADD COLUMN volumelist TEXT"); //在表device_init中添加TEXT字段mimc // database.execSQL("ALTER TABLE mark_table" // + " ADD COLUMN uripath TEXT"); //将原来的表的数据复制到新表中 // database.execSQL("INSERT INTO room_mark_table (_id,file_id,filepath,marks)" + // "SELECT _id,file_id,filepath,marks FROM mark_table"); //删除原来的数据表 // database.execSQL("DROP TABLE mark_table"); //重名名新的表为旧的数据表 // database.execSQL("ALTER TABLE mark_table_temp RENAME to mark_table"); // private static final String CREATE_TABLE = "create table " + SOUNDRECORDER_MARKS_TABLE + " (" + // " " + _ID + " Integer primary key autoincrement, " + FILE_ID + " integer, " // + FILEPATH + " text, " + MARKS + " text)"; } }; }

使用类型转换器 Room支持字符串和基本数据类型以及他们的包装类,但是如果不是基本数据类型,该如何存储呢?比如我们的User对象有个Date类型的字段birthday,我们该如何存储。Room提供了@TypeConverter可以将不可存储的类型转换为Room可以存储的类型。 上面的例子定义了两个方法,Room可以调用dateToTimestamp方法将Date转化为Long类型进行存储,也可以在查询的时候将获取的Long转换为Date对象。

为了让Room调用该转换器,使用@TypeConverters注解将转换器添加到AppDatabase上。

数据库升级 在app发布以后,我们可能会新增表或者修改原来表的结构,这就需要升级数据库。Room提供了 Migration 类用于迁移数据库,每一个 Migration 需要在构造函数里指定开始版本和结束版本。在运行时,Room会按照提供版本的顺序顺序执行每个Migration的migrate()方法,将数据库升级到最新的版本。

注意:为了使迁移逻辑正常执行,请使用完整查询,不要使用表示查询的常量。

基本使用

1.Lib引用

module 的 build.gradle 中添加以下依赖

implementation 'android.arch.persistence.room:runtime:2.2.3' implementation "androidx.room:room-rxjava2:2.2.5" annotationProcessor 'android.arch.persistence.room:compiler:2.2.3' annotationProcessor 'android.arch.persistence.room:rxjava'

前面的两句是必须的,后面的部分为可选的。点击 这里 可以查看最新依赖版本号和依赖声明方法。

Room 组件

Room 有 3 个主要的组件 从上图的结构上看,至少Room Database、DAO和Entity都需要我们进行定义。官网的文档也指出,我们需要创建这三个文件,才能正式的使用Room。 Room Database:定义一个抽象类,并继承Room Database DAO:定义一个接口类 Entity:普通Java Bean类 ? Database:包含数据库持有者,并作为与 App 持久关联数据的底层连接的主要访问点。 用 @Database 注解的类应满足以下条件: ? 是一个继承至 RoomDatabase 的抽象类。 ? 在注解中包含与数据库相关联的实体列表。 ? 包含一个具有 0 个参数的抽象方法,并返回用 @Dao 注解的类。 在运行时,您可以通过调用 Room.databaseBuilder() 或 Room.inMemoryDatabaseBuilder() 获取 Database 实例。 ? Entity:表示数据库内的表(Table)。 ? DAO:包含用于访问数据库的方法。

Room 各组件间关系 Room 的大致使用方法如下: ? App 通过 Room 的 Database 获取与数据库相关的数据库访问对象(DAO)。 ? 然后,App 使用 DAO 从数据库中获取 Entity,并且将 Entity 的变化保存到数据库中。 ? 最后,APP 使用 Entity 获取和设置数据库中表的数据 room分为三部分: Entity:数据库实体,系统根据Entity类创建数据库,里面规定了PrimaryKey,列名、表名等数据库必备设定 Dao:Database access object:定义了一些操作数据库的操作,比如说增删改查 Database:可以认为是完整的数据库,完整的数据库包括数据库信息和数据库操作,也就是Entity和Dao

值得注意的是,如果Entity()的参数为空,系统在创建数据库时,会把类名作为数据库的表名,如果要自定义表名,可以直接在Entity()里输入参数: @Entity(tableName = “yourTableName”) 数据库的元素 主键:每一个实体至少定义一个字段作为主键。可以将@PrimaryKey的autoGenerate属性设置为true来设置自动id。如果实体有一个复合的主键,可以使用 @Entity的primaryKeys属性来指定主键。

创建Entity类 创建Dao类 创建database类

我们通过@Database()来标记这个类为database类,在它的参数中我们可以定义: ? entities:传入所有Entity的class对象; ? version:数据库版本号。 ? exportSchema:设置是否导出数据库schema,默认为true,需要在build.gradle中设置: 当数据库发生改变时,数据库版本号会接着改变,以便更好的进行备份恢复,这里我们用不到,就随便设计一个值

要求: ? 必须是abstract类而且的extends RoomDatabase。 ? 必须在类头的注释中包含与数据库关联的实体列表(Entity对应的类)。 ? 包含一个具有0个参数的抽象方法,并返回用@Dao注解的类。

使用数据库 我们终于能够操作我们的数据库了。但是所有的操作必须在后台线程中完成。你可以通过使用AsyncTask,Thread,Handler,RxJava或其它方式来完成

数据库升级

使用Room引用复杂数据 Room提供了功能支持基数数据类型和包装类型之间的转变, 但是并不允许实体间的对象引用

使用类型转换器

有时候, 应用需要使用自定义数据类型, 该数据类型的值将保存在数据库列中. 要添加这种自定义类 接下来, 添加@TypeConverters注解到AppDatabbase类上, 之后Room就能够在AppDatabase中定义的每一个实体和DAO上使用这个转换器.

//DAO: 包含用于访问数据库的方法. @Dao public interface UserDao { // OnConflictStrategy.REPLACE表示如果已经有数据,那么就覆盖掉 //数据的判断通过主键进行匹配,也就是uid,非整个user对象 //返回Long数据表示,插入条目的主键值(id) @Query("SELECT * FROM user") List<User> getAllUsers(); // LiveData是可以被观察到的数据持有类。它里面缓存或持有了最新的数据。当数据改变时会通知它的观察者。 // LiveData是可以感知生命周期的。UI组件只是观察相关数据,不会停止或恢复观察。 // LiveData自动管理所有这些,因为它在观察时意识到相关的生命周期状态变化。 @Query("SELECT * FROM user") LiveData<List<User>> getAllUser(); @Query("SELECT * FROM user WHERE id=:id") User getUser(int id); @Query("SELECT * FROM user WHERE name=:name") User getUser(String name); @Insert(onConflict = OnConflictStrategy.REPLACE) List<Long> insert(User... users); @Insert(onConflict = OnConflictStrategy.REPLACE) Long insert(User user); @Insert(onConflict = OnConflictStrategy.REPLACE) List<Long> insert(List<User> userLists); @Update int update(User... users); @Update() int updateAll(User... user); @Update() int updateAll(List<User> user); @Delete int delete(User user); @Delete int deleteAll(List<User> users); @Delete int deleteAll(User... users); //返回RxJava2中的Publisher和Flowable. @Query("SELECT * from user where name = :name LIMIT 1") Flowable<User> getUserByName(String name); //多表查询 @Query("SELECT * FROM book " + "INNER JOIN user ON user.id = book.user_id " + "WHERE user.id = :userName") List<Book> findBooksByUserName(String userName); }

下面是 Android Room 的官方介绍文档:

Room Persistence LibraryRoom 库的简单介绍)

Save data in a local database using Room(Room 的使用指南)

Android Room with a View - Java(Room 的使用实例)

使用ROOM无法查看到数据库表结构原因分析

使用room生成的数据库文件有三个:.db文件、.db-shm文件、.db-wal文件。

db-wal:从3.7.0版本开始,SQLite支持一种新的事务控制机制,称为“写前日志”或“WAL”。当数据库处于WAL模式时,到该数据库的所有连接都必须使用WAL。特定的数据库将使用回滚日志或WAL,但不能同时使用两者。WAL始终位于与数据库文件相同的目录中,并且具有与数据库文件相同的名称,但是附加了字符串“-wal”。

db-shm:从概念上讲,wal-index是共享内存,尽管当前的VFS实现为wal-index使用一个映射文件。映射文件位于与数据库相同的目录中,并具有与数据库相同的名称,后面附加了“-shm”后缀。因为WAL索引是共享内存,所以当客户机位于不同的机器上时,SQLite不支持网络文件系统上的journal_mode=WAL。数据库的所有用户必须能够共享相同的内存

所有以当要打开对应的数据库查看表结构的时候要同时把.db、.db-shm、.db-wal三个文件放在同一文件夹下,然后再数据库查看软件中导入对应的.db文件就可以查看数据库

可参考资料: Android Jetpack Room的详细教程


1.本站遵循行业规范,任何转载的稿件都会明确标注作者和来源;2.本站的原创文章,会注明原创字样,如未注明都非原创,如有侵权请联系删除!;3.作者投稿可能会经我们编辑修改或补充;4.本站不提供任何储存功能只提供收集或者投稿人的网盘链接。

标签: #Android #Jetpack #room #Room是什么Room #是一个对象关系映射ORM库 #可以很容易将