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 也是不错的选择。
目录
相关文章
|
人工智能 数据挖掘 API
基于neo4j数据库和dify大模型框架的rag模型搭建——后续补充
基于neo4j数据库和dify大模型框架的rag模型搭建——后续补充
1214 21
基于neo4j数据库和dify大模型框架的rag模型搭建——后续补充
|
Java 数据库 Docker
基于neo4j数据库和dify大模型框架的rag模型搭建
基于neo4j数据库和dify大模型框架的rag模型搭建
4099 35
|
前端开发 Java 编译器
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
671 36
当flutter react native 等混开框架-并且用vscode-idea等编译器无法打包apk,打包安卓不成功怎么办-直接用android studio如何打包安卓apk -重要-优雅草卓伊凡
|
物联网 区块链 vr&ar
未来已来:探索区块链、物联网与虚拟现实技术的融合与应用安卓与iOS开发中的跨平台框架选择
【8月更文挑战第30天】在科技的巨轮下,新技术不断涌现,引领着社会进步。本文将聚焦于当前最前沿的技术——区块链、物联网和虚拟现实,探讨它们各自的发展趋势及其在未来可能的应用场景。我们将从这些技术的基本定义出发,逐步深入到它们的相互作用和集成应用,最后展望它们如何共同塑造一个全新的数字生态系统。
|
XML 数据库 Android开发
Android数据库的使用(增删改查)
本文介绍了一个简单的数据库操作Demo,包含创建数据库、增删改查功能。通过5个按钮分别实现创建数据库、插入数据、删除数据、更新数据和查询数据的操作。代码结构清晰,适合初学者学习Android SQLite数据库基础操作。
428 5
|
数据库 Android开发
Android外部数据库的引用
简介:本文介绍了在Android项目中引用外部数据库的方法。首先,将现成的数据库文件放入项目的`assets`文件夹中(需手动创建)。其次,在APP引导界面通过代码将数据库拷贝至App目录下,确保数据库可用。最后,对数据库进行增删改查等操作。关键步骤包括判断数据库是否存在、使用`AssetManager`读取数据库文件并写入App私有目录,实现外部数据库的顺利集成与使用。
214 2
|
数据库 Android开发 开发者
Android常用的room增删改查语句(外部数据库)
本文分享了将一个原生数据库驱动的单词APP重构为使用Room库的过程及遇到的问题,重点解决了Room中增删改查的常用语句实现。文章通过具体示例(以“forget”表为例),详细展示了如何定义实体类、Dao接口、Database类以及Repository和ViewModel的设计与实现。同时,提供了插入、删除、更新和查询数据的代码示例,包括模糊查询、分页加载等功能。此外,针对外部数据库导入问题,作者建议可通过公众号“计蒙不吃鱼”获取更多支持。此内容适合有一定Room基础的开发者深入学习。
401 0
Android常用的room增删改查语句(外部数据库)
|
SQL XML Java
配置Spring框架以连接SQL Server数据库
最后,需要集成Spring配置到应用中,这通常在 `main`方法或者Spring Boot的应用配置类中通过加载XML配置或使用注解来实现。
830 0
|
SQL 数据库连接 数据库
在C++的QT框架中实现SQLite数据库的连接与操作
以上就是在C++的QT框架中实现SQLite数据库的连接与操作的基本步骤。这些步骤包括创建数据库连接、执行SQL命令、处理查询结果和关闭数据库连接。在实际使用中,你可能需要根据具体的需求来修改这些代码。
921 14
|
存储 缓存 Java
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡
2700 3
java语言后台管理ruoyi后台管理框架-登录提示“无效的会话,或者会话已过期,请重新登录。”-扩展知识数据库中密码加密的方法-问题如何解决-以及如何重置若依后台管理框架admin密码-优雅草卓伊凡