Room 数据库快速入门

1 篇文章 0 订阅
1 篇文章 0 订阅

被无穷无尽的 SQLite 样板代码折磨后,发现 Google 的 Room 封装似乎是个不错的选择,不过大量运用注释,使得不熟悉 SQLite 的开发者如果一开始就直接上手 Room,会不知其所以然,这里记录下我的学习笔记。

1. 回顾直接操作 SQLite

我本人在操作数据库数据库时有个习惯,就是不使用封装的api直接写SQL。因为大多数操作数据库的api实在是过于繁琐。如果直接写SQL来写入数据库,只需要使用到 SQLiteOpenHelper, SQLiteDatabase,Cursor 三个类和 SQLiteDatabase下的两个 execSQL()rawQuery()主要方法

  • SQLiteOpenHelper

    SQLite数据库的辅助类,用于管理数据库的创建和版本控制,一般我们也将数据库操作封装在该类里

    • onCreate()

      重写用于创建数据库的表和初始化内容,该方法只会在输入的数据库名匹配不到数据库时被调用

    • onUpgrade()

      重写用于数据库的版本升级,该方法的内容在长期运营的项目中会变得相当繁琐

  • SQLiteDatabase

    通过SQLiteOpenHelper下的writableDatabasereadableDatabase方法可以获得该实例
    该类包含一系列封装好的操作 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语句,但是可读性不高,如有兴趣可以访问官方网站

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值