Android数据库框架该如何选?

10,209 阅读8分钟

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

大家在 Android 上做数据持久化经常会用到数据库。除了借助 SQLiteHelper 以外,业界也有不少成熟的三方库供大家使用。

本文就这些三方库做一个横向对比,供大家在技术选型时做个参考。

  • Room
  • Relam
  • GreenDAO
  • ObjectBox
  • SQLDelight

以 Article 类型的数据存储为例,我们如下设计数据库表:

Field NameTypeLengthPrimaryDescription
idLong20yes文章id
authorText10作者
titleText20标题
descText50摘要
urlText50文章链接
likesInt10点赞数
updateDateText20更新日期

1. Room

Room 是 Android 官方推出的 ORM 框架,它提供了一个基于 SQLite 抽象层,屏蔽了 SQLite 的访问细节,更容易与官方推荐的 AAC 组件搭配实现单一事件来源(Single Source of Truth)。

developer.android.com/training/da…

工程依赖

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 框架。

docs.mongodb.com/realm/sdk/a…

工程依赖

//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 平台进行了优化,运行时的内存开销非常小。

github.com/greenrobot/…

工程依赖

//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 使用一个团队。

docs.objectbox.io/kotlin-supp…

工程依赖

//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。

cashapp.github.io/sqldelight/…

工程依赖

//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 的直接操作,更加类型安全的读写数据库

最后,通过一张表格总结一下各种框架的特点:

出身存储引擎RxJavaCoroutine附件文件数据类型
RoomGoogle亲生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 之间。

数据来源: proandroiddev.com/android-dat…

选型建议

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

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