大家在 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
進行初始化
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
執行個體
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 也是不錯的選擇。