天天看點

Android 資料庫架構哪家強?1. Room2. Realm3. GreenDAO4.ObjectBox5. SQLDelight6. 總結

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

關于性能方面的比較可以參考下圖,橫坐标是讀寫的資料量,縱坐标是耗時:

Android 資料庫架構哪家強?1. Room2. Realm3. GreenDAO4.ObjectBox5. SQLDelight6. 總結
Android 資料庫架構哪家強?1. Room2. Realm3. GreenDAO4.ObjectBox5. SQLDelight6. 總結

從實驗結果可知 Room 和 GreenDAO 底層都是基于 SQLite,性能接近,在查詢速度上 GreenDAO 表現更好一些; Realm 自有引擎的資料拷貝效率高,複雜對象也無需做映射,在性能表現上優勢明顯; ObjectBox 作為一個 KV 資料庫,性能由于 SQL 也是預期中的。 圖檔缺少 SQLDelight 的曲線,實際性能與 GreeDAO 相近,在查詢速度上優于 Room。

Android 資料庫架構哪家強?1. Room2. Realm3. GreenDAO4.ObjectBox5. SQLDelight6. 總結

空間性能方面可參考上圖( 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 也是不錯的選擇。