Room Persistence Library(官方介绍)
官方ORM(Object Relational Mapping)框架专题
Google官方推出的Android架构组件系列文章(六)Room持久化库
Room 的官方API 可以查看这里
介绍
Room是谷歌官方的数据库ORM(对象关系映射)框架,使用起来非常方便。
Room提供了一个SQLite之上的抽象层,使得在充分利用SQLite功能的前提下顺畅的访问数据库。
对于需要处理大量结构化数据的App来说,把这些数据做本地持久化会带来很大的好处。常见的用例是缓存重要数据块。这样当设备无法连网的时候,用户仍然可以浏览内容。而用户对内容做出的任何改动都在网络恢复的时候同步到服务端。
引入
1、项目build.gradle中添加如下代码仓库
allprojects {
repositories {
jcenter()
google()
}
}
2、app Module中引入
// Room依赖
implementation 'android.arch.persistence.room:runtime:1.1.0'
annotationProcessor "android.arch.persistence.room:compiler:1.1.0"
组成
Room中有三个主要的组件:
1、Database
数据库组件,底层连接的主要入口,主要作用:
- 创建database holder
- 使用注解定义实体类
- 实体类定义了从数据库中获取数据的对象(DAO)
这个被注解的类是一个继承RoomDatabase的抽象类。在运行时,可以通过调用Room.databaseBuilder() 或者 Room.inMemoryDatabaseBuilder()来得到它的实例。
2、Entity
实体类组件, 一个类表示数据库的一个表。
注意
- 1、你必须在Database类中的entities数组中引用这些entity类
- 2、entity中的每一个field都将被持久化到数据库,除非使用了@Ignore注解。
3、DAO
DAO查询组件,DAO(Data Access Object) 数据访问对象是一个面向对象的数据库接口。
DAO是Room的主要组件,负责定义查询(添加或者删除等)数据库的方法。
4、示意图
示例代码
1、User.java ---- 实体类组件(Entity)
-
建表:当一个类用@Entity注解并且被@Database注解中的entities属性所引用时(
@Database(entities = {User.class}, version = 1)
),Room就会在数据库中为那个entity创建一张表。 - 表名:Room默认把类名作为数据库的表名,自定义表名需要使用@Entity注解的tableName属性,@Entity(tableName = "users")。
- 建列:默认Room会为实体类中定义的每一个字段(field)都创建一个数据表列(column)。
- 列名:默认使用字段名作为列名,如果想指定列名,可以使用 @ColumnInfo(name = "your_name")。
- 持久化:要持久化一个字段(字段数据写入数据库),Room必须有获取它的渠道。你可以把字段写成public,也可以为它提供一个setter和getter。如果你使用setter和getter的方式,记住它们要基于Room的Java Bean规范。
- 忽略:如果一个实体类中有你不想持久化的字段,那么你可以使用@Ignore来注释它们。
- 主键:每个实体类Entity都必须至少定义一个field作为主键(primary key),主键自增需要使用@PrimaryKey的autoGenerate属性。
- 组合主键:需要使用@Entity注解的primaryKeys属性,比如@Entity(primaryKeys = {"firstName", "lastName"}),多个字段联合形成一个主键组合,保证主键的唯一性
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
@Ignore
Bitmap picture;
}
2、UserDao.java ---- DAO查询组件
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List<User> getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
3、AppDatabase.java ---- 数据库组件
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
4、获取database实例
获取database实例的时候应该保持单例模式,因为数据库的实例对内存的开销是比较大的,而且程序内一般也不需要多个database的实例。
AppDatabase db = Room.databaseBuilder(getApplicationContext(),AppDatabase.class, "database-name").build();
相关概念
1、索引(Indices )
为了提高查询的效率,可能给特定的字段建立索引。
要为一个entity添加索引,在@Entity注解中添加indices属性,列出你想放在索引或者组合索引中的字段。
代码示例:
@Entity(indices = {@Index("name"), @Index("last_name", "address")})
class User {
@PrimaryKey
public int id;
public String firstName;
public String address;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
2、唯一性(uniqueness)
指定某个字段或者几个字段的值必须是唯一的,比如用户名或手机号之类的账户唯一标识字段。
可以通过把@Index注解的unique属性设置为true来实现唯一性
@Entity(indices = {@Index(value = {"first_name", "last_name"},
unique = true)})
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
3、外键约束(Foreign Key)
一个表中的外键(Foreign Key) 指向另一个表中的主键(Primary Key),在更新和删除时起到约束的作用。比如,如果你想在删除主键表中的一条数据时可以同时删除外键约束表中相对应的数据,你可以在@ForeignKey注解中加上onDelete=CASCADE。
下面的代码就指定了Book表中的user_id字段为User表的外键,与User表的id字段一一对应,使用Entity的foreignKeys属性指定,写法如下:
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
class Book {
@PrimaryKey
public int bookId;
public String title;
@ColumnInfo(name = "user_id")
public int userId;
}
-
注意
替换冲突『 @Insert(onConfilict=REPLACE) 』不适用于外键约束,onConfilict不是单独的sql命令,可以理解为一组REMOVE和REPLACE的操作,请参见SQLite文档的ON_CONFLICT语句,onConfilict有如下五种冲突解决算法。
4、对象嵌套
就是一个实体类中嵌入另一个实体类,可以多层嵌套。比如你在User中嵌套Address,如果你使用@Embedded注解Address的话,那么User表中就拥有了Address的所有字段了。
为了防止多个实体嵌套造成字段重复,你可以通过设置prefix属性来保持每列的唯一性。Room会将提供的值添加到嵌入对象的每个列名的开头。
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
数据查询(DAO)
DAO抽象出了一种操作数据库的简便方法。下面介绍一下常见的查询方式。
1、新增、插入(Insert)
创建一个DAO方法并使用@Insert注解,Room就会在工作线程中将所有参数插入到数据库。
如果@Insert方法仅仅接收一个参数,那它可以返回一个long,表示插入项的rowId。如果参数是一个数组或集合,它会返回long []或List<Long>。
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);
@Insert
public void insertBothUsers(User user1, User user2);
@Insert
public void insertUsersAndFriends(User user, List<User> friends);
}
2、修改、更新(Update)
根据每个entity的主键作为更新的依据,此方法可以返回一个int值,指示数据库中更新的行数。
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
3、删除(Delete)
使用主键找到要删除的entity,此方法可以返回一个int值,指示数据库中被删除的行数。
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
4、查询(Query)
@Query(查询)是DAO
类中使用的主要注解。可以让你执行数据库读/写操作。每个@Query
方法都会在编译时验证,如果查询语句有问题,会发生编译错误而不是运行时故障。
Room还会检查查询的返回值,如果返回的对象字段名和查询结果的相应字段名不匹配,Room将以下面两种方式提醒你:
- 如果是某些字段名不匹配会给出警告。
- 如果没有匹配的字段名则会给出错误提示。
4-1、简单查询
@Dao
public interface MyDao {
@Query("SELECT * FROM user")
public User[] loadAllUsers();
}
4-2、条件查询 (传参)
注意查询方法的入参在查询语句中的写法“:minAge”,另外,在使用in操作符进行查询时,别忘记加上“()”哦!
@Dao
public interface MyDao {
//查询User表中年龄大于入参的数据集合
@Query("SELECT * FROM user WHERE age > :minAge")
public User[] loadAllUsersOlderThan(int minAge);
//查询User表中年龄介于入参之间的数据集合
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public User[] loadAllUsersBetweenAges(int minAge, int maxAge);
//查询User表中名字中入参关键字的数据集合
@Query("SELECT * FROM user WHERE first_name LIKE :search "
+ "OR last_name LIKE :search")
public List<User> findUserWithName(String search);
//查询User表中id符合入参集合中的数据集合
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List<User> loadAllByIds(int[] userIds);
}
4-3、部分查询
有时候我们仅仅需要的是数据表中的部分数据,这个时候我们可以指定DAO的查询方法只返回我们需要的字段,这样不仅节约资源而且查询更快。
方法是在查询语句中指定需要获取的字段,然后指定对应的实体类来获取查询的返回数据。
例如,User的实际字段有如下四个,但是我们只需要其中的first_name和last_name,那么我们可以重新定义一个实体类UserName,然后在查询方法中指定只查询first_name和last_name字段,并使用UserName实体来获取查询语句的返回数据。
注:这些“裁剪”的实体类也是可以使用@Embedded注解的。
@Entity(tableName = "users")
public class User {
@PrimaryKey(autoGenerate = true)
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
@Ignore
Bitmap picture;
}
public class UserName{
@ColumnInfo(name="first_name")
public String firstName;
@ColumnInfo(name="last_name")
public String lastName;
}
@Dao
public interface MyDao {
@Query("SELECT first_name, last_name FROM user")
public List<NameTuple> loadFullName();
}
4-4、原始查询 RawQuery
我们可以利用 RawQuery
进行原始SQL语句查询,示例代码:
@Dao
interface RawDao {
@RawQuery
User getUserViaQuery(SupportSQLiteQuery query);
}
SimpleSQLiteQuery query = new SimpleSQLiteQuery("SELECT * FROM User WHERE id = ? LIMIT 1", new Object[]{userId});
User user2 = rawDao.getUserViaQuery(query);
1、Room将根据函数的返回类型("User ")生成代码,如果未能通过正确的查询,将导致运行时异常。
2、RawQuery方法只能用于读取查询。对于写入查询,请使用RoomDatabase.getOpenHelper().getWritableDatabase()
5、可观察的查询(Observable queries)
5-1、Query
当执行查询的时候,你通常希望app的UI能自动在数据更新的时候更新。为此,在query方法中使用 LiveData 类型的返回值。当数据库变化的时候,Room会生成所有的必要代码来更新LiveData。
@Dao
public interface MyDao {
@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")
public LiveData<List<User>> loadAllUsersBetweenAges(int minAge, int maxAge);
}
使用步骤:
- 创建一个LiveData实例来保存某种类型的数据。 这通常在您的ViewModel类中完成。
public class UserViewModel extends ViewModel {
private LiveData<List<User>> mUsers;
public LiveData<List<User>> getUsers() {
if (mUsers== null) {
mUsers= MyDao.loadAllUsersBetweenAges(10,30);
}
return mUsers;
}
...
}
- 创建一个Observer对象,该对象定义onChanged()方法,该方法在LiveData对象的数据发生变化时回调, 通常是在UI控制器中创建Observer对象,比如Activity或Fragment。
public class UserActivity extends AppCompatActivity {
private UserViewModel mModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mModel = ViewModelProviders.of(this).get(UserViewModel.class);
final Observer<List<User>> userObserver = new Observer<List<User>>() {
@Override
public void onChanged(@Nullable List<User> users) {
//update the ui
}
};
mModel.getCurrentName().observe(this, userObserver);
}
}
- 使用observe()方法将Observer对象附加到LiveData对象。 observe()方法使用LifecycleOwner对象。 这将Observer对象订阅到LiveData对象,以便通知其更改。 您通常将Observer对象附加到UI控制器中,比如Activity或Fragment。
5-2、RawQuery
RawQuery方法可以返回可观察的类型,但您需要使用注释中的observedEntities()字段指定在查询中访问哪些表。
代码示例:
@Dao
interface RawDao {
@RawQuery(observedEntities = User.class)
LiveData<List<User>> getUsers(SupportSQLiteQuery query);
}
LiveData<List<User>> liveUsers = rawDao.getUsers(
new SimpleSQLiteQuery("SELECT * FROM User ORDER BY name DESC"));
Rxjava
Room还可以让你定义的查询返回RxJava2的Publisher和Flowable对象。要使用这个功能,在Gradle dependencies中添加android.arch.persistence.room:rxjava2。然后你就可以返回RxJava2中定义的对象类型了,如下面的代码所示:
@Dao
public interface MyDao {
@Query("SELECT * from user where id = :id LIMIT 1")
public Flowable<User> loadUserById(int id);
}
Cursor
如果你的app需要直接获得返回的行,你可以在查询中返回Cursor对象。但是非常不鼓励使用Cursor ,因为它无法保证行是否存在,或者行包含什么值。
public interface MyDao {
@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")
public Cursor loadRawUsersOlderThan(int minAge);
}
多表查询
一些查询可能要求查询多张表来计算结果。Room允许你书写任何查询,因此表连接(join)也是可以的。
而且如果响应是一个可观察的数据类型,比如Flowable或者LiveData,Room将观察查询中涉及到的所有表,检测出所有的无效表。
下面的代码演示了如何执行一个表连接查询来查出借阅图书的user与被借出图书之间的信息。
逻辑:入参userName ---- (user.name LIKE :userName) ----> user ---- (user.id = loab.userid) ----> loab ---- (loan.book_id = book.id) ----> book
语句:book <---- (loan.book_id = book.id) ---- loab <---- (user.id = loab.userid) ---- user <---- (user.name LIKE :userName) ---- userName
@Dao
public interface MyDao {
@Query("SELECT * FROM book "
+ "INNER JOIN loan ON loan.book_id = book.id "
+ "INNER JOIN user ON user.id = loan.user_id "
+ "WHERE user.name LIKE :userName")
public List<Book> findBooksBorrowedByNameSync(String userName);
}
- INNER JOIN:内连接,显示左表和右表符合连接条件的记录
- JOIN: 如果表中有至少一个匹配,则返回行
- LEFT JOIN: 即使右表中没有匹配,也从左表返回所有的行
- RIGHT JOIN: 即使左表中没有匹配,也从右表返回所有的行
- FULL JOIN: 只要其中一个表中存在匹配,就返回行
类型转换
1、Room对Java的基本数据类型以及其包装类型都提供了支持
2、但是有时候你可能使用了一个自定义的数据类型,并且你想将此类型的数据存储到数据库表中的字段里。
为了实现自定义数据类型的转换,你需要一个类型转换器TypeConverter,它将负责处理自定义数据类和Room可以保存的已知类型之间的转换。
比如,如果我们想要保存Date实例,那么第一步
- 写一个TypeConverter类,实现Date类型和Long类型的数据转换
public class Converters {
@TypeConverter
public static Date fromTimestamp(Long value) {
return value == null ? null : new Date(value);
}
@TypeConverter
public static Long dateToTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}
可以看到上面的转换类,提供了两个方法以实现Date类型和Long类型的数据相互转换。
-
使用TypeConverter类,实现持久化Date类型的数据
这里就需要把这个转换类Converters
添加到我们的数据库组件AppDatabase
中了
@Database(entities = {User.java}, version = 1)
@TypeConverters({Converter.class})
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
数据库升级
-
基本使用
实现就是在对应的Database中通过Migration进行升级,使用的方式是:
1、利用Migration 构建数据库升级语句
//数据库升级
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
}
};
2、在数据库的构造方法中通过addMigrations()方法传入对应的Migration
public synchronized static AppDatabase getInstance(byte[] passphrase) {
if (INSTANCE == null) {
synchronized (AppDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room
.databaseBuilder(mContext.getApplicationContext(), AppDatabase.class, sDbPath)
.openHelperFactory(new HelperFactory(passphrase))
.allowMainThreadQueries()
.addMigrations(migration_1_2)
.build();
}
}
}
return INSTANCE;
}
3、数据库版本(version )+1
@Database(entities = {MarkBook.class}, version = 2, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase {
....
总结:就是把数据库的变化通过SQL语句传到数据库的构造方法中。
- 常用的几种数据库升级
新增表
//数据库升级
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE IF NOT EXISTS SYMBOL_SETTING (_id INTEGER primary key NOT NULL, dbName TEXT, symbolJson TEXT)");
}
};
增加字段
//数据库升级
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE SURVEY_RECORD ADD STATUS INTEGER DEFAULT 0");
}
};
数据库多次升级
//数据库升级,SURVEY_RECORD新增SJBZ、SZFHZZZB字段
private static final Migration migration_2_3 = new Migration(2, 3) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE SURVEY_RECORD ADD SJBZ TEXT");
database.execSQL("ALTER TABLE SURVEY_RECORD ADD SZFHZZZB TEXT");
}
};
//数据库升级
private static final Migration migration_1_2 = new Migration(1, 2) {
@Override
public void migrate(@NonNull SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE SURVEY_RECORD ADD STATUS INTEGER DEFAULT 0");
}
};
public synchronized static TaskDatabase getInstance(byte[] passphrase) {
if (INSTANCE == null) {
synchronized (TaskDatabase.class) {
if (INSTANCE == null) {
INSTANCE = Room
.databaseBuilder(mContext.getApplicationContext(), TaskDatabase.class, sDbPath)
.openHelperFactory(new HelperFactory(passphrase))
.allowMainThreadQueries()
.addMigrations(migration_1_2,migration_2_3)
.build();
}
}
}
return (INSTANCE);
}
备注:数据库升级的时候最好文字说明一下这次升级了什么,不要因为有SQL语句就不写注解了,这样不好。
以普通的方式打开SQLite数据库
有时候我们需要以普通的方式打开SQLite数据库,以方便我们使用原生的更新、删除、查询语句
TaskDatabase database = TaskDatabase.getInstance(Encryption.getInstance().getDBEncryptionPassword());
SupportSQLiteDatabase db = database.getOpenHelper().getWritableDatabase();
db.execSQL("");