Android数据库框架该如何选?

简介: Android数据库框架该如何选?

大家在 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)。

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

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

出身 存储引擎 RxJava Coroutine 附件文件 数据类型
Room Google亲生 SQLite 支持 支持 编译期代码生成 基本型 + TypeConverter
Realm 三方 C++ Core 支持 部分支持 支持复杂类型
GreenDAO 三方 SQLite 不支持 不支持 编译期代码生成 基本型+ PropertyConverter
ObjectBox 三方 Json 支持 不支持 支持复杂类型
SQLDelight 三方 SQLite 支持 支持 手写.sq 基本型 + ColumnAdapter

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

image.png

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

image.png

空间性能方面可参考上图( 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 也是不错的选择。
目录
相关文章
|
4月前
|
物联网 区块链 vr&ar
未来已来:探索区块链、物联网与虚拟现实技术的融合与应用安卓与iOS开发中的跨平台框架选择
【8月更文挑战第30天】在科技的巨轮下,新技术不断涌现,引领着社会进步。本文将聚焦于当前最前沿的技术——区块链、物联网和虚拟现实,探讨它们各自的发展趋势及其在未来可能的应用场景。我们将从这些技术的基本定义出发,逐步深入到它们的相互作用和集成应用,最后展望它们如何共同塑造一个全新的数字生态系统。
|
5月前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台框架解析
在移动应用开发的广阔舞台上,安卓和iOS一直是两大主角。随着技术的进步,开发者们渴望能有一种方式,让他们的应用能同时在这两大平台上运行,而不必为每一个平台单独编写代码。这就是跨平台框架诞生的背景。本文将探讨几种流行的跨平台框架,包括它们的优势、局限性,以及如何根据项目需求选择合适的框架。我们将从技术的深度和广度两个维度,对这些框架进行比较分析,旨在为开发者提供一个清晰的指南,帮助他们在安卓和iOS的开发旅程中,做出明智的选择。
|
1月前
|
算法 JavaScript Android开发
|
22天前
|
开发框架 Dart Android开发
安卓与iOS的跨平台开发:Flutter框架深度解析
在移动应用开发的海洋中,Flutter作为一艘灵活的帆船,正引领着开发者们驶向跨平台开发的新纪元。本文将揭开Flutter神秘的面纱,从其架构到核心特性,再到实际应用案例,我们将一同探索这个由谷歌打造的开源UI工具包如何让安卓与iOS应用开发变得更加高效而统一。你将看到,借助Flutter,打造精美、高性能的应用不再是难题,而是变成了一场创造性的旅程。
|
2月前
|
Java 程序员 API
Android|集成 slf4j + logback 作为日志框架
做个简单改造,统一 Android APP 和 Java 后端项目打印日志的体验。
123 1
|
3月前
|
前端开发 Java 数据库
💡Android开发者必看!掌握这5大框架,轻松打造爆款应用不是梦!🏆
在Android开发领域,框架犹如指路明灯,助力开发者加速应用开发并提升品质。本文将介绍五大必备框架:Retrofit简化网络请求,Room优化数据库访问,MVVM架构提高代码可维护性,Dagger 2管理依赖注入,Jetpack Compose革新UI开发。掌握这些框架,助你在竞争激烈的市场中脱颖而出,打造爆款应用。
414 3
|
3月前
|
编译器 Android开发 开发者
带你了解Android Jetpack库中的依赖注入框架:Hilt
本文介绍了Hilt,这是Google为Android开发的依赖注入框架,基于Dagger构建,旨在简化依赖注入过程。Hilt通过自动化的组件和注解减少了DI的样板代码,提高了应用的可测试性和可维护性。文章详细讲解了Hilt的主要概念、基本用法及原理,帮助开发者更好地理解和应用Hilt。
90 8
|
4月前
|
搜索推荐 前端开发 算法
基于用户画像及协同过滤算法的音乐推荐系统,采用Django框架、bootstrap前端,MySQL数据库
本文介绍了一个基于用户画像和协同过滤算法的音乐推荐系统,使用Django框架、Bootstrap前端和MySQL数据库构建,旨在为用户提供个性化的音乐推荐服务,提高推荐准确性和用户满意度。
298 7
基于用户画像及协同过滤算法的音乐推荐系统,采用Django框架、bootstrap前端,MySQL数据库
|
4月前
|
设计模式 Java Android开发
探索安卓应用开发:从新手到专家的旅程探索iOS开发中的SwiftUI框架
【8月更文挑战第29天】本文旨在通过一个易于理解的旅程比喻,带领读者深入探讨安卓应用开发的各个方面。我们将从基础概念入手,逐步过渡到高级技术,最后讨论如何维护和推广你的应用。无论你是编程新手还是有经验的开发者,这篇文章都将为你提供有价值的见解和实用的代码示例。让我们一起开始这段激动人心的旅程吧!
|
4月前
|
Android开发
基于Amlogic 安卓9.0, 驱动简说(三):使用misc框架,让驱动更简单
如何使用Amlogic T972安卓9.0系统上的misc框架来简化驱动程序开发,通过misc框架自动分配设备号并创建设备文件,从而减少代码量并避免设备号冲突。
56 0
基于Amlogic 安卓9.0, 驱动简说(三):使用misc框架,让驱动更简单