手记

Android官方ORM框架ROOM

而对于Android开发者来说,这次大会上Kotlin被Google钦点成为成为Android官方开发语言无不是一个重磅炸弹,当看到这个消息的时候我也是激动的不行!!我相信用过kotlin写过一段时间Android的人一定会和我有相同的感受!!!

但是除了Kotlin这个Android新的御用语言外,还有更激动人心的的部分。

Android Architecture Components

A new collection of libraries that help you design robust, testable, and maintainable >apps. Start with classes for managing your UI component lifecycle and handling data >persistence.

Now available in preview!

一款可以帮助您设计健壮,可测试和可维护的App的工具库集合。 从管理UI组件生命周期和处理数据持久化的类开始使用。
(英语一般,翻译的不好,轻喷:p)

如果你已经激动难耐,忍不住想快速一览Android Architecture Components,请点击链接前往。

这是一套从Model持久化(Room ORM),到Repository模式,到ViewModel的响应式渲染(LiveData),再到Activity/Fragment生命周期处理的整体架构组件。

并且,Architecture Components的实现广泛的采用了『编译时』注解,同时这次大招还同时包含了官方的ORM库-Room

Architecture Components 目前主要由以下几个部分组成:

  • Lifecycles

  • LiveData

  • ViewModel

  • Room Persistence Library

Room

由于篇幅有限,这里主要介绍一下Room,说起Android的ORM,那一定少不了DBFlow(一款采用编译时注解的高性能Android ORM库)。

从下图,我们可以看出,Room和DBFlow一样,都采用了编译时注解在编译时解析处理注解并成相关的ORM代码。

QQ20170518-201014.png-31kB

QQ20170518-201113.png-65.1kB

目前,采用编译时注解的库我相信大家都司空见惯了,比如Dagger2,ButterKnife等,官方ORM采用编译时注解来避免反射的性能损耗也并不让人意外,我个人目前倒是是特别好奇Room相较于DBFlow孰强孰弱了:p。

DBFlow简单对比

Room和DBFlow在设计上还是有所不同,DBFlow采用了ActiveRecord的模式。
在定义Model(Entity)的时候我们需要继承一个基类,继承了BaseModel,同时也有就有增删改查的能力:


//DBFlow//User.java@Table(database = AppDatabase.class, name = "user")public class User extends BaseModel {@PrimaryKey@Column(name = "id")private int uid;@Column(name = "first_name")private String firstName;@Column(name = "last_name")private String lastName;}//AppDatabase.java@Database(name = AppDatabase.DATABASE_NAME, version = AppDatabase.DATABASE_VERSION)public class AppDatabase {static final String DATABASE_NAME = "basic_db";static final int DATABASE_VERSION = 1;}


而Room则采用了Dao模式,本身不继承基类


//Room//User.java@Entity(tableName = "user")public class User {@PrimaryKeyprivate int uid;@ColumnInfo(name = "first_name")private String firstName;@ColumnInfo(name = "last_name")private String lastName;// Getters and setters are ignored for brevity,// but they're required for Room to work.}//UserDao.java@Daopublic 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);@Insertvoid insertAll(User... users);@Deletevoid delete(User user);}//AppDatabase.java@Database(entities = {User.class}, version = 1)@TypeConverters(DateConverter.class)public abstract class AppDatabase extends RoomDatabase {static final String DATABASE_NAME = "basic_db";public abstract UserDao userDao();}


注意到@Query("SELECT * FROM user")吗,Room支持注解级别的SQL,我只能说很灵活很强大。

既然是ORM,那么对象关系映射必不可少。


@Entity(foreignKeys = @ForeignKey(entity = User.class,parentColumns = "id",childColumns = "user_id"))class Book {@PrimaryKeypublic int bookId;public String title;@ColumnInfo(name = "user_id")public int userId;}


增删查改

除了@Query注解,还支持@Insert,@Update,@Delete


@Daopublic interface MyDao {@Insertpublic void insertUser(User user);@Deletevoid delete(User user);@Updatepublic void updateUsers(User... users);}


嵌套对象

嵌套对象也是可以的。


class Address {public String street;public String state;public String city;@ColumnInfo(name = "post_code")public int postCode;}@Entityclass User {@PrimaryKeypublic int id;public String firstName;@Embeddedpublic Address address;}


@Query传参


@Daopublic interface MyDao {//一个参数@Query("SELECT * FROM user WHERE age > :minAge")public User[] loadAllUsersOlderThan(int minAge);//两个参数@Query("SELECT * FROM user WHERE age BETWEEN :minAge AND :maxAge")public User[] loadAllUsersBetweenAges(int minAge, int maxAge);//集合@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")public List<NameTuple> loadUsersFromRegions(List<String> regions);}

获取列的子集(POJO子集)

有时候我们可能只需要部分字段,Room提供子集查询功能:
首先创建一个POJO类


public class NameTuple {@ColumnInfo(name="first_name")public String firstName;@ColumnInfo(name="last_name")public String lastName;}


现在,你可以在查询方法中使用这个POJO


@Daopublic interface MyDao {@Query("SELECT first_name, last_name FROM user")public List<NameTuple> loadFullName();}


响应式查询

使用Room我们可以采用响应式的方式获取数据,Room支持返回官方的LiveData或者Flowable(RxJava2)类型的对象


@Daopublic interface MyDao {//LiveData@Query("SELECT first_name, last_name FROM user WHERE region IN (:regions)")public LiveData<List<User>> loadUsersFromRegionsSync(List<String> regions);//RxJava2@Query("SELECT * from user where id = :id LIMIT 1")public Flowable<User> loadUserById(int id);}


Cursor查询

如果你更喜欢使用Cursor,Room也能满足你:)


@Daopublic interface MyDao {@Query("SELECT * FROM user WHERE age > :minAge LIMIT 5")public Cursor loadRawUsersOlderThan(int minAge);}


多表查询


@Daopublic 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);}

你也可以从这些查询返回POJO。例如,您可以编写一个加载用户及其宠物名称的查询,如下所示


@Daopublic interface MyDao {@Query("SELECT user.name AS userName, pet.name AS petName "+ "FROM user, pet "+ "WHERE user.id = pet.user_id")public LiveData<List<UserPet>> loadUserAndPetNames();//你也可以在另外的java文件中定义UserPet,只要你确保采用public修饰static class UserPet {public String userName;public String petName;}}


类型转换器

类型转换器是一款高质量的ORM必不可少的部分之一。

Room对Java基本数据类型以及其装箱类型都提供了支持,但是有时候你可能使用了一个自定义的数据类型,并且你想将此类型的数据存储到数据库表中的字段里。

为了实现这个功能,你需要提供一个转换器,把自定义的数据类型,转换成Room能够持久化的数据类型。


public class Converters {@TypeConverterpublic static Date fromTimestamp(Long value) {return value == null ? null : new Date(value);}@TypeConverterpublic static Long dateToTimestamp(Date date) {return date == null ? null : date.getTime();}}


上面的示例提供了两个方法,用于把Long转换成Date,并且反向转换。由于Room已经知道如何持久化Long类型的数据,就可以使用转换器持久化Date类型的数据。


@Database(entities = {User.java}, version = 1)@TypeConverters({Converter.class})public abstract class AppDatabase extends RoomDatabase {public abstract UserDao userDao();}

接下来,只需将@TypeConverters注解添加到AppDatabase类,Room便可以使用转换器。

使用这些转换器,你可以在其他查询中使用自定义类型,就像使用原始类型一样,如下所示:


//User.java@Entitypublic class User {...private Date birthday;}

@Dao//UserDao.javapublic interface UserDao {...@Query("SELECT * FROM user WHERE birthday BETWEEN :from AND :to")List<User> findUsersBornBetweenDates(Date from, Date to);}

你还可以将@TypeConverter限制为不同的范围,包括单个实体,DAO和DAO方法。有关更多详细信息,请参阅@TypeConverters注释的参考文档。

数据迁移

Room允许你编写迁移类来保留用户数据,每个Migration类都需要指定版本。

在运行时,Room会每个Migration类的migrate()方法,使用正确的顺序将数据库迁移到更高版本。

!!!注意:如果你没有提供一个有效的迁移,Room将重新构建数据库,这意味着你会丢失所有的数据。


//添加迁移文件Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name").addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();//从版本1迁移到2,可以看到,这里新建了一张Fruit表static final Migration MIGRATION_1_2 = new Migration(1, 2) {@Overridepublic void migrate(SupportSQLiteDatabase database) {database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "+ "`name` TEXT, PRIMARY KEY(`id`))");}};//从版本2迁移到3,Book表新增了一个pub_year字段static final Migration MIGRATION_2_3 = new Migration(2, 3) {@Overridepublic void migrate(SupportSQLiteDatabase database) {database.execSQL("ALTER TABLE Book "+ " ADD COLUMN pub_year INTEGER");}};


!!!注意:为了使迁移的逻辑能够保持正常运行,请使用完整查询,而不是使用引用常量的查询。

迁移过程完成后,Room会验证Schema以确保迁移正确。如果Room发现问题,它会抛出一个包含不匹配信息的异常。

测试迁移

迁移并不是简单的写入,如果无法正确写入,可能会导致App无限崩溃。

为了保持App的稳定性,应该事先测试迁移。

Room提供了一个Maven测试插件来协助测试过程。要使此插件正常工作,需要导出数据库的Schema。

导出Schemas

编译后后,Room将数据库的Schema信息导出为JSON文件。要导出Schema,请在build.gradle文件中设置room.schemaLocation注解处理器属性,如下所示:


//build.gradleandroid {...defaultConfig {...javaCompileOptions {annotationProcessorOptions {arguments = ["room.schemaLocation":"$projectDir/schemas".toString()]}}}}

您应该将导出的JSON文件(存储了数据库的Schema历史记录)存储在版本控制系统中(Git),因为它可以让Room在创建旧版本的数据库的时候进行测试。


//build.gradleandroid {...sourceSets {androidTest.assets.srcDirs += files("$projectDir/schemas".toString())}}

测试包提供了一个MigrationTestHelper类,可以读取这些schema文件。同时它也是一个Junit4 TestRule类,所以它可以管理创建的数据库。

下面是一个迁移测试示例:


@RunWith(AndroidJUnit4.class)public class MigrationTest {private static final String TEST_DB = "migration-test";@Rulepublic MigrationTestHelper helper;public MigrationTest() {helper = new MigrationTestHelper(InstrumentationRegistry.getContext(),MigrationDb.class.getCanonicalName(),new FrameworkSQLiteOpenHelperFactory());}@Testpublic void migrate1To2() throws IOException {SupportSQLiteDatabase db = helper.createDatabase(TEST_DB, 1);// db has schema version 1. insert some data using SQL queries.// You cannot use DAO classes because they expect the latest schema.db.execSQL(...);// Prepare for the next version.db.close();// Re-open the database with version 2 and provide// MIGRATION_1_2 as the migration process.db = helper.runMigrationsAndValidate(TEST_DB, 2, true, MIGRATION_1_2);// MigrationTestHelper automatically verifies the schema changes,// but you need to validate that the data was migrated properly.}}


测试你的数据库

当你运行App的测试用例的时候,如果没有设计到数据库测试,则不需要创建完整的数据库,Room允许你轻松地模拟测试中的数据访问层。

这是可以实现的,因为DAO不会泄漏数据库的任何细节。测试其余的App时,则应该创建DAO类的Mock类或假的实例。

这里有两种方法来测试你的数据库:

  • 在你的开发机上

  • 在一个Android设备上

在你开发机上测试

Room使用了SQLite支持库,它提供与Android Framework类中的界面相匹配的界面。此支持允许您传递支持库的自定义实现来测试数据库查询。

在Android设备上测试

官方推荐的方法是测试数据库的方法是,编写在Android设备上运行的JUnit测试用例。因为这些测试不需要创建一个Activity,所以它们应该比您的UI测试执行得更快。

配置测试时,您应该为数据库指定一个测试版本(TestDatabase.class),使测试更加封闭,如下所示:


@RunWith(AndroidJUnit4.class)public class SimpleEntityReadWriteTest {private UserDao mUserDao;private TestDatabase mDb;@Beforepublic void createDb() {Context context = InstrumentationRegistry.getTargetContext();mDb = Room.inMemoryDatabaseBuilder(context, TestDatabase.class).build();mUserDao = mDb.getUserDao();}@Afterpublic void closeDb() throws IOException {mDb.close();}@Testpublic void writeUserAndReadInList() throws Exception {User user = TestUtil.createUser(3);user.setName("george");mUserDao.insert(user);List<User> byName = mUserDao.findUsersByName("george");assertThat(byName.get(0), equalTo(user));}}


有关测试数据库迁移的更多信息,请参阅迁移测试

附录:实体之间没有对象引用

从数据库到对象模型的关系映射(ORM)是常见的做法,这种做法在服务器工作的很好,可以实现实现高性能的延迟加载。

但是在客户端,延迟加载很可能在UI线程发生。

在UI线程进行IO操作显然是不可取的,UI线程只有约16毫秒的时间来计算和绘制Activity的最新布局,即使查询只需要5毫秒,UI绘制仍然有可能耗尽绘制时间,这将会导致明显的卡顿,更糟的是,如果查询并行运行单独的事务,或者设备忙于其他disk-heavy任务,查询可能需要更多的时间才能完成。但是,如果不使用延迟加载,则App将获取到超出其需求的数据,从而导致消耗更多不必要的内存。

ORM通常是将此决定权留给开发人员,以便他们可以为App的用例做最好的事情。

不幸的是,开发人员通常在他们的App和UI之间共享数据模型。随着UI随着时间的推移而变化,会有难以预料和调试的问题出现。

例如,加载Book对象列表的UI,每本书都有一个Author对象。你最初可能使用延迟加载来查询,通过调用Book实例的getAuthor()方法来返回作者。第一次调用getAuthor()调用查询数据库。一段时间后,你意识到你需要在App的使用者页面中显示作者的姓名。你可以如下所示轻松的实现:

1
authorNameTextView.setText(user.getAuthor().getName());


然而,这个看似合理的变化导致在主线程上查询作者表。

如果采用渴求式加载作者信息,在不再需要该数据的时候,就很难改变数据的加载方式了,例如某些页面不再需要显示有关特定作者的信息。

因此,您的App还得继续加载不再显示的数据。如果作者类引用另一个表,例如使用getBooks()方法,这种情况会更糟。

由于这些原因,Room禁止实体类之间的对象引用。相反,你必须显式请求您的App需要的数据

原文链接:http://www.apkbus.com/blog-822717-68631.html

0人推荐
随时随地看视频
慕课网APP