大家在 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 相关的三个文件:DaoMasterDaoSessiionArticleDao ,

  • 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

关于性能方面的比较可以参考下图,横坐标是读写的数据量,纵坐标是耗时:

android room数据库 定义字段 android数据库框架选择_Room

android room数据库 定义字段 android数据库框架选择_SQLDelight_02

从实验结果可知 Room 和 GreenDAO 底层都是基于 SQLite,性能接近,在查询速度上 GreenDAO 表现更好一些; Realm 自有引擎的数据拷贝效率高,复杂对象也无需做映射,在性能表现上优势明显; ObjectBox 作为一个 KV 数据库,性能由于 SQL 也是预期中的。 图片缺少 SQLDelight 的曲线,实际性能与 GreeDAO 相近,在查询速度上优于 Room。

android room数据库 定义字段 android数据库框架选择_android_03

空间性能方面可参考上图( 50K 条记录的内存占用情况)。 Realm 需要加载 so 同时为了提高性能缓存数据较多,运行时内存占用最大,SQLite 系的数据库依托平台服务,内存开销较小,其中 GreenDAO 在运行时内存的优化是最好的。 ObjectBox 介于 SQLite 与 Realm 之间。

数据来源: https://proandroiddev.com/android-databases-performance-crud-a963dd7bb0eb

选型建议

上述个框架目前都在维护中,都存在不少用户,大家在选型上可以遵循以下原则:

  1. Room 虽然在性能上不具优势,但是作为 Google 的亲儿子,与 Jetpack 全家桶兼容最好,而且天然支持协程,如果你的项目只用在 Android 平台上且对性能不敏感,首推 Room ;
  2. 如果你的项目是一个 KMM 或其他跨平台应用,那么建议选择 SQLDelight ;
  3. 如果你对性能有比较高的需求,那么 Realm 无疑是更好的选择 ;
  4. 如果对查询条件没有过多要求,那么可以考虑 KV 型数据库的 ObjectBox,如果只用在 Android 平台,那么前不久 stable 的 DataStore 也是不错的选择。