概述
处理大量结构化数据的应用可极大地受益于在本地保留这些数据。最常见的使用场景是缓存相关的数据,这样一来,当设备无法访问网络时,用户仍然可以在离线状态下浏览该内容。
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用于处理网络数据和本地数据。 具体的流程如下:
使用
1. 添加依赖
dependencies {
...
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 {
@Query("SELECT * FROM user")
List<User> queryAllUser();
@Query("SELECT * FROM user WHERE name = :name")
List<User> queryUserByName(String name);
@Insert
void insertCustom(User user);
@Update
void updateUser(User... users);
@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<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> {...}
- 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();
}
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是为了专门处理本地缓存数据和网络数据,统一数据。
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<User> users = userRepository.queryUser();
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或其他三方库。
|