处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的使用场景是缓存相关的数据,这样一来,当设备无法访问网络时,用户仍然可以在离线状态下浏览该内容。
Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite
的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:针对 SQL 查询的编译时验证。 可最大限度减少重复和容易出错的样板代码的方便注解。 简化了数据库迁移路径。
Room用于数据持久化,使用简单,对SQL不了解也可以快速使用,但暴露出来的api有限,遇到复杂的数据还需使用SQL。Room本身性能并无特别强大之处,但是在Androidx中配合了Lifecycle,可以感知生命周期是其他数据库无法比拟的;同时搭配WorkManager使用可以实现非即时(至少15分钟之后),且一定会实现的任务(APP没有被删除的情况下)(目前仅限于Google pixel)。本文只对Room的简单使用进行阐述,不涉及Lifecycle等其他内容。
Room 包含三个主要组件:
数据库类为应用提供与该数据库关联的 DAO 的实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。这里加了一层Repository用于处理网络数据和本地数据。
具体的流程如下:
dependencies {
...
// 数据持久化room
implementation 'androidx.room:room-runtime:2.4.2'
annotationProcessor 'androidx.room:room-compiler:2.4.2'
}
defaultConfig {
minSdkVersion MinSdkVersion
targetSdkVersion TargetSdkVersion
versionCode 1
versionName "1.0"
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName(),
"room.schemaLocation":"$projectDir/schemas".toString()]
}
}
}
@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 +
'}';
}
}
除此之外还有foreignKeys:外键、indices:索引、defaultValue:默认值等,具体使用还需自行查阅。
public interface UserDao {
/**
* 查询所有用户
*
* @return
*/
@Query("SELECT * FROM user")
List<User> queryAllUser();
/**
* 根据名称查询所有用户
*
* @return
*/
@Query("SELECT * FROM user WHERE name = :name")
List<User> 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+SQL语句进行操作。批量操作时Room内部使用了事务,无需用户单独控制。
@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();
}
Room.databaseBuilder还有其他配置,根据需要进行配置。
数据库操作可能会耗时,建议创建异步任务进行操作。
public class QueryUserTask extends AsyncTask<Void, Void, List<User>> {
private UserDao userDao;
public QueryUserTask(UserDao userDao) {
this.userDao = userDao;
}
@Override
protected List<User> doInBackground(Void... voids) {
Log.d("QueryUserTask", "doInBackground: start");
List<User> users = userDao.queryAllUser();
Log.d("QueryUserTask", "doInBackground: end");
return users == null ? Collections.EMPTY_LIST : users;
}
}
AsyncTask为异步任务,Java默认是同步任务,异步任务需要单独创建。
public abstract class AsyncTask<Params, Progress, Result> {...}
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<User> queryUser() {
if (userDao == null) {
Log.e("UserRepository", "QueryUser: userDao is null");
return Collections.EMPTY_LIST;
}
List<User> users = new QueryUserTask(userDao).doInBackground();
return users;
}
}
使用Repository是为了专门处理本地缓存数据和网络数据,统一数据。
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<User> 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或其他三方库。