背景
按照Anko官方的建议开始逐步替换anko,
在替换anko-sql时准备使用提到的Room.
介绍
Room是谷歌提供的ORM,多数使用注解的方式.
兼具灵活性(@Query
注解可以自定义查询语句)与
便捷性(免去常规的插入语句,免去表创建语句等).
但好像和 active-record 风格不一样,还需要DAO,感觉奇怪.
目前感觉的亮点
不需要自己写创建数据库的sql语句
如果是 select *
可以将结果自动转成entity
提供的 @Insert
, @Update
, @Delete
注解可以自动生成sql模板代码
能够使用 LiveData
来监控数据库变化
依赖
1 2 3 4 5 6 apply plugin: 'kotlin-kapt' dependices { implementation "androidx.room:room-runtime:2.2.5" kapt "androidx.room:room-compiler:2.2.5" }
有的地方写
1 2 implementation "androidx.room:room-runtime:2.2.5" annotationProcessor "androidx.room:room-compiler:2.2.5"
但不知道为什么会报 AppDatabase_Impl
没有生成的问题,
换成 kapt
之后就没问题了
必要组件
Entity
1 2 3 4 5 6 7 @Entity(tableName = "users" ) data class User ( var name: String, var salary: Int ) { @PrimaryKey(autoGenerate = true) var id: Long ? = null }
默认情况下表名等于类名,使用 tableName
来自定义
默认情况下列名等于变量名,使用 @ColumnInfo(name = "first_name")
来定义
默认情况下全部列名作为表的列,使用 @Ignore
表示该变量不需要作为列名.
这里定义好id的自动增加后,设置默认null只是为了语法不报错,id还是自动从1增加的.
数据库的notnull是根据entity的类型决定的, Int
就是notnull, Int?
就是null
高级一些的用法
多个主键 @Entity(primaryKeys = {"last_name", "first_name"})
单个索引 @Index("name")
多个索引 @Index(value = {"last_name", "first_name"})
默认值 @ColumnInfo(defaultValue = "0")
, Room 2.2.0才加入的功能
DAO
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Dao interface UserDao { @Query("SELECT * FROM Users" ) fun getAllUsers () : List<User> @Query("SELECT * FROM Users WHERE id = :id" ) fun getUserById (id: String ) : User? @Query("SELECT * FROM Users LIMIT 1" ) fun getOneUser () : User? @Insert(onConflict = OnConflictStrategy.REPLACE) fun addUser (user: User ) @Update fun upDateUser (user: User ) @Delete fun deleteUser (user: User ) @Query("DELETE FROM Users" ) fun deleteAllUsers () }
除了查询,其他的DAO注解还是很好用的,再也不用手写插入代码了
1 2 3 4 5 6 insert( Invoice.TABLE_NAME, Invoice.COLUMN_SENDER_ID to owner.senderId, Invoice.COLUMN_SENDER_NM to senderNm.text.toString(), Invoice.COLUMN_CREATED_USER_ID to owner.thisApp!!.loginUserId )
Database
初始化DB的实例开销比较大,
因此常常使用单例模式.
kotlin的单例模式有许多中写法,
这里只是其中一种.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Database(entities = [User::class], version = 1) abstract class AppDatabase : RoomDatabase () { abstract fun userDao () : UserDao companion object { private var instance: AppDatabase? = null fun getInstance (context: Context ) : AppDatabase { if (instance == null ) { instance = Room.databaseBuilder( context.applicationContext, AppDatabase::class .java, "sample.db" ).allowMainThreadQueries().build() } return instance as AppDatabase } } }
使用举例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 class MainActivity : AppCompatActivity () { override fun onCreate (savedInstanceState: Bundle ?) { super .onCreate(savedInstanceState) setContentView(R.layout.activity_main) val userDao = AppDatabase.getInstance(this ).userDao() for (i in (0 until 10 )) { val user = User("test$i " , 1 ) userDao.addUser(user) } userDao.getAllUsers().forEach { Log.d("room" , "==query==${it.id} ,${it.name} ,${it.salary} " ) } userDao.deleteAllUsers() Log.d("room" , "1" ) } }
还是需要提供context给 getInstance
,
原生的sql本身就是需要context的
1 2 val crSQLiteOpenHelper = CRSQLiteOpenHelper(applicationContext, "create_db" , null , 1 )this .db = crSQLiteOpenHelper.writableDatabase
和livedata搭配的用法
Room可以在数据库发生改变时更新模型的值,
然后模型的值会驱动UI变化,
这样保证用户看到的一定被存储下来了,
不会因为断电等无法保存.
目前的一些实现方法是通过ViewModel实现的,而且也很简单.
首先在DAO中加入返回值为Livedata的方法
1 2 3 4 5 6 7 8 @Dao interface UserDao { @Query("SELECT * FROM Users ORDER BY id DESC LIMIT 1" ) fun getOneUser () : LiveData<User> }
然后在AndroidViewModel中,
使用context初始化一个userDao,
并使用userDao的方法来获取一个类型为Livedata的 user
对象
1 2 3 4 5 6 7 8 9 10 11 12 13 class UserViewModel (application: Application) : AndroidViewModel(application) { var userDao = AppDatabase.getInstance(application).userDao() var user = userDao.getOneUser() }
最后在Activity中定义
1 2 3 viewModel.user.observe(this , Observer { text1.text = it.toString() })
流程是
数据库发生变化
UsedDao的getOneUser()方法被调用
UserViewModel的user对象被改变
好在不需要设置userDao.getOneUser().observe()
如果安卓的未来变成像tensorflow一样的图编程,可能会非常不好debug
View被改变
采用repository架构
Repository主要用途是为应用程序提供整洁的,用于获取数据的API,
隐藏掉本地操作和远程操作的细节
方便了测试(代码被解耦,可以手动写虚拟的repository来测试viewmodel)
方便应用程序实现在断网时数据存在本地,联网后又重新把数据提交到云端
方便应用程序把一些常用的网络查询缓存在内存中,减少流量消耗,提升响应速度等
实现起来也比较简单.
先准备一个Repository
1 2 3 4 5 6 7 8 9 10 class UserRepository (application : Application) { var userDao = AppDatabase.getInstance(application).userDao() fun getUser () : LiveData<User> { return userDao.getOneUser() } }
然后将viewmodel中直接使用userDao的代码换成经过repository
1 2 3 4 5 6 7 class UserViewModel (application: Application) : AndroidViewModel(application) { var userRepository = UserRepository(application) var user = userRepository.getUser() }
总结
activity保有AndroidViewModel对象.
activity和viewmodel各自保有绑定的数据用以显示变量.
viewmodel保有Repository对象,且数据由repository的方法或数据直接驱动.
repository保有DAO和网络请求的对象.
数据库发生变化时,DAO的方法自动执行.
由于返回的是Livedata<User?>,该变量发生变化,
经过层层传递,最终在activity中被打开并导致了UI发生变化.
希望改进
目前使用的是AndroidViewmodel,而官方的指南中通过依赖注入而实现了使用ViewModel,
虽然不知道是否依赖注入对ViewModel无法销毁导致的内存泄露有什么影响,
但还是希望使用ViewModel
升级数据库
首先需要修改entity
1 2 3 4 5 6 7 8 9 @Entity(tableName = "users" ) data class User ( var name: String, var salary: Int , @ColumnInfo(defaultValue = "0" ) var age: Int ) { @PrimaryKey(autoGenerate = true) var id: Long = 0 }
和数据库本身的版本
1 2 3 4 5 @Database(entities = [User::class], version = 2) abstract class AppDatabase : RoomDatabase () { }
然后才能使用语句来定义迁移的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 companion object { private var instance: AppDatabase? = null val MIGRATION_1_2 = object : Migration(1 , 2 ) { override fun migrate (database: SupportSQLiteDatabase ) { database.execSQL("ALTER TABLE users ADD age INTEGER NOT NULL DEFAULT 0" ) } } fun getInstance (context: Context ) : AppDatabase { if (instance == null ) { instance = Room.databaseBuilder( context.applicationContext, AppDatabase::class .java, "sample.db" ).addMigrations(MIGRATION_1_2) .allowMainThreadQueries().build() } return instance as AppDatabase } }
TODO 数据库的降级是否需要写一些语句?还是不用管
参考
基于kotlin的基础用法
官方指南中的建议
比较低级但更详细的Repository用法
官方的数据库升级指南