Android - Android Architecture Components - Room 在 Kotlin 中使用 (1)

參考文章

Android Room 官方文檔

Room Persistence Library(官網文檔翻譯)

在kotlin中使用room(Room Persistence Library)和遇到的坑

Android官方ORM框架ROOM(Google I/O 2017)

囉嗦

Room 不瞭解的可以看上面文章就可以了,我就不做過多贅述。每個例子都有一個環境或場景,好吧,場景如下:

描述

用戶擁有哪些書

User

字段 說明
u_id 序號
u_name 姓名
u_phone 電話
u_create_date 創建日期
u_update_date 更新日期
u_status 狀態

Book

字段 說明
b_id 序號
b_name 書名
b_author 作者
b_copyright 出版
b_price 價格
b_status 狀態
user_id 用戶id

1. 環境配置

kotlin 環境不再贅述,自行操作,或者直接使用 Android Studio 3.0 新建kotlin 工程。

如果是 3.0 建的kotlin 項目, 則 project/build.gradle 下自己帶有 google maven 倉庫。

allprojects {
    repositories {
        google()
        jcenter()
        mavenCentral()
    }
}

如果是 2.x 配置的 kotlin 環境,則在 project/build.gradle 下配置google maven 倉庫:

allprojects {
    repositories {
        jcenter()
        maven { url 'https://maven.google.com' }
    }
}

參考(牆):https://developer.android.com/topic/libraries/architecture/adding-components.html

配置 Room 依賴

如果是 3.0 推薦使用下面方式

implementation 'android.arch.persistence.room:runtime:1.0.0-alpha5'
// kotlin
kapt "android.arch.persistence.room:compiler:1.0.0-alpha5"
// java
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha5"
// 測試 room
testCompile 'android.arch.persistence.room:testing:1.0.0-alpha5'
// 配合 rxjava2 使用
implementation 'android.arch.persistence.room:rxjava2:1.0.0-alpha5'

如果 是 2.x ,將 implementation 換成 compile 即可,注意:

如果是 kotlin 環境,使用 kapt 來操作註解:

kapt "android.arch.persistence.room:compiler:1.0.0-alpha5"

如果是 java 環境,使用 annotationProcessor 來操作註解:

annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha5"

當然,同時放置也沒事。

2. 基本使用

先看看要實現的內容,

這裏寫圖片描述

  • entity ( table )
  • dao
  • database
  • converters
  • Dbhelper

2.1 table 實現

table (entity) 實現時,注意實現其構造函數,否則報錯;有些字段不支持,通過 TypeConverters 處理。

註解說明:

  • @Entity 指明表,默認表名爲實體類名,也可通過 tableName 指定表名
  • @PrimaryKey 主鍵,自增的話指明 autoGeneratetrue
  • @ColumnInfo 指定列,默認列表爲屬性名,也可通過name 指定列名

UserTable

import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.PrimaryKey
import java.util.*

/**
 * Created by yuan on 02/08/2017.
 * 用戶表
 * id 自增
 */
@Entity(tableName = "user")
data class UserTable constructor(@ColumnInfo(name = "u_id")
                                 @PrimaryKey(autoGenerate = true)
                                 var id: Int = 0,
                                 @ColumnInfo(name = "u_name")
                                 var name: String? = null,
                                 @ColumnInfo(name = "u_phone")
                                 var phone: String? = null,
                                 @ColumnInfo(name = "u_create_date")
                                 var createDate: Date? = null,
                                 @ColumnInfo(name = "u_update_date")
                                 var updateDate: Date? = null,
                                 @ColumnInfo(name = "u_status")
                                 var status: Int? = 0
) {
    constructor() : this(0)
}

BookTable

外鍵可通過 foreignKeys 進行指定,foreignkeys 爲數組使用 arrayOf 操作,如下所示:

import android.arch.persistence.room.ColumnInfo
import android.arch.persistence.room.Entity
import android.arch.persistence.room.ForeignKey
import android.arch.persistence.room.PrimaryKey

/**
 * Created by yuan on 02/08/2017.
 * book table
 * 注意:數據類型必須爲其支持的類型
 */
@Entity(tableName = "book", foreignKeys = arrayOf(ForeignKey(entity = UserTable::class,
        parentColumns = arrayOf("u_id"),
        childColumns = arrayOf("user_id")))
)
data class BookTable(
        @ColumnInfo(name = "b_id")
        @PrimaryKey(autoGenerate = true)
        var id: Int = 0,
        @ColumnInfo(name = "b_name")
        var name: String = "",
        @ColumnInfo(name = "b_author")
        var author: String = "",
        @ColumnInfo(name = "b_price")
        var price: Double = 0.toDouble(),
        @ColumnInfo(name = "b_copyright")
        var copyright: String = "",
        @ColumnInfo(name = "b_status")
        var status: Int = 0,
        @ColumnInfo(name = "user_id")
        var userId: Int = 0
) {
    // 必須有公共構造方法
    constructor() : this(0)
}

2.2 Dao 實現

Dao 是一個接口

註解說明:

  • @Dao 指明爲 Dao
  • @Insert 插入操作
  • @Update 更新操作
  • @Delete 刪除操作
  • @Query 查詢操作 ,需要指定 Sql 語句

更多操作,下篇會出個實例講解

UserDao

import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
import cn.labelnet.android.roomdb.base.data.tables.UserTable

/**
 * Created by yuan on 02/08/2017.
 * 用戶 dao
 */
@Dao
interface UserDao {

    /**
     * 插入一個用戶
     */
    @Insert
    fun insertUser(user: UserTable)

    /**
     * 插入多個用戶
     */


    /**
     * 查詢全部用戶
     */
    @Query("select * from user")
    fun selectUsers(): List<UserTable>
}

BookDao

import android.arch.persistence.room.Dao
import android.arch.persistence.room.Insert
import android.arch.persistence.room.Query
import cn.labelnet.android.roomdb.base.data.tables.BookTable

/**
 * Created by yuan on 02/08/2017.
 * book dao
 */
@Dao
interface BookDao {

    @Insert
    fun insertBook(book: BookTable)

    @Query("select * from book")
    fun selectBooks(): List<BookTable>
}

2.3 Database

抽象類,需要繼承 RoomDatabase

註解說明

  • @Database 指定操作,將 Dao 層全部集中在一起,可以指定 version 使用 entities 指定所用的 entity(table)
  • @TypeConverters 指定轉換器,看要加在什麼上面
import android.arch.persistence.room.Database
import android.arch.persistence.room.RoomDatabase
import android.arch.persistence.room.TypeConverters
import cn.labelnet.android.roomdb.base.data.Converters
import cn.labelnet.android.roomdb.base.data.dao.BookDao
import cn.labelnet.android.roomdb.base.data.dao.UserDao
import cn.labelnet.android.roomdb.base.data.tables.BookTable
import cn.labelnet.android.roomdb.base.data.tables.UserTable

/**
 * Created by yuan on 02/08/2017.
 * 操作用戶的 database
 */
@Database(entities = arrayOf(UserTable::class, BookTable::class), version = 1)
@TypeConverters(Converters::class)
abstract class AppDataBase : RoomDatabase() {

    /**
     *  user 操作 dao
     */
    abstract fun userDao(): UserDao

    /**
     * book 操作 dao
     */
    abstract fun bookDao(): BookDao

}

2.4 converters

有些類型不可以直接操作,就需要 converter 進行轉換,比如日期類型。

上面 UserTable 中的 createDateDate 類型,在生成數據表的存儲的時候用 String 進行存儲,這是使用 converter 。 當然 Long 類型存儲日期時間也是可以的,不過就不是下面寫法了,自己另行寫吧。

  @ColumnInfo(name = "u_create_date")
  var createDate: Date? = null,

註解說明:

  • @TypeConverter 指明轉換方法
  • @TypeConverters 作用於需要轉換的地方

如果作用於 Datebase 上,那麼整個數據庫中日期類型都按照這樣的格式轉換。

如果作用於 Entity(table) 上,那麼就只有該 Entity(table) 轉換。

import android.arch.persistence.room.TypeConverter
import java.text.SimpleDateFormat
import java.util.*

/**
 * Created by yuan on 02/08/2017.
 * 轉換器
 */
class Converters {

    val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)

    /**
     * 時間戳轉日期
     */
    @TypeConverter
    fun fromTimestamp(value: String?): Date {
        if (value == null) {
            return Date(System.currentTimeMillis())
        }
        synchronized(format) {
            return format.parse(value)
        }
    }

    /**
     * 日期轉時間
     */
    @TypeConverter
    fun dateToTimestamp(date: Date?): String {
        if (date == null) {
            val nowDate = Date(System.currentTimeMillis())
            synchronized(format) {
                return format.format(nowDate)
            }
        }
        synchronized(format) {
            return format.format(date)
        }
    }
}

轉換器 存儲結果:

這裏寫圖片描述


2.5 DbHelper

使用單例方式就行初始化db , 生成數據庫操作對象:

 val appDb = Room.databaseBuilder(context,
                AppDataBase::class.java,
                DB_NAME)
                .build()

關閉數據庫

appDb.close()

數據庫版本更新

 val appDb = Room.databaseBuilder(context,
                AppDataBase::class.java,
                DB_NAME)
                .addMigrations(//實現 Migration )
                .build()

完整代碼

import android.arch.persistence.db.SupportSQLiteDatabase
import android.arch.persistence.room.Room
import android.arch.persistence.room.migration.Migration
import android.content.Context
import android.util.Log
import cn.labelnet.android.roomdb.base.data.dao.BookDao
import cn.labelnet.android.roomdb.base.data.dao.UserDao
import cn.labelnet.android.roomdb.base.data.database.AppDataBase

/**
 * Created by yuan on 02/08/2017.
 * db 操作類
 */
class DbHelper constructor(context: Context) {

    var appDb: AppDataBase? = null
    val DB_NAME = "room_test_db.db"

    /**
     * 初始化 db
     */
    init {
        appDb = Room.databaseBuilder(context,
                AppDataBase::class.java,
                DB_NAME)
                .build()
                //.addMigrations(MigrateDb(1, 2))
    }

    /**
     * 獲取 user dao
     */
    fun getUserDao(): UserDao {
        return appDb!!.userDao()
    }

    /**
     * 獲取 book dao
     */
    fun getBookDao(): BookDao {
        return appDb!!.bookDao()
    }


    /**
     * 單例實現
     */
    companion object {

        var INSTANCE: DbHelper? = null

        fun init(context: Context): DbHelper {
            if (INSTANCE == null) {
                synchronized(DbHelper::class) {
                    if (INSTANCE == null) {
                        INSTANCE = DbHelper(context)
                    }
                }
            }
            return INSTANCE!!
        }

    }

    /**
     * 關閉數據庫
     */
    fun onDestory() {
        if (appDb != null) {
            appDb!!.close()
        }
    }

    /**
     * 版本合併
     */
    class MigrateDb(startVersion: Int, endVersion: Int) : Migration(startVersion, endVersion) {
        override fun migrate(database: SupportSQLiteDatabase?) {
            Log.v("DbHelper", "migrate")
        }
    }

}

3. 問題與坑

Question 1

Caused by: java.lang.RuntimeException: cannot find implementation for  AppDatabase_Impl does not exist
                                                       at android.arch.persistence.room.Room.getGeneratedImplementation(Room.java:90)
                                                       at android.arch.persistence.room.RoomDatabase$Builder.build(RoomDatabase.java:340)
                                                       at com.ttp.kotlin.kotlinsample.room.AppDatabase$Companion.getInMemoryDatabase(AppDatabase.kt:19)

解決: kotlin 下使用 kapt

annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha5"

換成

kapt "android.arch.persistence.room:compiler:1.0.0-alpha5"

Question 2

Error:Entities and Pojos must have a usable public constructor.

解決

entity 需要實現構造函數

@Entity(tableName = "user")
data class UserTable constructor(@ColumnInfo(name = "u_id")
                                 @PrimaryKey(autoGenerate = true)
                                 var id: Int = 0,
                                 @ColumnInfo(name = "u_name")
                                 var name: String? = null,
                                 @ColumnInfo(name = "u_phone")
                                 var phone: String? = null,
                                 @ColumnInfo(name = "u_create_date")
                                 var createDate: Date? = null,
                                 @ColumnInfo(name = "u_update_date")
                                 var updateDate: Date? = null,
                                 @ColumnInfo(name = "u_status")
                                 var status: Int? = 0
) {
    // 實現構造函數
    constructor() : this(0)
}

Question 3

Warning:warning: Supported source version 'RELEASE_7' from annotation processor 'android.arch.persistence.room.RoomProcessor' less than -source '1.8'

Warning:warning: Supported source version 'RELEASE_7' from annotation processor 'android.arch.lifecycle.LifecycleProcessor' less than -source '1.8'

解決:使用 kapt 時,不需要重複在build.gradle 上進行引入 kotlin-kapt , 當然沒有引入最好了。

apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
// 不需要它,已經默認有了,它導致上面
//apply plugin: 'kotlin-kapt'

Question 4

Error:Cannot figure out how to save this field into database. You can consider adding a type converter for it.
Error:Cannot figure out how to read this field from a cursor.

解決:有些字段或類型不認識,需要在 converter 進行轉換;

  • Date
  • ArrayList
class Converters {

    val format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.CHINA)

    /**
     * 時間戳轉日期
     */
    @TypeConverter
    fun fromTimestamp(value: String?): Date {
        if (value == null) {
            return Date(System.currentTimeMillis())
        }
        synchronized(format) {
            return format.parse(value)
        }
    }

    /**
     * 日期轉時間
     */
    @TypeConverter
    fun dateToTimestamp(date: Date?): String {
        if (date == null) {
            val nowDate = Date(System.currentTimeMillis())
            synchronized(format) {
                return format.format(nowDate)
            }
        }
        synchronized(format) {
            return format.format(date)
        }
    }
}

Question 5

 Process: cn.labelnet.android.roomdb, PID: 22630
                                                                            java.lang.IllegalStateException: Cannot access database on the main thread since it may potentially lock the UI for a long period of time.
                                                                                at android.arch.persistence.room.RoomDatabase.assertNotMainThread(RoomDatabase.java:143)
                                                                                at android.arch.persistence.room.RoomDatabase.beginTransaction(RoomDatabase.java:190)
                                                                                at cn.labelnet.android.roomdb.base.data.dao.UserDao_Impl.insertUser(UserDao_Impl.java:63)
                                                                                at cn.labelnet.android.roomdb.main.MainActivity.onItemClick(MainActivity.kt:53)
                                                                                at cn.labelnet.android.roomdb.main.adapter.MainAdapter$onBindViewHolder$1.onClick(MainAdapter.kt:53)
                                                                                at android.view.View.performClick(View.java:5637)
                                                                                at android.view.View$PerformClick.run(View.java:22429)
                                                                                at android.os.Handler.handleCallback(Handler.java:751)
                                                                                at android.os.Handler.dispatchMessage(Handler.java:95)
                                                                                at android.os.Looper.loop(Looper.java:154)
                                                                                at android.app.ActivityThread.main(ActivityThread.java:6119)
                                                                                at java.lang.reflect.Method.invoke(Native Method)
                                                                                at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:886)
                                                                                at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:776)

解決 : 數據庫操作放在 io 線程,不要在 ui 線程做。子線程操作自行解決,我這裏就不贅述了。

Question 6

java.lang.IllegalStateException: Room cannot verify the data integrity. Looks like you've changed schema but forgot to update the version number. You can simply fix this by increasing the version number.
                                                                                at android.arch.persistence.room.RoomOpenHelper.checkIdentity(RoomOpenHelper.java:112)
                                                                                at android.arch.persistence.room.RoomOpenHelper.onOpen(RoomOpenHelper.java:93)
                                                                                at android.arch.persistence.db.framework.FrameworkSQLiteOpenHelper$1.onOpen(FrameworkSQLiteOpenHelper.java:64)
                                                                                at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:266)
                                                                                at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:163)

解決:數據庫改變,需要使用版本更新操作。

Room.databaseBuilder(context,
                AppDataBase::class.java,
                DB_NAME)
                .addMigrations(MigrateDb(1, 2))
                .build()

/**
     * 版本合併
     */
    class MigrateDb(startVersion: Int, endVersion: Int) : Migration(startVersion, endVersion) {
        override fun migrate(database: SupportSQLiteDatabase?) {
            // 版本更新操作
        }
    }

4. 最後

以開源,倉庫地址 : RoomDb

下篇說明說用

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章