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://www.jianshu.com/p/ff8167c1d191/
*/
Flowable<User> getUser();
/**
* 将数据写入数据库中
* 如果数据已经存在则进行更新
* Completable 可以看作是 RxJava 的 Runnale 接口
* 但他只能调用 onComplete 和 onError 方法,不能进行 map、flatMap 等操作
* Completable:https://www.jianshu.com/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的详细教程