Android|Android随笔-Room简单使用

概述

【Android|Android随笔-Room简单使用】处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的使用场景是缓存相关的数据,这样一来,当设备无法访问网络时,用户仍然可以在离线状态下浏览该内容。
Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite
的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:
针对 SQL 查询的编译时验证。 可最大限度减少重复和容易出错的样板代码的方便注解。 简化了数据库迁移路径。
Room用于数据持久化,使用简单,对SQL不了解也可以快速使用,但暴露出来的api有限,遇到复杂的数据还需使用SQL。Room本身性能并无特别强大之处,但是在Androidx中配合了Lifecycle,可以感知生命周期是其他数据库无法比拟的;同时搭配WorkManager使用可以实现非即时(至少15分钟之后),且一定会实现的任务(APP没有被删除的情况下)(目前仅限于Google pixel)。本文只对Room的简单使用进行阐述,不涉及Lifecycle等其他内容。
组成 Room 包含三个主要组件:
  • Database:数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点。
  • Entity:数据实体,用于表示应用的数据库中的表。
  • DAO:数据访问对象 ,提供您的应用可用于查询、更新、插入和删除数据库中的数据的方法。
数据库类为应用提供与该数据库关联的 DAO 的实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。这里加了一层Repository用于处理网络数据和本地数据。
Android|Android随笔-Room简单使用
文章图片

具体的流程如下:
Android|Android随笔-Room简单使用
文章图片

使用 1. 添加依赖
dependencies { ... // 数据持久化room implementation 'androidx.room:room-runtime:2.4.2' annotationProcessor 'androidx.room:room-compiler:2.4.2' }

2. 添加数据库schemas(选)
defaultConfig { minSdkVersion MinSdkVersion targetSdkVersion TargetSdkVersion versionCode 1 versionName "1.0"javaCompileOptions { annotationProcessorOptions { arguments = [AROUTER_MODULE_NAME: project.getName(), "room.schemaLocation":"$projectDir/schemas".toString()] } } }

3. 创建Entity
@Entity(tableName = "user") public class User { // 主键,自增 @PrimaryKey(autoGenerate = true) @NonNull public Integer id; // 名称 @ColumnInfo(name = "name") public String name; // 年龄 public Integer age; public User(String name, Integer age) { this.name = name; this.age = age; }@Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }

  • @Entity:表示User是一张数据表
  • tableName:数据库表名,不写时默认为“”
  • @primaryKeys:主键
  • autoGenerate:默认false,为true时表示主键无需赋值,值会自动递增
  • @ColumnInfo:字段属性,name为该属性在数据库中字段名称,不写默认为属性名称
除此之外还有foreignKeys:外键、indices:索引、defaultValue:默认值等,具体使用还需自行查阅。
4. 创建DAO
public interface UserDao {/** * 查询所有用户 * * @return */ @Query("SELECT * FROM user") List queryAllUser(); /** * 根据名称查询所有用户 * * @return */ @Query("SELECT * FROM user WHERE name = :name") List queryUserByName(String name); /** * 插入用户 * * @param user */ @Insert void insertCustom(User user); /** * 更新用户 * * @param users */ @Update void updateUser(User... users); /** * 删除用户 * * @param user */ @Delete void deleteUser(User user); /** * 清空数据 */ @Query("DELETE FROM user") void deleteAllUsers(); }

  • @Query:查
  • @Insert:增
  • @Update:改
  • @Delete:删
具体需求根据业务来,可以批量操作,也可单一操作,若有些操作无法实现的可用@Query+SQL语句进行操作。批量操作时Room内部使用了事务,无需用户单独控制。
5. 创建Database
@Database(entities = {User.class}, version = 1, exportSchema = false) public abstract class UserDatabase extends RoomDatabase { // 数据库名称 private static final String DATABASE_NAME = "user_db"; // 数据库实例 private static UserDatabase INSTANCE; // 单例 public static UserDatabase getInstance(Context context) { if (INSTANCE == null) { synchronized (UserDatabase.class) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), UserDatabase.class, DATABASE_NAME) .fallbackToDestructiveMigration()// 暴力升级,可能会出现数据丢失 .build(); } } return INSTANCE; }// 暴露操作对象 public abstract UserDao userDao(); }

  • @Database:数据库,需要指定entities
  • version:数据库版本,一般只能升不能降,表格有所改动均需要升级
  • exportSchema:数据配置,默认为true,需要进行第二步配置,若false,则无需配置
  • fallbackToDestructiveMigration:暴力升级,可能会出现数据丢失
Room.databaseBuilder还有其他配置,根据需要进行配置。
6. 创建异步任务
数据库操作可能会耗时,建议创建异步任务进行操作。
public class QueryUserTask extends AsyncTask> { private UserDao userDao; public QueryUserTask(UserDao userDao) { this.userDao = userDao; }@Override protected List doInBackground(Void... voids) { Log.d("QueryUserTask", "doInBackground: start"); List users = userDao.queryAllUser(); Log.d("QueryUserTask", "doInBackground: end"); return users == null ? Collections.EMPTY_LIST : users; } }

AsyncTask为异步任务,Java默认是同步任务,异步任务需要单独创建。
public abstract class AsyncTask {...}

  • Params:参数类型,无参使用Void
  • Progress:任务进度,不需要使用Void
  • Result:返回类型,没有返回值使用Void
7. 创建仓库Repository
public class UserRepository { private UserDao userDao; private Context context; public UserRepository(Context context) { this.context = context.getApplicationContext(); UserDatabase centerDataBase = UserDatabase.getInstance(context.getApplicationContext()); userDao = centerDataBase.userDao(); }/** * 查询所有用户 * * @return */ public List queryUser() { if (userDao == null) { Log.e("UserRepository", "QueryUser: userDao is null"); return Collections.EMPTY_LIST; } List users = new QueryUserTask(userDao).doInBackground(); return users; } }

使用Repository是为了专门处理本地缓存数据和网络数据,统一数据。
8. 调用
public class MainActivity extends AppCompatActivity {private TextView query; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); query = findViewById(R.id.tv_query_all); // 触发 query.setOnClickListener(v -> queryUser()); }private void queryUser(){ // 子线程 new Thread(() -> { UserRepository userRepository = new UserRepository(getApplicationContext()); // 查询用户 List users = userRepository.queryUser(); // 更新ui mHandler.sendEmptyMessage(users.size()); }).start(); }private Handler mHandler = new Handler(Looper.getMainLooper()){ @Override public void handleMessage(@NonNull Message msg) { super.handleMessage(msg); query.setText("已查询" + msg.what + "条数据"); } }; }

数据库操作时需要在子线程中进行,操作完成后若需更新UI,则需切换到主线程进行UI刷新。若是数据库操作比较耗时,在主线程中操作,容易出现ANR。若一定要在主线程进行操作,则在UserDatabase实例时,需要调用allowMainThreadQueries():
// 单例 public static UserDatabase getInstance(Context context) { if (INSTANCE == null) { synchronized (UserDatabase.class) { INSTANCE = Room.databaseBuilder(context.getApplicationContext(), UserDatabase.class, DATABASE_NAME) .fallbackToDestructiveMigration()// 暴力升级,可能会出现数据丢失 .allowMainThreadQueries()// 允许在主线程操作 .build(); } } return INSTANCE; }

这里使用了Handler进行线程切换,msg.what一般用户区分消息类型,若需要传递数据需要使用Message包裹对象,不可像代码中那样使用,代码中使用msg.what传递消息内容是为了简单方便,但显得不专业,不建议使用。主线程更新也可使用runOnUiThread(Runnable action),原理是一样的。
一般逻辑操作是放在ViewModel中进行,通常还有DataBinding,这里为了简单就直接在Activity中进行。
总结 Room使用看似有些繁琐,但相比于Android原生的SQLite还是简单不少,若是数据操作比较复杂,还是建议使用SQLite或其他三方库。

    推荐阅读