Android 主流数据库框架盘点!
The following article is from AndroidPub Author fundroid
大家在 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)。
https://developer.android.com/training/data-storage/room
工程依赖
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 框架。
https://docs.mongodb.com/realm/sdk/android/
工程依赖
//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 平台进行了优化,运行时的内存开销非常小。
https://github.com/greenrobot/greenDAO
工程依赖
//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 使用一个团队。
https://docs.objectbox.io/kotlin-support
工程依赖
//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。
https://cashapp.github.io/sqldelight/android_sqlite/
工程依赖
//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 之间。
数据来源:https://proandroiddev.com/android-databases-performance-crud-a963dd7bb0eb
选型建议
上述个框架目前都在维护中,都存在不少用户,大家在选型上可以遵循以下原则:
Room 虽然在性能上不具优势,但是作为 Google 的亲儿子,与 Jetpack 全家桶兼容最好,而且天然支持协程,如果你的项目只用在 Android 平台上且对性能不敏感,首推 Room ; 如果你的项目是一个 KMM 或其他跨平台应用,那么建议选择 SQLDelight ; 如果你对性能有比较高的需求,那么 Realm 无疑是更好的选择 ; 如果对查询条件没有过多要求,那么可以考虑 KV 型数据库的 ObjectBox,如果只用在 Android 平台,那么前不久 stable 的 DataStore 也是不错的选择。
---END---
更文不易,点个“在看”支持一下👇