參考文章
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
主鍵,自增的話指明autoGenerate
爲true
@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
中的 createDate
爲 Date
類型,在生成數據表的存儲的時候用 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
下篇說明說用