除了SQLite一定要试试Room

22 篇文章 2 订阅
1 篇文章 0 订阅

在这里插入图片描述

不舍的春节已过,收起心来,2021咱们一起加油。

Android开发者使用数据库的时候,最先想到的是SQLite。如果有对外公开的需求,则需再包装一层ContentProvider。除此之外,也可以选择开源的数据库框架,比如GreenDaoDBFlow等。

本文将讲述Google推出的数据库框架Room,和你一起探讨如何使用Room、其实现的大概原理以及和它的优势

简介

Room是房间的意思。房间除了能存放物品,还能带给人温暖和安心的感觉。用Room给这个抽象的软件架构命名,增加了人文色彩,很有温度。

先来看一下Room框架的基本组件。在这里插入图片描述
使用起来大体就是这几个步骤,很便捷。

通过RoomDatabase获取DAO接口
通过DAO获取和更新Entities表
通过Entities获取和更新字段

使用前需要构筑如下依赖。

dependencies {
  def room_version = "2.2.6"

  implementation "androidx.room:room-runtime:$room_version"
  kapt "androidx.room:room-compiler:$room_version"
  
  implementation "androidx.room:room-ktx:$room_version"
  testImplementation "androidx.room:room-testing:$room_version"
}

实战

下面将通过一个展示电影列表的demo演示Room框架的使用。

组件构建

首先构建一个电影表Movie,有名称、演员、上映年份、评分这么几个字段。

@Entity
class Movie() : BaseObservable() {
    @PrimaryKey(autoGenerate = true)
    var id = 0

    @ColumnInfo(name = "movie_name", defaultValue = "Harry Potter")
    lateinit var name: String

    @ColumnInfo(name = "actor_name", defaultValue = "Jack Daniel")
    lateinit var actor: String

    @ColumnInfo(name = "post_year", defaultValue = "1999")
    var year = 1999

    @ColumnInfo(name = "review_score", defaultValue = "8.0")
    var score = 8.0
}
  • @Entity表示数据库中的表
  • @PrimaryKey表示主键,autoGenerate意味着主键自增,默认为false
  • @ColumnInfo表示字段,namedefaultValue,顾名思义代表名称和默认值

然后构建一个访问Movie表的DAO接口。

@Dao
interface MovieDao {
    @Insert
    fun insert(vararg movies: Movie?): LongArray?

    @Delete
    fun delete(movie: Movie?): Int

    @Update
    fun update(vararg movies: Movie?): Int

    @get:Query("SELECT * FROM movie")
    val allMovies: LiveData<List<Movie?>?>
}
  • @Dao表示访问数据库表的接口,需要声明为接口或抽象类,Room框架将在编译阶段生成_Impl实现类,此处则将生成MovieDao_Impl.java文件
  • @Insert@Delete@Update@Query分别表示数据库的增删改查方法

最后需要构建Room框架使用的入口RoomDatabase

@Database(entities = [Movie::class], version = 1)
abstract class MovieDataBase : RoomDatabase() {
    abstract fun movieDao(): MovieDao

    companion object {
        @Volatile
        private var sInstance: MovieDataBase? = null
        private const val DATA_BASE_NAME = "jetpack_movie.db"

        @JvmStatic
        fun getInstance(context: Context): MovieDataBase? {
            if (sInstance == null) {
                synchronized(MovieDataBase::class.java) {
                    if (sInstance == null) {
                        sInstance = createInstance(context)
                    }
                }
            }
            return sInstance
        }

        private fun createInstance(context: Context): MovieDataBase {
            return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
                    ...
                    .build()
        }
    }
}
  • @Database 表示继承自RoomDatabase的抽象类,entities指定Entities表的实现类列表,version指定了DB版本
  • 必须提供获取DAO接口的抽象方法,比如上面定义的movieDao()RoomDatabase将通过这个方法实例化DAO接口
  • RoomDatabase实例的内存开销较大,建议使用单例模式管理
  • 编译的时候将生成_Impl实现类,此处将生成MovieDataBase_Impl.java文件

组件调用

本demo将结合ViewModelRoom进行数据交互,依赖LiveData进行异步查询,画面上则采用Databinding将数据和视图自动绑定。

class DemoActivity : AppCompatActivity() {
    private var movieViewModel: MovieViewModel? = null
    private var binding: ActivityRoomDbBinding? = null
    private var movieList: List<Movie?>? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityRoomDbBinding.inflate(layoutInflater)
        setContentView(binding!!.root)
        binding!!.lifecycleOwner = this

        movieViewModel = ViewModelProvider(this).get(MovieViewModel::class.java)
        movieViewModel?.getMovieList(this, { movieList: List<Movie?>? ->
            if (movieList == null) return@getMovieList
            this.movieList = movieList
            binding?.setMovieList(movieList)
        })
    }
}

ViewModel通过MediatorLiveData担当列表查询的中介,当DB初始化结束后再更新UI。

class MovieViewModel(application: Application) : AndroidViewModel(application) {
    private val mediatorLiveData = MediatorLiveData<List<Movie?>?>()
    private val db: MovieDataBase?
    private val mContext: Context

    init {
        mContext = application
        db = MovieDataBase.getInstance(mContext)
        if (db != null) {
            mediatorLiveData.addSource(db.movieDao().allMovies) { movieList ->
                if (db.databaseCreated.value != null) {
                    mediatorLiveData.postValue(movieList)
                }
            }
        };
    }

    fun getMovieList(owner: LifecycleOwner?, observer: Observer<List<Movie?>?>?) {
        if (owner != null && observer != null)
            mediatorLiveData.observe(owner, observer)
    }
}

RoomDatabase创建后异步插入初始化数据,并通知MediatorLiveData

abstract class MovieDataBase : RoomDatabase() {
    val databaseCreated = MutableLiveData<Boolean?>()
    ...

    companion object {
        ...
        private fun createInstance(context: Context): MovieDataBase {
            return Room.databaseBuilder(context.applicationContext, ...)
                    ...
                    .addCallback(object : Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            Executors.newFixedThreadPool(5).execute {
                                val dataBase = getInstance(context)
                                val ids = dataBase!!.movieDao().insert(*Utils.initData)
                                dataBase.databaseCreated.postValue(true)
                            }
                        }
                        ...
                    })
                    .build()
        }
    }
}

运行下demo看下效果。
在这里插入图片描述
通过AndroidStudioDatabase Inspector工具可以看到DB数据创建成功了。
*Database Inspector支持实时刷新,查询等基本的DB操作,能够满足一般的需求,大家可以试试。
在这里插入图片描述

DAO的具体使用

@Insert

@Insert支持设置冲突策略,默认为OnConflictStrategy.ABORT即中止并回滚。还可以指定为其他策略。

  • OnConflictStrategy.REPLACE 冲突时替换为新记录
  • OnConflictStrategy.IGNORE 忽略冲突(不建议使用)
  • OnConflictStrategy.ROLLBACK 废弃了,使用ABORT替代
  • OnConflictStrategy.FAIL 废弃了,使用ABORT替代

@Insert修饰的方法的返回值可为空,也可为插入行的ID或ID列表

  • fun insertWithOutId(movie: Movie?)
  • fun insert(movie: Movie?): Long?
  • fun insert(vararg movies: Movie?): LongArray?
@Delete

@Insert一样支持不返回删除结果或返回删除的函数,不再赘述。

@Update

@Insert一样支持设置冲突策略和定制返回更新结果。此外需要注意的是@Update操作将匹配参数的主键id去更新字段。

  • fun update(vararg movies: Movie?): Int
@Query

查询操作主要依赖@Update的value,指定不同的SQL语句即可获得相应的查询结果。在编译阶段就将验证语句是否正确,避免错误的查询语句影响到运行阶段。

  • 查询所有字段
    @get:Query(“SELECT * FROM movie”)
  • 查询指定字段
    @get:Query(“SELECT id, movie_name, actor_name, post_year, review_score FROM movie”)
  • 排序查询
    @get:Query(“SELECT * FROM movie ORDER BY post_year DESC”) 比如查询最近发行的电影列表
  • 匹配查询
    @Query(“SELECT * FROM movie WHERE id = :id”)
  • 多字段匹配查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord " + " OR actor_name LIKE :keyWord”) 比如查询名称和演员中匹配关键字的电影
  • 模糊查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ " + " OR actor_name LIKE ‘%’ || :keyWord || ‘%’”) 比如查询名称和演员中包含关键字的电影
  • 限制行数查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE :keyWord LIMIT 3”) 比如查询名称匹配关键字的前三部电影
  • 参数引用查询
    @Query(“SELECT * FROM movie WHERE review_score >= :minScore”) 比如查询评分大于指定分数的电影
  • 多参数查询
    @Query(“SELECT * FROM movie WHERE post_year BETWEEN :minYear AND :maxYear”) 比如查询介于发行年份区间的电影
  • 不定参数查询
    @Query(“SELECT * FROM movie WHERE movie_name IN (:keyWords)”)
  • Cursor查询
    @Query(“SELECT * FROM movie WHERE movie_name LIKE ‘%’ || :keyWord || ‘%’ LIMIT :limit”)
    fun searchMoveCursorByLimit(keyWord: String?, limit: Int): Cursor?
    注意:Cursor需要保证查询到的字段和取值一一对应,所以不推荐使用
  • 响应式查询
    demo采用的LiveData进行的观察式查询,还可以配合RxJava2,Kotlin的Flow进行响应式查询。

进阶使用

数据库升级降级

@Entities类里增加了新字段后,重新运行已创建过DB的demo会发生崩溃。
Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number.

@Databaseversion升级为2之后再次运行仍然发生崩溃。
A migration from 1 to 2 was required but not found. Please provide the necessary Migration path via RoomDatabase.Builder.addMigration(Migration ...) or allow fallback of the RoomDatabase.Builder.fallbackToDestructiveMigration* methods.

意思是提醒我们调用fallbackToDestructiveMigration()允许升级失败的时候破坏性地删除DB。如果照做的话,将能避免发生崩溃,并且onDestructiveMigration()将被回调。在这个回调里可以试着重新初始化DB。

private fun createInstance(context: Context): MovieDataBase {
    return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
            .fallbackToDestructiveMigration()
            .addCallback(object : Callback() {
                override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
                    super.onDestructiveMigration(db)
                    // Init DB again after db removed.
                    Executors.newFixedThreadPool(5).execute {
                        val dataBase = getInstance(context)
                        val ids = dataBase!!.movieDao().insert(*Utils.initData)
                        dataBase.databaseCreated.postValue(true)
                    }
                }
            })
            .build()
}

但是DB升级后,无论原有数据被删除还是重新初始化都是用户难以接受的。我们可以通过addMigrations()指定升级之后的迁移处理来达到保留旧数据和增加新字段的双赢。

比如如下展示的从版本1升级到版本2,并增加一个默认值为8.0的评分列的迁移处理。

private fun createInstance(context: Context): MovieDataBase {
    return Room.databaseBuilder(context.applicationContext, MovieDataBase::class.java, DATA_BASE_NAME)
            // .fallbackToDestructiveMigration()
            .addMigrations(object : Migration(1, 2) {
                override fun migrate(database: SupportSQLiteDatabase) {
                    database.execSQL("ALTER TABLE movie "
                            + " ADD COLUMN review_score INTEGER NOT NULL DEFAULT 8.0")
                }
            })
            ...
            })
            .build()
}

注意:

  • 数据库降级的话则是调用fallbackToDestructiveMigrationOnDowngrade()来指定在降级的时候删除原有DB,当然可以像上述那样指定drop column的迁移处理
  • 如果想要迁移数据,无论是升级还是降级,必须要给@Databaseversion指定正确的目标版本。Migration迁移的起始版本以及实际的迁移处理migrate()都必不可少
事务处理

当我们的DB操作需要保持一致性,或者查询关联性结果的时候需要保证事务处理。Room提供了@Transaction注解帮助我们快速实现这个需求,它将确保注解内的方法运行在同一个事务模式。

@Dao
public interface MovieDao {
    @Transaction
    default void insetNewAndDeleteOld(Movie newMovie, Movie oldMovie) {
        insert(newMovie);
        delete(oldMovie);
    }
}

需要注意的是,事务处理比较占用性能,避免在事务处理的方法内执行耗时逻辑
另外,@Inset@Delete@Update的处理自动在事务模式进行处理,无需增加@Transaction注解

public long[] insert(final Movie... movies) {
  __db.assertNotSuspendingTransaction();
  __db.beginTransaction();
  try {
    long[] _result = __insertionAdapterOfMovie.insertAndReturnIdsArray(movies);
    __db.setTransactionSuccessful();
    return _result;
  } finally {
    __db.endTransaction();
  }
}

上面的源码也启发我们可以手动执行事务处理,一般来说不需要,取决于具体情况。RoomDatabasebeginTransaction()endTransaction()不推荐外部使用了,可以采用封装好的runInTransaction()实现。

db.runInTransaction(Runnable {
    val database = db.getOpenHelper().getWritableDatabase();

    val contentValues = ContentValues()
    contentValues.put("movie_name", newMovie.getName())
    contentValues.put("actor_name", newMovie.getActor())
    contentValues.put("post_year", newMovie.getYear())
    contentValues.put("review_score", newMovie.getScore())

    database.insert("movie", SQLiteDatabase.CONFLICT_ABORT, contentValues)
    database.delete("movie", "id = " + oldMovie.getId(), null)
})

原理浅谈

简要介绍下Room的部分实现原理。因篇幅有限只展示关键流程,感兴趣者可自行探究具体代码。

RoomDatabase的创建

RoomDatabase$Builderbuild()调用后便通过反射创建了@Databse注解修饰的RoomDatabase实例XXX_Impl

RoomDatabase$Builder#build
Room#getGeneratedImplementation
Class#forName
Class#newInstance

SupportSQLiteDatabase的创建

SupportSQLiteDatabase是模仿SQLiteDatabase作成的接口,供Room框架内部对DB进行操作。由FrameworkSQLiteDatabase实现,其将通过持有SQLiteDatabase实例,代理DB操作。

SupportSQLiteDatabase的创建由增删改查等DB操作触发,需要经历DB的创建,表的创建,表的初始化,升降级以及打开等过程。

创建DB文件
MovieDao_Impl#searchBestMove
RoomDatabase#assertNotSuspendingTransaction
FrameworkSQLiteOpenHelper#getWritableDatabase
OpenHelper#getWritableSupportDatabase
SQLiteOpenHelper#getWritableDatabase
getDatabaseLocked
SQLiteDatabase#openDatabase创建DB文件
创建表
FrameworkSQLiteOpenHelper$OpenHelper#getWrappedDb
FrameworkSQLiteDatabase构造包装成SupportSQLiteDatabase
FrameworkSQLiteOpenHelper$OpenHelper#onCreate
RoomOpenHelper#onCreate
SupportSQLiteOpenHelper.Callback#createAllTables
FrameworkSQLiteDatabase#execSQL创建表
初始化表
RoomOpenHelper#onCreate
SupportSQLiteOpenHelper.Callback#onCreate
RoomDatabase$Callback#onCreate回调初始化处理
升级表

如果DB文件已经存在并且版本和目标版本不一致的话,将执行迁移处理。迁移处理Migration未配置或者执行失败的话将删除DB并执行回调。

SQLiteOpenHelper#getDatabaseLocked
FrameworkSQLiteOpenHelper$OpenHelper#onUpgrade
RoomOpenHelper#onUpgrade
Migration#migrate回调迁移处理
MovieDataBase_Impl#dropAllTables
RoomDatabase$Callback#onDestructiveMigration回调删除后的回调
打开表

DB的创建或升级都正常完成后将回调onOpen()

FrameworkSQLiteOpenHelper$OpenHelper#onOpen
RoomOpenHelper#onOpen
SupportSQLiteOpenHelper.Callback#onOpen
RoomDatabase$Callback#onOpen

注意

Room框架的使用过程中遇到了些容易出错的地方,需要格外留意。

  • RoomDatabase的实例建议采用单例模式管理
  • 不要在UI线程执行DB操作,否则发生异常:Cannot access database on the main thread since it may potentially lock the UI for a long period of time.。调用allowMainThreadQueries()可以回避,但不推荐
  • 不要在Callback#onCreate()里同步执行insert等处理,将会阻塞DB实例的初始化发生异常:getDatabase called recursively
  • @Entity注解类不要提供多个构造函数,使用@Ignore可以回避
  • Callback#onCreate()回调即DB数据的初始化并非由RoomDatabase$Builder#build()触发,而是由具体的增删改查触发,切记

总结

通过上述的实战和原理介绍可以看出,Room的本质是在SQLite的基础上进行封装的抽象层,通过一系列注解让用户能够更简便的使用SQLite。正因为此,它具备了一些优势,值得开发者大胆使用。

  • 通过简单的注解完成接口的定义,简单易上手
  • 注解在编译时验证SQL语句的合法性,提高开发效率
  • 支持使用RxJava2LiveData以及Kotlin协程进行异步查询
  • 相较其他数据库框架SQL执行效率更高

本文DEMO

https://github.com/ellisonchan/JetpackDemo

参考资料

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
Android Room是Google为Android平台开发的一个SQLite对象映射数据库框架,它提供了一种简单的方式来访问SQLite数据库。下面是使用Room进行SQLite查询的基本步骤: 1. 定义实体类:在Room中,表是通过实体类来表示的。你需要定义一个Java类来表示数据库中的每个表,并使用注释来指定表名、列名等信息。 2. 定义DAO接口:DAO(Data Access Object)是用于访问数据库的接口。你需要定义一个接口来提供对实体类的CRUD操作。 3. 创建数据库:使用Room,你可以在应用程序中创建一个SQLite数据库。你需要创建一个继承自RoomDatabase的抽象类,并定义抽象方法来获取DAO对象。 4. 执行查询操作:在DAO接口中定义查询语句,并在应用程序中调用该方法来执行查询操作。以下是一个使用Room进行查询的示例: ```java @Dao public interface UserDao { @Query("SELECT * FROM user WHERE id = :userId") User getUserById(int userId); @Query("SELECT * FROM user WHERE name LIKE :name") List<User> getUsersByName(String name); @Insert void insertUser(User user); } ``` 在上面的示例中,@Query注释指定了查询语句,getUserById方法根据用户ID查询用户,getUsersByName方法根据名称查询用户,insertUser方法将用户插入数据库。 要使用上述查询方法,你需要创建一个RoomDatabase实例并获取UserDao对象。以下是一个使用Room进行查询的示例: ```java UserDatabase db = Room.databaseBuilder(getApplicationContext(), UserDatabase.class, "user.db").build(); UserDao userDao = db.userDao(); // 根据ID查询用户 User user = userDao.getUserById(1); // 根据名称查询用户 List<User> users = userDao.getUsersByName("John"); // 插入用户 User newUser = new User("Alice", "alice@example.com"); userDao.insertUser(newUser); ``` 在上面的示例中,我们创建了一个UserDatabase实例,并使用其userDao()方法获得UserDao对象。然后我们可以使用UserDao对象的方法来执行查询和插入操作。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TechMerger

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值