还在使用原生的sqllite?有这么清爽且稳如狗的room为啥不用呢?
Room是Google官方推荐使用的数据库,相比较某些优秀数据库框架来说,不用过于担心某天库会停止维护,且访问数据库非常流畅,并且提供了与常规的ORM框架一样,通过添加编译期注解来进行表和字段的配置,譬如@Database、@Dao、@Entity、@Query、@Insert、@Update、@Detele等的注解,可以使用简单代码实现相比以前SQLite更复杂的代码的效果,这点儿有点儿类似于java世界里的mybatis。总而言之, Room功能强大,速度和稳定性不弱,还简单易用,算得上是一个优秀的数据库。
这里总结下使用room数据库的过程和遇到的问题,以及如何稳定的进行数据库的迁移和升级。
代码以kotlin为例,java也类似差不多的。
首先准备,引入依赖,在app文件夹下的build.gradle中增加:
//ROOM数据库 implementation "android.arch.persistence.room:runtime:1.1.1" kapt "android.arch.persistence.room:compiler:1.1.1"
需要注意的是,使用的是kotlin,annotationProcessor "android.arch.persistence.room:compiler:1.1.1"的写法需换成kapt "android.arch.persistence.room:compiler:1.1.1"
还需引入一个插件:apply plugin: 'kotlin-kapt'
代码结构目录是不是很清晰,在room下dao的单独建dao包,操作接口在这里实现,所有的表定义单独在entity包中。
接下来最好在 app文件夹下的build.gradle中再增加项配置,让编译后自动输出生成的schemas,里面有创建和修改表结构的sql语句。(这在数据库升级时很有用,可以拷贝过来用)
在app文件夹下的build.gradle的defaultConfig 增加:
javaCompileOptions { annotationProcessorOptions { arguments = [ "room.schemaLocation":"$projectDir/schemas".toString(), "room.incremental":"true", "room.expandProjection":"true"] } }
这样在app文件夹下会多输出个schemas文件夹,里面有对应版本的sql,json文件。在做数据库升级时这很有用,可以复用里面的sql语句。
简单的使用:
第一步,在entiy包中增加实体类的定义,每个实体类对应一个表,,类前面加@Entity注解,默认类名就是最终生成的表名,如果不想让一致,可以指定表名(@Entity (tableName = "users"))。
使用主键 : 一个Entry中至少需要一个主键,使用@PrimaryKey来注释. 自增类型的主键,则可以设置 @PrimaryKey 的 autoGenerate 属性。
忽略字段: 使用@Ignore注解,如,@Ignore val picture: Bitmap?
更改字段名使用@ColumnInfo(name = "xxx")注解,如果不指定,默认就是属性名。
package com.xxx.xx.room.entity import android.arch.persistence.room.Entity import android.arch.persistence.room.PrimaryKey @Entity class User{ // 自增主键 @PrimaryKey(autoGenerate = true) var id = 0 var userName: String? = null var passWord: String? = null }
第二步,在dao包里写对应的dao,对应的操作,
注意这些操作接口最好都带个返回值,比如insert返回long,delete返回Int。因为最终的使用总要对操作结果来个判断吧。
如下,对user表的增删改查全部有啦,够简单和清爽吧。
package com.xxx.xx.room.dao import android.arch.persistence.room.* import com.xxx.xx.room.entity.User @Dao interface UserDao { //查询user表中所有数据 @get:Query("SELECT * FROM user") val all: List<User?>? @Query("SELECT * FROM user WHERE 'id' IN (:userIds)") fun loadAllByIds(userIds: IntArray?): List<User?>? @Query("SELECT * FROM User LIMIT 1") fun findUser(): User? @Insert fun insert(user: User?):Long @Delete fun delete(vararg users: User?):Int // 改 @Update fun update(vararg users: User): Int @Query("DELETE FROM User") fun deleteAllUser() @Query("SELECT COUNT(*) FROM User") fun countAll():Int }
Insert还可以开启个对冲突的策略,默认的添加重复的数据(主键一致)会抛异常的。使用 @Insert(onConflict = OnConflictStrategy.REPLACE)重复时则会替换。
第三步,添加room数据库并封装个单例操作类,
//AppDb.kt package com.xxx.xx.room import android.arch.persistence.room.Database import android.arch.persistence.room.RoomDatabase import com.xxx.xx.room.dao.AgeDao import com.xxx.xx.room.dao.UserDao import com.xxx.xx.room.entity.Age import com.xxx.xx.room.entity.User @Database(entities = [User::class,Age::class], version = 2,exportSchema = true) abstract class AppDb : RoomDatabase() { abstract fun userDao(): UserDao abstract fun ageDao(): AgeDao }
//Dbhelper.kt package com.xxx.xx.room import android.arch.persistence.db.SupportSQLiteDatabase import android.arch.persistence.room.Room import android.arch.persistence.room.RoomDatabase import android.arch.persistence.room.migration.Migration import android.content.Context import android.os.Environment import java.io.File class DbHelper { var db: AppDb var DB_PATH = Environment.getExternalStorageDirectory().absolutePath + File.separator + "xxx" + File.separator+"db"+ File.separator// var DB_NAME = DB_PATH +"mydb" private constructor(context:Context){ //判断目录是否存在,不存在则创建该目录 val dir = File(DB_PATH) if (!dir.exists()) { dir.mkdirs() } //允许在主线程中查询 db = Room.databaseBuilder(context,AppDb::class.java, DB_NAME) .allowMainThreadQueries().addMigrations(MIGRATION_1_2).setJournalMode(RoomDatabase.JournalMode.TRUNCATE) .build() } //数据库迁移 val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `Age` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userAge` TEXT, `age` INTEGER)") } } companion object { var context: Context?=null private var mInstance: DbHelper? = null fun getInstance(): DbHelper { if (DbHelper.mInstance == null) { synchronized(DbHelper::class.java) { if (DbHelper.mInstance == null) { DbHelper.mInstance = DbHelper(context!!) } } } return DbHelper.mInstance!! } } }
最后就可以愉快的使用啦:
... var userDao: UserDao userDao = DbHelper.getInstance().db.userDao() var user = User() user.userName = "yang" user.passWord="123456" userDao.insert(user) ...
当修改表字段或者增加表结构时,数据库升级注意事项:
无论是增加新表还是只是修改表字段或增加表字段,都需要增加下数据库的版本号并增加Migration处理,
@Database(entities = [User::class,Age::class], version = 2,exportSchema = true)
db = Room.databaseBuilder(context,AppDb::class.java, DB_NAME) .allowMainThreadQueries().addMigrations(MIGRATION_1_2).setJournalMode(RoomDatabase.JournalMode.TRUNCATE).build() //数据库迁移 val MIGRATION_1_2 = object : Migration(1, 2) { override fun migrate(database: SupportSQLiteDatabase) { database.execSQL("CREATE TABLE IF NOT EXISTS `Age` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `userAge` TEXT, `age` INTEGER)") } }
如果不更改version还增加了表结构或修改了表字段,则会crash,报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.
如果只增加version没有对应的Migration,同样会crash,
虽然有不提供自定义Migration,又不想引发crash的fallbackToDestructiveMigration方法,但非常不建议这么搞,要去对这些表结构的变化做处理,考虑到数据的安全性。
如果仅是测试,清空数据或删除掉db文件就可以从新来过了。
如果有正式的数据,在进行表结构的更改前,需做好安全测试保证数据不丢失。
多表关联,因为SQLite是关系型数据库, 你可以指定对象之间的关系. 尽管大多数对象关系的映射允许实体对象引用彼此, 而Room却显式地禁止了这个特性。
尽管不能使用直接的对象关系, Room仍然允许在实体之间定义外键约束。比如
@ForeignKey(entity = OrderTransdtlRecord::class, parentColumns = ["billno"],childColumns = ["billno"], onDelete = CASCADE, onUpdate = CASCADE)
有些时候, 在数据库逻辑中, 你想将一个实体或者POJO表示为一个紧密联系的整体, 即使这个对象包含几个域. 在这些情况下, 你能够使用@Embedded注解来表示一个对象, 而你想将这个对象分解为表内的子域. 然后你可以查询这些嵌套域, 就像你查询其它的独立列一样。