赞
踩
Google在没有推出Room组件之前,如果我们想要操作数据库,android是采用sqlite作为数据库存储。有使用过sqlite的读者应该十分清楚,sqlite的使用相当的复杂,而且代码写起来既繁琐又容易出错。因此,各大开源社区便推出了各种ORM库来简化sqlite的操作。而作为Google的亲儿子Room也是对sqlite的一层抽象。它使用了APT机制,通过注解的方式在编译阶段生成sqlite代码,使开发者不需要写冗杂的sqlite代码,提高开发效率。
想要了解一个东西的用法,我们首先要会用它。sqlite的用法这里不多介绍了,如果不会使用的读者可以参考下面这边博客:android使用SQLite。sqlite的用法是繁琐了点,大家多写几遍就熟悉了。
我们重点看下room的用法。
从上面这幅图中可以清楚的看到,整个room的操作流程:room对象持有了dao对象的引用,dao对象则持有了entity对象的引用,entity对象则被ui(一般是activity、fragment)持有。说的通俗一点,就是Room类中有一个dao类型的成员变量,而dao类中则有一个entity类型的成员变量,ui中则有一个entity的成员变量。
那么room、dao、entity都是些什么东西呢?在代码中又是怎么表现出来的呢?
多说无益,我们直接上代码演示:
dependencies {
def room_version = "2.4.1"//目前最新的稳定版本
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}
package com.example.roomdemo01.simple1.entity; import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.PrimaryKey; @Entity public class Student { // 主键 SQL 唯一的 autoGenerate自增长 @PrimaryKey(autoGenerate = true) private int uid; @ColumnInfo(name = "name") private String name; @ColumnInfo(name = "pwd") private String password; @ColumnInfo(name = "address") private int address; // 注意:升级的字段 1-2个升级开始 @ColumnInfo(name = "flag") private int flag; public int getFlag() { return flag; } public void setFlag(int flag) { this.flag = flag; } public Student(String name, String password, int address) { this.name = name; this.password = password; this.address = address; } public int getUid() { return uid; } public void setUid(int uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getAddress() { return address; } public void setAddress(int address) { this.address = address; } @Override public String toString() { return "Student{" + "uid=" + uid + ", name='" + name + '\'' + ", password='" + password + '\'' + ", address='" + address + '\'' + '}'; } }
package com.example.roomdemo01.simple1.entity; import androidx.room.ColumnInfo; // @Entity 不能加,加了就是一张表了 public class StudentTuple { @ColumnInfo(name = "name")//需要添加ColumnInfo注解,这样才能知道数据库的字段映射到model的哪个属性 public String name; @ColumnInfo(name="pwd") public String password; public StudentTuple(String name, String password) { this.name = name; this.password = password; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } @Override public String toString() { return "StudentTuple{" + "name='" + name + '\'' + ", password='" + password + '\'' + '}'; } }
@Entity标签用于将Student类与Room中的数据表对应起来。tableName属性可以为数据表设置表名,若不设置,则表名与类名相同,也就是说model类加了Entity标签就是一张表。
@PrimaryKey标签用于指定该字段作为表的主键。
@ColumnInfo标签可用于设置该字段存储在数据库表中的名字,并指定字段的类型。
@Ignore标签用来告诉Room忽略该字段或方法。Room不会持久化被@Ignore标签标记过的字段的数据
Dao层
package com.example.roomdemo01.simple1.dao; import androidx.room.Dao; import androidx.room.Delete; import androidx.room.Insert; import androidx.room.Query; import androidx.room.Update; import com.example.roomdemo01.simple1.entity.Student; import com.example.roomdemo01.simple1.entity.StudentTuple; import java.util.List; @Dao public interface StudentDao { @Insert void insert(Student... students); @Delete void delete(Student student); @Update void update(Student student); @Query("select * from Student") List<Student> getAll(); // 查询一条记录 @Query("select * from Student where name like:name") Student findByName(String name); // 数组查询 多个记录 @Query("select * from Student where uid in(:userIds)") List<Student> getAllId(int[] userIds); //我们只查询name pwd字段,如果给Student类接收,那么address字段就会为null; //如果不想出现这种情况,我们可以新建一个普通的Model类,切记该类不能添加Entity注解,因为添加了就是一张表了 @Query("select name,pwd from Student") StudentTuple getRecord(); }
// 为了养成好习惯 规则 ,要写 exportSchema = false ,因为在升级过程中会记录所有的历史版本信息,,因为内部要记录升级的所有副本 //使用抽象类是为了能够在编译时期使用apt生成真正的类 @Database(entities = {Student.class}, version = 1, exportSchema = false) public abstract class AppDataBase extends RoomDatabase { private static final AppDataBase databaseInstance; private static final String DATABASE_NAME = "my_db" public static synchronized AppDataBase getInstance(Context context) { if (databaseInstance == null) { databaseInstance = Room.databaseBuilder( context.getApplicationContext(), AppDataBase.class, DATABASE_NAME) //从assets/database目录下读取students.db //.createFromAsset("databases/students.db") // 可以设置强制主线程,默认是让你用子线程 // .allowMainThreadQueries() .build(); } return databaseInstance; } // 暴露dao public abstract StudentDao userDao(); }
@Database标签用来告诉系统这是Room数据库对象。entity属性用于指定该数据库有哪些表,若需要建立多张表,则表名以逗号隔开。version属性用于指定数据库版本号,后面数据库的升级正是根据版本号进行判断的。
ui层
package com.example.roomdemo01.simple1.ui; import android.os.Bundle; import android.util.Log; import androidx.appcompat.app.AppCompatActivity; import androidx.room.Room; import com.example.roomdemo01.R; import com.example.roomdemo01.simple1.dao.StudentDao; import com.example.roomdemo01.simple1.db.AppDataBase; import com.example.roomdemo01.simple1.entity.Student; import com.example.roomdemo01.simple1.entity.StudentTuple; import java.util.List; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); // 数据库的操作应该是在子线程 DbTest t = new DbTest(); t.start(); } // 测试我们刚刚写的 三个角色Room数据库 增删改查 public class DbTest extends Thread { @Override public void run() { // 数据库的操作都在这里 AppDataBase MyDB = AppDataBase.getInstance(MainActivity.this); StudentDao dao = MyDB.userDao(); dao.insert(new Student("Studnet1", "123", 1)); dao.insert(new Student("Studnet2", "456", 2)); dao.insert(new Student("Studnet3", "789", 3)); dao.insert(new Student("Studnet4", "111", 4)); // 查询全部数据 List<Student> all = dao.getAll(); Log.d("DbTest", "run: all.toString():" + all.toString()); Log.i("DeDbTest", "--------------------------"); // 查询名字为 Studnet3 的一条数据 Student stu = dao.findByName("Studnet3"); Log.d("DbTest", "run: stu.toString():" + stu.toString()); Log.i("DbTest", "--------------------------"); // 查询 2 3 4 uid的三条数据 List<Student> allID = dao.getAllId(new int[]{2, 3, 4}); Log.d("DbTest", "run: allID.toString():" + allID.toString()); Log.i("DbTest", "--------------------------"); // 查询student表里面的数据 到 StudentTuple里面去 StudentTuple record = dao.getRecord(); Log.d("DbTest", "run: record.toString():" + record.toString()); } } }
是不是很像后端中spring的开发方式,分层以及大量的注解。
运行结果如下图所示:
现在,我们知道了room的基本用法了。这里又个疑问,Google为什么要大费周章的弄个room呢?如果仅仅只是为了对sqlite做一层封装那么个人感觉有点大材小用了。其实,Room组件最大的魅力在于能够于livedata组件一起使用。LiveData组件大家知道:它是整个jetpack的核心组件,只要数据有一点改变那么ui层就能立马得知,从而改变ui。这可就和Google推荐的开发模式:jetpack+kotlin+mvvm遥相呼应了。
下面,来演示下LiveData如何与Room结合使用,其实用法不难:
// 在dao层中使用 LiveData 关联 Room,这样从数据库中拿到的数据便可以映射到LiveData中
@Query("select * from Student order by uid")
LiveData<List<Student>> getAllLiveDataStudent();
然后,我们在ui层中对LiveData进行观察
package com.example.roomdemo02; import android.os.Bundle; import android.widget.ListView; import androidx.appcompat.app.AppCompatActivity; import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModelProviders; import java.util.List; public class MainActivity extends AppCompatActivity { ListView listView; StudentViewModel studentViewModel; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); listView = findViewById(R.id.listView); studentViewModel = ViewModelProviders.of(this).get(StudentViewModel.class); // 观察者,只要页面在前台,只要数据改变,用户就可以肉眼看到这种变化 studentViewModel.getAllLiveDataStudent().observe(this, new Observer<List<Student>>() { @Override public void onChanged(List<Student> students) { // 更新UI listView.setAdapter(new GoodsAdapter(MainActivity.this, students)); } }); // 模拟 仓库 new Thread() { @Override public void run() { super.run(); try { Thread.sleep(6000); } catch (InterruptedException e) { e.printStackTrace(); } // 默认给数据库ROOM增加数据 for (int i = 0; i < 50; i++) { studentViewModel.insert(new Student("Student", "123", 1)); } } }.start(); // 模拟仓库 数据库 数据被修改了,一旦数据库被修改,那么数据会驱动UI的发生改变 new Thread() { @Override public void run() { for (int i = 0; i < 50; i++) { try { Thread.sleep(1000); } catch (Exception e) { e.printStackTrace(); } studentViewModel.update(new Student(6, "Student" + i, "123", 1)); } } }.start(); } }
注意:LiveData通常与ViewModel一起使用,ViewModel是用于存放数据的,因此可以将数据库放在ViewModel中进行实例化。但数据库的实例化需要用到Context,而ViewModel中最好不要传入Context,因此,我们不宜直接使用ViewModel,而应该用它的子类AndroidViewModel,并且在viemodel的构造函数中传入Application类型的上下文。
import android.content.Context; import androidx.annotation.NonNull; import androidx.room.Database; import androidx.room.Room; import androidx.room.RoomDatabase; import androidx.room.migration.Migration; import androidx.sqlite.db.SupportSQLiteDatabase; // exportSchema = false 尽量写,内部需要检测,如果没有写,会抛出异常,因为内部要记录升级的所有副本 @Database(entities = {Student.class}, version = 2, exportSchema = false) public abstract class AppDatabase extends RoomDatabase { private static AppDatabase instance; public static synchronized AppDatabase getInstance(Context context) { if (instance == null) { instance = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class , "my_db") // 可以强制在主线程运行数据库操作 .allowMainThreadQueries() // 暴力升级 不管三七二十一 强制执行(数据会丢失)(慎用) // .fallbackToDestructiveMigration() // 稳定的方式升级 .addMigrations(MIGRATION_1_2) .build(); } return instance; } public abstract StudentDao studentDao(); // 下面是稳定升级的方式 static final Migration MIGRATION_1_2 = new Migration(1, 2) { @Override public void migrate(SupportSQLiteDatabase database) { // 在这里用SQL脚本完成数据的变化 database.execSQL("alter table student add column flag integer not null default 1"); } }; // ROOM 是不能降级的,我非要删除一个字段,却要保证数据的稳定性,这个是特殊情况 // 特殊手法降级 static final Migration MIGRATION_2_3 = new Migration(2, 3) { @Override public void migrate(@NonNull SupportSQLiteDatabase database) { // SQL 四步法 // 1.先建立临时表 // database.execSQL("create table student_temp (uid integer primary key not null,name text,pwd text,addressId)"); // 2.把之前表的数据(SQL语句的细节,同学们可以上网查询下) // database.execSQL("insert into student_temp (uid,name,pwd,addressid) " + " select uid,name,pwd,addressid from student"); // 3.删除student 旧表 // database.execSQL("drop table student"); // 4.修改 临时表 为 新表 student // database.execSQL("alter table student_temp rename to student"); } }; }
首先,我们需要明确我们的疑惑是什么?笔者在学习room时的疑惑有下面几点:
接下来,我们就带着这两个疑惑来看源码。首先,我们从Room的初始化方法开始看起。我们知道Room采用了建造者模式
public T build() { //noinspection ConstantConditions if (mContext == null) { throw new IllegalArgumentException("Cannot provide null context for the database."); } //noinspection ConstantConditions if (mDatabaseClass == null) { throw new IllegalArgumentException("Must provide an abstract class that" + " extends RoomDatabase"); } if (mQueryExecutor == null && mTransactionExecutor == null) { mQueryExecutor = mTransactionExecutor = ArchTaskExecutor.getIOThreadExecutor(); } else if (mQueryExecutor != null && mTransactionExecutor == null) { mTransactionExecutor = mQueryExecutor; } else if (mQueryExecutor == null && mTransactionExecutor != null) { mQueryExecutor = mTransactionExecutor; } if (mMigrationStartAndEndVersions != null && mMigrationsNotRequiredFrom != null) { for (Integer version : mMigrationStartAndEndVersions) { if (mMigrationsNotRequiredFrom.contains(version)) { throw new IllegalArgumentException( "Inconsistency detected. A Migration was supplied to " + "addMigration(Migration... migrations) that has a start " + "or end version equal to a start version supplied to " + "fallbackToDestructiveMigrationFrom(int... " + "startVersions). Start version: " + version); } } } if (mFactory == null) { mFactory = new FrameworkSQLiteOpenHelperFactory(); } if (mCopyFromAssetPath != null || mCopyFromFile != null) { if (mName == null) { throw new IllegalArgumentException("Cannot create from asset or file for an " + "in-memory database."); } if (mCopyFromAssetPath != null && mCopyFromFile != null) { throw new IllegalArgumentException("Both createFromAsset() and " + "createFromFile() was called on this Builder but the database can " + "only be created using one of the two configurations."); } mFactory = new SQLiteCopyOpenHelperFactory(mCopyFromAssetPath, mCopyFromFile, mFactory); } //上面这些都是些支线逻辑,我们不需要看 DatabaseConfiguration configuration = new DatabaseConfiguration( mContext, mName, mFactory, mMigrationContainer, mCallbacks, mAllowMainThreadQueries, mJournalMode.resolve(mContext), mQueryExecutor, mTransactionExecutor, mMultiInstanceInvalidation, mRequireMigration, mAllowDestructiveMigrationOnDowngrade, mMigrationsNotRequiredFrom, mCopyFromAssetPath, mCopyFromFile); T db = Room.getGeneratedImplementation(mDatabaseClass, DB_IMPL_SUFFIX); db.init(configuration); return db; }
上面的build方法中重点在于最后一句init方法,我们进入init方法看下究竟做了什么事情。
点击去之后,发现居然是个抽象方法,那么我们进入它是实现类看下,这个AppDatabase_Impl其实就是APT生成的类
看到这里大家应该都明白了吧!其实room底层也是调用了sqlite来创建数据库的。我们看下createAllTables方法是怎么被调的。
createAllTables被RoomOpenHelper的onCreate方法调用,而RoomOpenHelper的onCreate方法则是被OpenHelper类的onCreate方法调用,这个OpenHelper类则是在FrameworkSQLiteOpenHelper类的构造方法中被初始化,而FrameworkSQLiteOpenHelper则是在前面的build方法的时候被初始化的。
至此,整个room的初始化逻辑链路全部连接起来了。
至于,第二点则跟简单了,APT会生成一个Impl类,此例是StudentDao_Impl类,我们调用的其实就是该类的对应方法。
感谢各位读者耐心读到最后,谢谢!!!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。