大家在 Android 上做数据持久化经常会用到数据库。除了借助 SQLiteHelper 以外,业界也有不少成熟的三方库供大家使用。
本文就这些三方库做一个横向对比,供大家在技术选型时做个参考。
- Room
- Relam
- GreenDAO
- ObjectBox
- SQLDelight
以 Article 类型的数据存储为例,我们如下设计数据库表:
Field Name | Type | Length | Primary | Description |
id | Long | 20 | yes | 文章id |
author | Text | 10 | 作者 | |
title | Text | 20 | 标题 | |
desc | Text | 50 | 摘要 | |
url | Text | 50 | 文章链接 | |
likes | Int | 10 | 点赞数 | |
updateDate | Text | 20 | 更新日期 |
1. Room
Room 是 Android 官方推出的 ORM 框架,它提供了一个基于 SQLite 抽象层,屏蔽了 SQLite 的访问细节,更容易与官方推荐的 AAC 组件搭配实现单一事件来源(Single Source of Truth)。
工程依赖
implementation "androidx.room:room-runtime:$latest_version" implementation "androidx.room:room-ktx:$latest_version" kapt "androidx.room:room-compiler:$latest_version" // 注解处理器
Entity 定义数据库表结构
Room 使用 data class 定义 Entity
代表 db 的表结构, @PrimaryKey
标识主键, @ColumnInfo
定义属性在 db 中的字段名
@Entity data class Article( @PrimaryKey val id: Long, val author: String, val title: String, val desc: String, val url: String, val likes: Int, @ColumnInfo(name = "updateDate") @TypeConverters(DateTypeConverter::class) val date: Date, )
Room 底层基于 SQLite 所以只能存储基本型数据,任何对象类型必须通过 TypeConvert
转化为基本型:
class Converters { @TypeConverter fun fromString(value: String?): Date? { return format.parse(value) } @TypeConverter fun dateToString(date: Date?): String? { return SimpleDateFormat("yyyy-MM-dd", Locale.US).format(date) } }
DAO
Room 的最主要特点是基于注解生成 CURD 代码,减少手写代码的工作量。
首先通过 @Dao
创建 DAO
@Dao interface ArticleDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveArticls(vararg articles: Article) @Query("SELECT * FROM Article") fun getArticles(): Flow<List<Article>> }
然后通过 @Insert
, @Update
, @Delete
等定义相关方法用来更新数据;定义 @Query
方法从数据库读取信息,SELECT
的 SQL 语句作为其注解的参数。
@Query
方法支持 RxJava 或者 Coroutine Flow 类型的返回值,KAPT 会根据返回值类型生成相应代码。当 db 的数据更新造成 query 的 Observable
或者 Flow
结果发生变化时,订阅方会自动收到新的数据。
注意:虽然 Room 也支持 LiveData 类型的返回值,LiveData 是一个 Androd 平台对象。一个比较理想的 MVVM 架构,其数据层最好是 Android 无关的,所以不推荐使用 LiveData 作为返回值类型
AppDatabase 实例
最后,通过创建个 Database
实例来获取 DAO
@Database(entities = [Article::class], version = 1) // 定义当前db的版本以及数据库表(数组可定义多张表) @TypeConverters(value = [DateTypeConverter::class]) // 定义使用到的 type converters abstract class AppDatabase : RoomDatabase() { abstract fun articleDao(): ArticleDao companion object { @Volatile private var instance: AppDatabase? = null fun getInstance(context: Context): AppDatabase = instance ?: synchronized(this) { instance ?: buildDatabase(context).also { instance = it } } private fun buildDatabase(context: Context): AppDatabase = Room.databaseBuilder(context, AppDatabase::class.java, "ArticleDb") .fallbackToDestructiveMigration() // 数据库升级策略 .build() } }
2. Realm
Realm 是一个专门针对移动端设计的数据库,不同于 Room 等其他 ORM 框架,Realm 底层并不依赖 SQLite,有自己的一套基于零拷贝的存储引擎,在速度上明显优于其他 ORM 框架。
工程依赖
//root build.gradle dependencies { ... classpath "io.realm:realm-gradle-plugin:$realmVersion" ... } // module build.gradle apply plugin: 'com.android.application' apply plugin: 'realm-android'
Entity
Realm 要求 Entity 必须要有一个空构造函数,所以不能使用 data class 定义。 Entity 必须继承自 RealmObject
open class RealmArticle : RealmObject() { @PrimaryKey val id: Long = 0L, val author: String = "", val title: String = "", val desc: String = "", val url: String = "", val likes: Int = 0, val updateDate: Date = Date(), }
除了整形、字符串等基本型,Realm 也支持存储例如 Date
这类的常见的对象类型,Realm 内部会做兼容处理。你也可以在 Entity 中使用自定义类型,但需要保证这个类也是 RealmObject
的派生类。
初始化
要使用 Realm 需要传入 Application
进行初始化
Realm.init(context)
DAO
定义 DAO 的关键是获取一个 Realm 实例,然后通过 executeTransactionAwait
开启事务,在内部完成 CURD 操作。
class RealmDao() { private val realm: Realm = Realm.getDefaultInstance() suspend fun save(articles: List<Article>) { realm.executeTransactionAwait { r -> // open a realm transaction for (article in articles) { if (r.where(RealmArticle::class.java).equalTo("id", article.id).findFirst() != null) { continue } val realmArticle = r.createObject(Article::class.java, article.id) // create object (table) // save data realmArticle.author = article.author realmArticle.desc = article.desc realmArticle.title = article.title realmArticle.url = article.url realmArticle.likes = article.likes realmArticle.updateDate = article.updateDate } } } fun getArticles(): Flow<List<Article>> = callbackFlow { // wrap result in callback flow `` realm.executeTransactionAwait { r -> val articles = r.where(RealmArticle::class.java).findAll() articles.forEach { offer(it) } } awaitClose { println("End Realm") } } }
除了获取默认配置的 Realm ,还可以基于自定义配置获取实例
val config = RealmConfiguration.Builder() .name("default-realm") .allowQueriesOnUiThread(true) .allowWritesOnUiThread(true) .compactOnLaunch() .inMemory() .build() // set this config as the default realm Realm.setDefaultConfiguration(config)
3. GreenDAO
greenDao 是 Android 平台上的开源框架,跟 Room 一样也是一套基于 SQLite 的轻量级 ORM 解决方案。greenDAO 针对 Android 平台进行了优化,运行时的内存开销非常小。
工程依赖
//root build.gradle buildscript { repositories { jcenter() mavenCentral() // add repository } dependencies { ... classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' // greenDao 插件 ... } }
//module build.gradle //添加 GreenDao插件 apply plugin: 'org.greenrobot.greendao' dependencies { //GreenDao依赖添加 implementation 'org.greenrobot:greendao:latest_version' } greendao { // 数据库版本号 schemaVersion 1 // 生成数据库文件的目录 targetGenDir 'src/main/java' // 生成的数据库相关文件的包名 daoPackage 'com.sample.greendao.gen' }
Entity
greenDAO 的 Entity 定义和 Room 类似,@Property
用来定义属性在 db 中的名字
@Entity data class Article( @Id(assignable = true) val id: Long, val author: String, val title: String, val desc: String, val url: String, val likes: Int, @Property(nameInDb = "updateDate") @Convert(converter = DateConvert::class.java, columnType = String.class) val date: Date, )
greenDAO 只支持基本型数据,复杂类型通过 PropertyConverter
进行类型转换
class DateConverter : PropertyConverter<Date, String>{ @Override fun convertToEntityProperty(value: Integer): Date { return format.parse(value) } @Override fun convertToDatabaseValue(date: Date): String { return SimpleDateFormat("yyyy-MM-dd", Locale.US).format(date) } }
生成 DAO 相关文件
定义 Entity 后,编译工程会在我们配置的 com.sample.greendao.ge
目录下生成 DAO 相关的三个文件:DaoMaster
,DaoSessiion
,ArticleDao
,
- DaoMaster: 管理数据库连接,内部持有着数据库对象 SQLiteDatabase,
- DaoSession:每个数据库连接可以开放多个 session,而 session 的开销很小,无需反复创建 connection
- XXDao:通过 DaoSessioin 获取访问具体 XX 实体的 DAO
初始化 DaoSession 的过程如下:
fun initDao(){ val helper = DaoMaster.DevOpenHelper(this, "test") //创建的数据库名 val db = helper.writableDb daoSession = DaoMaster(db).newSession() // 创建 DaoMaster 和 DaoSession }
数据读写
//插入一条数据,数据类型为 Article 实体类 fun insertArticle(article: Article){ daoSession.articleDao.insertOrReplace(article) } //返回全部文章 fun getArticles(): List<Article> { return daoSession.articleDao.queryBuilder().list() } //按名字查找一条数据,并返回List fun getArticle(name :String): List<Article> { return daoSession.articleDao.queryBuilder() .where(ArticleDao.Properties.Title.eq(name)) .list() }
通过 daoSession 获取 ArticleDao,而后可以通过 QueryBuilder
添加条件进行调价查询。
4.ObjectBox
ObjectBox 是专为小型物联网和移动设备打造的 NoSQL 数据库,它是一个键值存储数据库,非列式存储,在非关系型数据的存储场景中性能上更具优势。ObjectBox 和 GreenDAO 使用一个团队。
工程依赖
//root build.gradle dependencies { ... classpath "io.objectbox:objectbox-gradle-plugin:$latest_version" ... } // module build.gradle apply plugin: 'com.android.application' apply plugin: 'io.objectbox' ... dependencies { ... implementation "io.objectbox:objectbox-kotlin:$latest_version" ... }
Entity
@Entity data class Article( @Id(assignable = true) val id: Long, val author: String, val title: String, val desc: String, val url: String, val likes: Int, @NameInDb("updateDate") val date: Date, )
ObjectBox 的 Entity 和自家的 greenDAO 很像,只是个别注解的名字不同,例如使用 @NameInDb
替代 @Property
等
BoxStore
需要为 ObjectBox 创建一个 BoxStore
来管理数据
object ObjectBox { lateinit var boxStore: BoxStore private set fun init(context: Context) { boxStore = MyObjectBox.builder() .androidContext(context.applicationContext) .build() } }
BoxStore
的创建需要使用 Application
实例
ObjectBox.init(context)
DAO
ObjectBox 为实体类提供 Box 对象, 通过 Box 对象实现数据读写
class ObjectBoxDao() : DbRepository { // 基于 Article 创建 Box 实例 private val articlesBox: Box<Article> = ObjectBox.boxStore.boxFor(Article::class.java) override suspend fun save(articles: List<Article>) { articlesBox.put(articles) } override fun getArticles(): Flow<List<Article>> = callbackFlow { // 将 query 结果转换为 Flow val subscription = articlesBox.query().build().subscribe() .observer { offer(it) } awaitClose { subscription.cancel() } } }
ObjectBox 的 query 可以返回 RxJava 的结果, 如果要使用 Flow 等其他形式,需要自己做一个转换。
5. SQLDelight
SQLDelight 是 Square 家的开源库,可以基于 SQL 语句生成类型安全的 Kotlin 以及其他平台语言的 API。
工程依赖
//root build.gradle dependencies { ... classpath "com.squareup.sqldelight:gradle-plugin:$latest_version" ... } // module build.gradle apply plugin: 'com.android.application' apply plugin: 'com.squareup.sqldelight' ... dependencies { ... implementation "com.squareup.sqldelight:android-driver:$latest_version" implementation "com.squareup.sqldelight:coroutines-extensions-jvm:$delightVersion" ... }
.sq 文件
DqlDelight 的工程结构与其他框架有所不同,需要在 src/main/java
的同级创建 src/main/sqldelight
目录,并按照包名建立子目录,添加 .sq
文件
# Article.sq import java.util.Date; CREATE TABLE Article( id INTEGER PRIMARY KEY, author TEXT, title TEXT, desc TEXT, url TEXT, likes INTEGER, updateDate TEXT as Date ); selectAll: #label: selectAll SELECT * FROM Article; insert: #label: insert INSERT OR IGNORE INTO Article(id, author, title, desc, url, likes, updateDate) VALUES ?;
Article.sq
中对 SQL 语句添加 label 会生成对应的 .kt
文件 ArticleQueries.kt
。 我们创建的 DAO 也是通过 ArticleQueries
完成 SQL 的 CURD
DAO
首先需要创建一个 SqlDriver 用来进行 SQL 数据库的连接、事务等管理,Android平台需要传入 Context
, 基于 SqlDriver 获取 ArticleQueries
实例
class SqlDelightDao() { // 创建SQL驱动 private val driver: SqlDriver = AndroidSqliteDriver(Database.Schema, context, "test.db") // 基于驱动创建db实例 private val database = Database(driver, Article.Adapter(DateAdapter())) // 获取 ArticleQueries 实例 private val queries = database.articleQueries override suspend fun save(artilces: List<Article>) { artilces.forEach { article -> queries.insert(article) // insert 是 Article.sq 中的定义的 label } } override fun getArticles(): Flow<List<Article>> = queries.selectAll() // selectAll 是 Article.sq 中的定义的 label .asFlow() // convert to Coroutines Flow .map { query -> query.executeAsList().map { article -> Article( id = article.id, author = article.author desc = article.desc title = article.title url = article.url likes = article.likes updateDate = article.updateDate ) } } }
类似于 Room 的 TypeConverter
,SQLDelight 提供了 ColumnAdapter
用来进行数据类型的转换:
class DateAdapter : ColumnAdapter<Date, String> { companion object { private val format = SimpleDateFormat("yyyy-MM-dd", Locale.US) } override fun decode(databaseValue: String): Date = format.parse(databaseValue) ?: Date() override fun encode(value: Date): String = format.format(value) }
6. 总结
前文走马观花地介绍了各种数据库的基本使用,更详细的内容还请移步官网。各框架在 Entity 定义以及 DAO 的生成上各具特色,但是设计目的殊途同归:减少对 SQL 的直接操作,更加类型安全的读写数据库。
最后,通过一张表格总结一下各种框架的特点:
出身 | 存储引擎 | RxJava | Coroutine | 附件文件 | 数据类型 | |
Room | Google亲生 | SQLite | 支持 | 支持 | 编译期代码生成 | 基本型 + TypeConverter |
Realm | 三方 | C++ Core | 支持 | 部分支持 | 无 | 支持复杂类型 |
GreenDAO | 三方 | SQLite | 不支持 | 不支持 | 编译期代码生成 | 基本型+ PropertyConverter |
ObjectBox | 三方 | Json | 支持 | 不支持 | 无 | 支持复杂类型 |
SQLDelight | 三方 | SQLite | 支持 | 支持 | 手写.sq | 基本型 + ColumnAdapter |
关于性能方面的比较可以参考下图,横坐标是读写的数据量,纵坐标是耗时:
从实验结果可知 Room 和 GreenDAO 底层都是基于 SQLite,性能接近,在查询速度上 GreenDAO 表现更好一些; Realm 自有引擎的数据拷贝效率高,复杂对象也无需做映射,在性能表现上优势明显; ObjectBox 作为一个 KV 数据库,性能由于 SQL 也是预期中的。 图片缺少 SQLDelight 的曲线,实际性能与 GreeDAO 相近,在查询速度上优于 Room。
空间性能方面可参考上图( 50K 条记录的内存占用情况)。 Realm 需要加载 so 同时为了提高性能缓存数据较多,运行时内存占用最大,SQLite 系的数据库依托平台服务,内存开销较小,其中 GreenDAO 在运行时内存的优化是最好的。 ObjectBox 介于 SQLite 与 Realm 之间。
选型建议
上述个框架目前都在维护中,都存在不少用户,大家在选型上可以遵循以下原则:
- Room 虽然在性能上不具优势,但是作为 Google 的亲儿子,与 Jetpack 全家桶兼容最好,而且天然支持协程,如果你的项目只用在 Android 平台上且对性能不敏感,首推 Room ;
- 如果你的项目是一个 KMM 或其他跨平台应用,那么建议选择 SQLDelight ;
- 如果你对性能有比较高的需求,那么 Realm 无疑是更好的选择 ;
- 如果对查询条件没有过多要求,那么可以考虑 KV 型数据库的 ObjectBox,如果只用在 Android 平台,那么前不久 stable 的 DataStore 也是不错的选择。