Android資料庫框架該如何選?

語言: CN / TW / HK

theme: smartblue highlight: androidstudio


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

工程依賴

groovy 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 中的欄位名

kotlin @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 轉化為基本型:

```kotlin 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 ```kotlin @Dao interface ArticleDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveArticls(vararg articles: Article)

@Query("SELECT * FROM Article") fun getArticles(): Flow> } `` 然後通過@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

```kotlin @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/

工程依賴

groovy //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 kotlin 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 進行初始化

kotlin Realm.init(context)

DAO

定義 DAO 的關鍵是獲取一個 Realm 例項,然後通過 executeTransactionAwait 開啟事務,在內部完成 CURD 操作。

```kotlin class RealmDao() { private val realm: Realm = Realm.getDefaultInstance()

suspend fun save(articles: List

) { 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> = 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 ,還可以基於自定義配置獲取例項

kotlin 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

工程依賴

```groovy //root build.gradle buildscript { repositories { jcenter() mavenCentral() // add repository } dependencies { ... classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' // greenDao 外掛 ... } }

```

```groovy //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 中的名字 kotlin @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 進行型別轉換

```kotlin class DateConverter : PropertyConverter{ @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 的過程如下: kotlin fun initDao(){ val helper = DaoMaster.DevOpenHelper(this, "test") //建立的資料庫名 val db = helper.writableDb daoSession = DaoMaster(db).newSession() // 建立 DaoMaster 和 DaoSession }

資料讀寫

```kotlin

//插入一條資料,資料型別為 Article 實體類 fun insertArticle(article: Article){
daoSession.articleDao.insertOrReplace(article) }

//返回全部文章 fun getArticles(): List

{
return daoSession.articleDao.queryBuilder().list() }

//按名字查詢一條資料,並返回List fun getArticle(name :String): List

{
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

工程依賴

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

kotlin @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來管理資料 ```kotlin object ObjectBox { lateinit var boxStore: BoxStore private set

fun init(context: Context) { boxStore = MyObjectBox.builder() .androidContext(context.applicationContext) .build() } } ```

BoxStore 的建立需要使用 Application 例項 kotlin ObjectBox.init(context)

DAO

ObjectBox 為實體類提供 Box 物件, 通過 Box 物件實現資料讀寫 ```kotlin class ObjectBoxDao() : DbRepository { // 基於 Article 建立 Box 例項 private val articlesBox: Box

= ObjectBox.boxStore.boxFor(Article::class.java)

override suspend fun save(articles: List

) { articlesBox.put(articles) }

override fun getArticles(): Flow> = 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/

工程依賴

kotlin //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 例項 ```kotlin 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

) { artilces.forEach { article -> queries.insert(article) // insert 是 Article.sq 中的定義的 label } }

override fun getArticles(): Flow> = 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 用來進行資料型別的轉換: ```kotlin class DateAdapter : ColumnAdapter { 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

選型建議

上述個框架目前都在維護中,都存在不少使用者,大家在選型上可以遵循以下原則:

  1. Room 雖然在效能上不具優勢,但是作為 Google 的親兒子,與 Jetpack 全家桶相容最好,而且天然支援協程,如果你的專案只用在 Android 平臺上且對效能不敏感,首推 Room ;
  2. 如果你的專案是一個 KMM 或其他跨平臺應用,那麼建議選擇 SQLDelight ;
  3. 如果你對效能有比較高的需求,那麼 Realm 無疑是更好的選擇 ;
  4. 如果對查詢條件沒有過多要求,那麼可以考慮 KV 型資料庫的 ObjectBox,如果只用在 Android 平臺,那麼前不久 stable 的 DataStore 也是不錯的選擇。