When you’re doing data persistence on Android, you often use databases. In addition to SQLiteHelper, there are several mature three-party libraries available in the industry.

This paper makes a horizontal comparison of these tripartite libraries for everyone to make a reference in the selection of technology.

  • Room
  • Relam
  • GreenDAO
  • ObjectBox
  • SQLDelight

For an Article type data store, we design the database table as follows: | Field Name | Type | Length | Primary | Description | | : — | — – | : — | — – | : – | – – | | | id Long | | yes id | | article 20 | author | Text 10 | | | The author | | in the | Text 20 | | | title | | desc | Text 50 | | | the | | | url Text 50 | | | article links | | likes | Int 10 | | | thumb up several | | updateDate | Text 20 | | | | update date

1. Room

Room is the official ORM framework for Android. It provides an ABSTRACTION layer based on SQLite, which hides SQLite access details and makes it easier to implement a Single Source of Truth with the official recommended AAC component.

Developer.android.com/training/da…

Engineering depends on

implementation "androidx.room:room-runtime:$latest_version"
implementation "androidx.room:room-ktx:$latest_version"
kapt "androidx.room:room-compiler:$latest_version" // Annotation handler
Copy the code

Entity defines the database table structure

Room uses data class to define the table structure of the Entity representing the db, @primarykey to identify the PrimaryKey, and @columninfo to define the field names of attributes in the 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,
)
Copy the code

Room is based on SQLite, so it can only store primitive data. Any object type must be converted to primitive via 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)
  }
}
Copy the code

DAO

The main feature of Room is that it generates CURD code based on annotations, reducing the amount of handwritten code.

The Dao is first created with @DAO

@Dao
interface ArticleDao {
  @Insert(onConflict = OnConflictStrategy.REPLACE)
  suspend fun saveArticls(vararg articles: Article)

  @Query("SELECT * FROM Article")
  fun getArticles(a): Flow<List<Article>>
}
Copy the code

@insert, @update, @delete, etc. The @Query method is defined to read information from the database, with the SQL statement of SELECT as its annotation parameter.

The @Query method supports return values of type RxJava or Coroutine Flow, and KAPT generates code based on the return value type. The subscriber automatically receives new data when db data updates result in changes to the Query Observable or Flow result.

Note: LiveData is an Androd platform object, although Room also supports a return value of type LiveData. For an ideal MVVM architecture, the data layer should be Android independent, so using LiveData as the return value type is not recommended

AppDatabase instance

Finally, get the DAO by creating a Database instance

@Database(entities = [Article::class], version = 1) // Define the current version of db and database table (array can define multiple tables)
@TypeConverters(value = [DateTypeConverter::class]) // Define the type converters used
abstract class AppDatabase : RoomDatabase() {
  abstract fun articleDao(a): 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() // Database upgrade policy
            .build()
  }
}
Copy the code

2. Realm

Realm is a database designed specifically for mobile devices. Unlike other ORM frameworks such as Room, Realm doesn’t rely on SQLite and has its own zero-copy storage engine, which is significantly faster than other ORM frameworks.

Docs.mongodb.com/realm/sdk/a…

Engineering depends on

//root build.gradle
dependencies {
    ...
    classpath "io.realm:realm-gradle-plugin:$realmVersion". }// module build.gradle
apply plugin: 'com.android.application'
apply plugin: 'realm-android'
Copy the code

Entity

Realm requires an Entity to have an empty constructor, so data class definitions cannot be used. Entity must inherit from 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(),
}
Copy the code

In addition to primitives such as integers and strings, Realm also supports storing common object types such as dates, with internal compatibility processing. You can also use custom types in Entity, but make sure that class is also a derived class from RealmObject.

Initialize the

To use a Realm, pass in the Application for initialization

Realm.init(context)
Copy the code

DAO

The key to defining a DAO is to get an instance of a Realm and then open the transaction with executeTransactionAwait to complete the CURD operation internally.

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(a): 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")}}}Copy the code

In addition to getting a Realm configured by default, you can also get instances based on custom configurations

val config = RealmConfiguration.Builder()
    .name("default-realm")
    .allowQueriesOnUiThread(true)
    .allowWritesOnUiThread(true)
    .compactOnLaunch()
    .inMemory()
    .build()
// set this config as the default realm
Realm.setDefaultConfiguration(config)
Copy the code

3. GreenDAO

GreenDao is an open source framework for Android. Like Room, greenDao is a lightweight ORM solution based on SQLite. GreenDAO is optimized for the Android platform and has very little memory overhead at runtime.

Github.com/greenrobot/…

Engineering depends on

//root build.gradle
buildscript {
    repositories {
        jcenter()
        mavenCentral() // add repository
    }
    dependencies {
        ...
        classpath 'org. Greenrobot: greendao - gradle - plugin: 3.3.0' / / greenDao plug-in. }}Copy the code
//module build.gradle

// Add the GreenDao plugin
apply plugin: 'org.greenrobot.greendao'

dependencies {
    //GreenDao dependency is added
    implementation 'org.greenrobot:greendao:latest_version'
}


greendao {
    // Database version number
    schemaVersion 1
    // Directory to generate database files
    targetGenDir 'src/main/java'
    // The package name of the generated database-related files
    daoPackage 'com.sample.greendao.gen'
}


Copy the code

Entity

GreenDAO’s Entity definition is similar to Room’s. @Property is used to define the name of the Property in 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,
)
Copy the code

GreenDAO only supports basic data. Complex types are converted via 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)
  }
}
Copy the code

Generate DAO related files

After defining the Entity, the compilation project will generate three dao-related files in the com.sample.greendao.ge directory that we configured: DaoMaster, DaoSessiion, ArticleDao,

  • DaoMaster: Manages database connections, internally holding the database object SQLiteDatabase,
  • DaoSession: Each database connection can open multiple sessions, but the session cost is small, and there is no need to repeatedly create a connection
  • XXDao: Use DaoSessioin to obtain daOs that access specific XX entities

DaoSession initializes DaoSession as follows:

fun initDao(a){
    val helper = DaoMaster.DevOpenHelper(this."test") // The name of the database created
    val db = helper.writableDb
    daoSession = DaoMaster(db).newSession() // Create DaoMaster and DaoSession
}
Copy the code

Read and write data


// Insert a piece of data of type Article entity class
fun insertArticle(article: Article){  
    daoSession.articleDao.insertOrReplace(article)
}

// Return all articles
fun getArticles(a): List<Article> {   
    return daoSession.articleDao.queryBuilder().list()
}


// Find an item of data by name and return List
fun getArticle(name :String): List<Article> {   
    return daoSession.articleDao.queryBuilder()
          .where(ArticleDao.Properties.Title.eq(name))
          .list()
}

Copy the code

ArticleDao can be obtained through daoSession, and then conditions can be added through QueryBuilder to adjust the price query.

4.ObjectBox

ObjectBox is a NoSQL database designed for small Internet of Things and mobile devices. It is a key-value storage database, non-column storage, and has more performance advantages in non-relational data storage scenarios. ObjectBox and GreenDAO use a team.

Docs. Objectbox. IO/kotlin – supp…

Engineering depends on

//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". }Copy the code

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,
)
Copy the code

The Entity of an ObjectBox is very similar to greenDAO, except that the name of the individual annotation is different, such as @nameindb instead of @Property

BoxStore

You need to create a BoxStore for your ObjectBox to manage the data

object ObjectBox {
  lateinit var boxStore: BoxStore
    private set

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

The creation of BoxStore requires the use of an Application instance

ObjectBox.init(context)
Copy the code

DAO

ObjectBox provides Box objects for entity classes to read and write data from

class ObjectBoxDao() : DbRepository {
  // Create a Box instance based on Article
  private val articlesBox: Box<Article> = ObjectBox.boxStore.boxFor(Article::class.java)
  
  override suspend fun save(articles: List<Article>) {
      articlesBox.put(articles)
  }

  override fun getArticles(a): Flow<List<Article>> = callbackFlow { 
    // Convert the query result to Flow
    val subscription = articlesBox.query().build().subscribe()
        .observer { offer(it) }
    awaitClose { subscription.cancel() }
  }
}
Copy the code

ObjectBox’s Query can return RxJava results, but if you want to use other forms such as Flow, you need to do your own conversion.

5. SQLDelight

SQLDelight is Square’s open source library that generates type-safe APIS for Kotlin and other platform languages based on SQL statements.

Cashapp. Making. IO/sqldelight /…

Engineering depends on

//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". }Copy the code

The sq file

The project structure of DqlDelight is different from other frameworks. You need to create a SRC /main/ SQLDelight directory at the same level of SRC /main/ Java, create a subdirectory according to the package name, and add the.sq file

# 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 ? ;Copy the code

Adding label to SQL statements in Article. Sq generates the corresponding.kt file articlequeries.kt. The DAO we created was also SQL CURD with ArticleQueries

DAO

First, we need to create a SqlDriver for SQL database connection, transaction management, etc. Android platform needs to pass Context, based on SqlDriver to obtain the ArticleQueries instance

 class SqlDelightDao() {
  // Create the SQL driver
  private val driver: SqlDriver = AndroidSqliteDriver(Database.Schema, context, "test.db")
  // Create db instances based on drivers
  private val database = Database(driver, Article.Adapter(DateAdapter()))
  // Get the ArticleQueries instance
  private val queries = database.articleQueries
  
  override suspend fun save(artilces: List<Article>) {
    artilces.forEach { article ->
      queries.insert(article) // Insert is the label defined in Article. Sq}}override fun getArticles(a): Flow<List<Article>> = 
      queries.selectAll() // selectAll is the label defined in Article. Sq
      .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
          )
        }
      }
}
Copy the code

Similar to Room’s TypeConverter, SQLDelight provides ColumnAdapter for data type conversions:

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)
}
Copy the code

6. Summary

Above briefly introduced the basic use of various databases, more detailed content please go to the official website. Each framework has its own features in Entity definition and DAO generation, but the design goal is the same: less direct manipulation of SQL and more type-safe reading and writing of databases.

Finally, a table summarizes the features of each framework:

| | | | background storage engine RxJava | Coroutine | | attachment file data type | | : — | — – | : — | — – | : — | — – | : — | — – | : – | | Room | Google biological | SQLite support | | | | basic compile-time code + TypeConverter | | Realm | tripartite | Core c + + support | | part support | | free complex type | | GreenDAO | | three parties SQLite | does not support | does not support code generation | | compile-time basic + PropertyConverter | Tripartite | | ObjectBox | Json support | | does not support | | free complex type | | SQLDelight | | three parties SQLite support | | | handwritten. Sq | basic + ColumnAdapter |

For performance comparisons, see the following figure, where the horizontal axis is the amount of data read and written and the vertical axis is the time spent:

The experimental results show that both Room and GreenDAO are based on SQLite with similar performance. GreenDAO performs better in query speed. Realm’s own engine provides high data copy efficiency and no need to map complex objects. ObjectBox as a KV database, performance due to SQL is also expected. The image lacks SQLDelight’s curve, and its actual performance is similar to GreeDAO’s and better than Room’s in query speed.

For space performance, see the figure above (memory usage of 50K records). SQLite databases rely on platform services and have a low memory overhead. GreenDAO has the best memory optimization at runtime. ObjectBox is between SQLite and Realm.

Data sources: proandroiddev.com/android-dat…

Selection Suggestions

The above frameworks are currently under maintenance, and there are many users. We can follow the following principles in selection:

  1. Room may not have an advantage in terms of performance, but as a son of Google, it is the most compatible with Jetpack and naturally supports coroutines. If your project is only on The Android platform and not sensitive to performance, Room is the first choice.
  2. SQLDelight is recommended if your project is a KMM or other cross-platform application;
  3. If you have high performance requirements, then Realm is definitely the better choice;
  4. If you don’t have too many query criteria, consider the kV-type database ObjectBox. If you only use Android, then stable DataStore is a good choice.