被无穷无尽的 SQLite 样板代码折磨后,发现 Google 的 Room 封装似乎是个不错的选择,不过 Room 大量运用注解框架,使得不熟悉 SQLite 的开发者如果一开始就直接上手 Room,会不知其所以然,这里记录下我的学习笔记。
1. 回顾直接操作 SQLite
我本人在操作数据库时有个习惯,就是不使用封装的api直接写SQL。因为大多数操作数据库的api实在是过于繁琐。如果直接写SQL来写入数据库,只需要使用到 SQLiteOpenHelper, SQLiteDatabase,Cursor 三个类和 SQLiteDatabase下的两个 execSQL()
,rawQuery()
主要方法
-
SQLiteOpenHelper
SQLite数据库的辅助类,用于管理数据库的创建和版本控制,一般我们也将数据库操作封装在该类里
-
onCreate()
重写用于创建数据库的表和初始化内容,该方法只会在输入的数据库名匹配不到数据库时被调用
-
onUpgrade()
重写用于数据库的版本升级,该方法的内容在长期运营的项目中会变得相当繁琐
-
-
SQLiteDatabase
通过SQLiteOpenHelper下的
writableDatabase
,readableDatabase
方法可以获得该实例
该类包含一系列封装好的操作 SQLite 的 API,也允许直接执行 SQL 语句-
execSQL()
执行
CREATE
,INSERT
,DELETE
,UPDATE
时使用下面是向 book_type 表中插入一行的操作,其中
?
是传参的占位符// id 为 INTEGER 类型,type 为 VARCHAR 类型。分别对应Java的 Int 和 String db.execSQL("INSERT INTO 'book_type'('book_type_id','book_type_str') value(?,?)" , arrayOf(book_type_id, book_type_str))// Java 使用 Array.listOf()
-
rawQuery()
执行
SELECT
时使用。查询语句如果使用 api 进行操作,将变得极其繁琐,并不能起到隐去实现的作用。不如直接rawQuery()
写 SQL。下面是从 book 表中查询以 bookName 开头的 bookname 的所有行的操作,查询后将返回一个 Cursor 类型的表指针val cursor = db.rawQuery("SELECT * FROM book WHERE book.book_name LIKE '${bookName}%'")
-
-
Cursor
一个指向表的特定一行的数据指针,可以使用
moveToFirst()
指向表的第一行后,再重复调用moveToNext()
,遍历完全部的表项if(cursor.moveToFirst()){ do{ // 使用 getColumnIndex() 和对应的 getXXX 方法,即可取出对应列的内容 val book_name = cursor.getString(cursor.getColumnIndex("book_name")) }while(cursor.moveToNext()) }
2. 概览
与原来 SQLiteDatabase 封装的 api 不同,Room通过引入一套注解框架,完美地进行了深度封装,开发者不用再写一堆模板代码。
使用 Room 和 SQLite 交互时,需要与三个类进行交互
-
Entity
定义 SQLite 表的实体
-
DAO (Data Access Object)
定义用于与数据库交互的方法,即
INSERT
,DELETE
,UPDATE
,SELECT
-
RoomDatabase
类似于 SQLiteOpenHelper,SQLite 的辅助类
3. Entity
Entity 用于定义 Java层 与 SQLite层 之间的映射关系,Java层 中包含大小写的字段,在 SQLite层 中将 映射为对应的不区分大小写的字段。如果需要定义别名,需要使用 @ColumnInfo
注解
定义一个 Entity 需要使用到一系列注解
-
@Entity
拥有 @Entity 的类将被映射为一个表,该注解有两个常用的属性
- tableName 用于自定义表的别名
- primaryKeys 用于定义联合主键
- ignoredColumns 用于定义不会被映射的字段
- indices 用于定义索引
-
@PrimaryKey
定义单个属性为主键
- autoGenerate 用于设置主键自增
-
@ColumnInfo
- name 用于自定义列的别名
-
@Ignore
定义不会被映射的字段,比如用户持有的缓存中的BitMap,这种就不需要用数据库存储
假设我们需要定义以下 SQLite 语句的表
# eg.1
CREATE TABLE `writers`(
`writer_name` VARCHAR,
`writer_id` INTEGER PRIMARY KEY AUTOINCREMENT
);
# eg.2
CREATE TABLE `book_types`(
`book_type_str` VARCHAR,
`book_type_id` INTEGER PRIMARY KEY AUTOINCREMENT
);
# eg.3
CREATE TABLE `book`(
`book_type_id` INTEGER,
`writer_id` INTEGER,
`book_name` VARCHAR,
`book_isbn` VARCHAR,
`book_id` INTEGER PRIMARY KEY AUTOINCREMENT
);
CREATE INDEX `book_type_id` ON `book`(`book_type_id`);
CREATE INDEX `writer_id` ON `book`(`writer_id`);
其对应的 Entity 可以为
// eg.1 这里演示ignoredColumns的作用
@Entity(tableName="writers",
ignoredColumns=arrayOf("picture"))
data class Writer(
@ColumnInfo(name = "writer_name")
val writerName:String?,
var picture:Bitmap?=null,
@ColumnInfo(name = "writer_id")
@PrimaryKey(autoGenerate=true)
var writerId:Int = 0
)
// eg.2
@Entity(tableName="book_types")
data class BookType(
@ColumnInfo(name = "book_type_str")
val bookTypeStr:String?
@Ignore
val typePic:Bitmap?,
@ColumnInfo(name = "book_type_id")
@PrimaryKey(autoGenerate=true)
val bookTypeId:Int = 0
)
// eg.3
@Entity(tableName="books",
indices=arrayOf(Index(value=["book_type_id", "writer_id"],
unique=true)))
data class Book(
@ColumnInfo(name = "book_type_id")
val bookTypeId:Int,
@ColumnInfo(name = "writer_id")
val writerId:Int,
@ColumnInfo(name = "book_name")
val bookName:String,
@ColumnInfo(name = "book_isbn")
val bookIsbn:String,
@PrimaryKey(autoGenerate=true)
@ColumnInfo(name = "book_id")
val bookId:Int = 0
)
在使用 SQLite 时,我们常常将某个表的一行按照列名定义为一个单独的JavaBean,这种JavaBean只需要添加几个注解就能映射为CREATE
语句,这对于开发来说是极大的便利
3.1 提供全表检索支持
如果直接操作SQLite,要提供FTS(Full Text Search),需要手动建立一系列虚表。
Room 2.1.0 以上的版本,可以通过添加 @Fts3 或者 @Fts4 注释并设置INTEGER类型名称为rowid的主键
@Fts4(languageId = "lid")
@Entity(tableName = "users")
data class User(
// ...
@ColumnInfo(name = "lid") val languageId: Int
)
4. DAO
4.1 DML映射
Room 使用注解处理器映射了数据库的 DML,即 INSERT
, UPDATE
, DELET
@DAO
interface writerDao{
// 返回的Long为插入到的行的序数
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertWriters(vararg writers: Writer):List<Long>
// 返回的Int为更新的行数
@Update
fun updateWriters(vararg writers: Writer):Int
// 返回的Int为删除的行数
@Delete
fun deleteWriters(vararg writers: Writer):Int
}
4.2 DQL映射
Room 对 Query 语句的返回值进行了进一步的封装
在使用SQLite时,如果我们需要返回表的某些列作为一个对象时,需要定义一个 JavaBean 获取到 Cursor 后手动赋值,而使用了 Room 后,稍微给JavaBean添加正确的注解后即可直接作为返回值,样板代码交给Room的注解处理器实现。
如果需要使用传统的Cursor api,Dao返回的类更改为 Cursor 即可
data class BaseBook(
@ColumnInfo(name="book_name") val bookName:String?,
@ColumnInfo(name="book_isbn") val bookIsbn:String?
)
@Dao
interface BookDao{
@Query("SELECT * FROM book WHERE book.writer_id = (SELECT writer.writer_id FROM writer WHERE writer.writer_name IN (:writerNames));")
fun loadBaseBooksWithWriterName(writerNames:List<String>):List<BaseBook>
}
5. RoomDatabase
在SQLite中,我们使用如下方法创建 DatabaseOpenHelper 与获取到数据库
// 假设我们自定义的 DatabaseOpenHelper 为 AppDatabaseHelper
// 这里 创建一个以 BookBase.db 为名的数据库,版本号为 1
val dbHelper = AppDatabaseHelper(this, "BookBase.db", 1)
val writableDb = dbHelper.writableDatabase
接着再调用一系列的方法操作writableDb
对象,数据库的表的创建还有数据库操作的方法之间联系得很零散
而在定义 RoomDatabase 的时候,我们需要直接定义该数据库包含的表,和允许的操作方法,一目了然
因为创建一个RoomDatabase实例的开销相当高,所以和使用SQLite时类似,实例化
AppDatabase
对象时应遵循单例设计模式,保持全局只持有一个 RoomDatabase
@Database(entities = arrayOf(Writer::class, BookType::class, Book::class), version = 1)
abstract class AppDatabase : RoomDatabase(){
abstract fun writerDao():WriterDao
abstract fun bookDao():BookDao
}
// 使用包含 Database 注解的类创建 RoomDB
val db = Room.databaseBuilder(
applicationContext,
AppDatabase::class.java, "BookBase.db"
)
// 调用示例
db.writerDao().insertWriters(Writer("myName"))
6. 对象映射
嵌套对象
在查询 addresses 和 users 时,假设users表中同时拥有user和address的列。如果希望查询方法返回的 user 持有一个 address, 在传统的SQLite查询中,需要获得cursor后,手动创建对象,手动进行赋值,如果出现多个类似需求,可以写一套模板代码生成器完成这个工作。
而使用Room的注解@Embedded
可以便捷地返回嵌套对象
data class Address(
val street:String?,
val state:String?,
val city:String?,
@ColumnInfo(name="post_code") val postCode:Int
)
@Entity
data class User(
@PrimaryKey val id:Int,
val firstName:String?,
@Embedded val address:Address?
)
使用这个Entiry时,生成的user表中将会有id
, firstname
, address
, street
, state
, city
, postCode
七个列。
官方网站上还有更多的关系定义,但是其他的映射比较复杂,虽然可以避免编写DAO中的SQL语句,但是可读性不高,如有兴趣可以访问官方网站。