Android 强烈建议使用 Room 来替代 SQLite 。
Room 提供了一个覆盖 SQLite 的抽象层,使用 Room 可以流畅的访问 SQLite 的全部功能。
罗哩罗嗦的介绍就不写了,直接开始用法吧
dependencies {
def room_version = "2.2.0-alpha01"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version" // For Kotlin use kapt instead of annotationProcessor
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - RxJava support for Room
implementation "androidx.room:room-rxjava2:$room_version"
// optional - Guava support for Room, including Optional and ListenableFuture
implementation "androidx.room:room-guava:$room_version"
// Test helpers
testImplementation "androidx.room:room-testing:$room_version"
}
Declaring dependencies
Room 主要包含三个组件:
Database: 包含数据库持有者,作为与应用持久化相关数据的底层连接的主要接入点。这个类需要用 @Database
注解,并满足下面条件:
RoomDatabase
的抽象类@Dao
注解的类在运行时,可以通过 Room.databaseBuilder()
或者 Room.inMempryDatabaseBuilder()
方法来获取到 Database 实例。
Room 各个组件和 APP 的关系如下图:
它代表了数据库中的一个表,这个类的属性就是表中的字段:
@Entity(tableName = "StudentTable")
public class Student implements Serializable {
@PrimaryKey(autoGenerate = true)
private int studentId;
@ColumnInfo(name = "student_name")
private String studentName;
@ColumnInfo(name = "student_age")
private int studentAge;
@ColumnInfo(defaultValue = "中心小学")
private String school;
@Ignore
private Bitmap bitmap;
//省略构造方法和 set/get 方法
}
其中:
Entity
必须添加这个注解,Room 才会将这个类看作是代表一张表
tableName
来定义,如果不定义,会默认为类名。public
的,或者提供了 set/get 方法,否则 Room 会无法访问到 Entity 中的属性。@PrimaryKey
设置主键,这个是必须的,这一点和 SQLite 不同
autoGenerate = true
来设置主键自增ColumnInfo
表示数据库中的字段名,默认是属性名,也可以使用 name
来自定义ColumnInfo
中使用 defaultValue
来为数据库字段设置默认值@Ignore
注解忽略之即可。索引、外键、关系等进阶内容在后面
Dao 是应用操作数据库的接口,应用程序无须关注数据库的具体操作,只需要使用 Dao 提供的接口来对数据做增删查改即可。
DAO 可以是接口,也可以是抽象类。如果它是一个抽象类,它可以有一个以 RoomDatabase
为唯一参数的构造函数(没有也行)。
Dao
注解XXXImpl does not exist
的话,请检查依赖是否添加正确。下面是常用的 CURD 操作
@Dao
public abstract class StudentDao {
//添加单个 Entity
@Insert()
public abstract long insert(Student student);
//添加多个 Entity,使用可变长参数形式或者参数为 List
@Insert()
public abstract long[] insertAll(Student... students);
//或者
@Insert()
public abstract List<Long> insertAll(List<Student> students);
}
方法的参数可以是一个 Entity 对象或者 Entity 对象的可变长参数或者 List;对于插入方法的返回值:
long[]
或者 List<Long>
当我们在 Entity
中有部分字段是有默认值的,那么我们在插入数据的时候,还可以这样写:
@Entity
public class Playlist {
@PrimaryKey(autoGenerate = true)
long playlistId;
String name;
@Nullable
String description
@ColumnInfo(defaultValue = "normal")
String category;
@ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")
String createdTime;
@ColumnInfo(defaultValue = "CURRENT_TIMESTAMP")
String lastModifiedTime;
}
public class NameAndDescription {
String name;
String description
}
@Dao
public interface PlaylistDao {
@Insert(entity = Playlist.class)
public void insertNewPlaylist(NameAndDescription nameDescription);
}
在上面的例子中,insertNewPlaylist
方法使用 entity
指定了 Entity 类,那么参数就可以任意 POJO 对象,只不过这个对象的字段必须是指定的 Entity 类中未设置默认值的字段。
@Dao
public abstract class StudentDao {
public StudentDao(RoomDatabase database) {
}
@Delete
public abstract int delete(Student student);
@Delete
public abstract int deleteAll(Student... students);
@Delete
public abstract int deleteAll(List<Student> students);
}
Delete 方法使用主键来查找需要删除的主体。方法的参数可以是 Entity 对象,或者是 Entity 对象的可变长参数或是 List;返回值是一个 int 值,表示删除成功的行数。
有一点搞不懂,官方文档中,Delete 也可以像 Insert 那样,通过制定一个 Entity 类,然后参数使用任意 POJO 对象,但是 Delete 方法是使用主键来查找需要删除的主题的,所以这个功能似乎没啥用,对于参数对象,无须指定其他属性,只需要设置主键属性即可,所以这个功能似乎有些多余。
@Dao
public abstract class StudentDao {
public StudentDao(RoomDatabase database) {
}
@Query("SELECT * FROM Student")
public abstract Student[] queryAllStudent();
@Query("SELECT * FROM Student WHERE studentId = :id")
public abstract List<Student> queryStudentById(int id);
@Query("SELECT student_name FROM Student")
public abstract List<String> queryAllStudentName();
@Query("SELECT student_name FROM Student where studentId in (:ids)")
public abstract List<String> queryStudentNameById(List<Integer> ids);
}
在编译的时候,Room 就会对 SQL 语句做检查,如果存在问题,会编译不通过,而不会在运行时出错。
对于方法参数,Room 支持使用 :xxx
形式将参数列表绑定到查询语句中,但是需要注意的是,只支持 999 个项绑定到语句中,这是 SQLite 的限制(999也足够了...)。
在 Query 方法中,除了 SELECT 语句之外,还支持 INSERT
,UPDATE
和DELETE
:
INSERT 查询可以返回 void
或 long
。如果是 long
值,则该值是插入的行的 rowid。
请注意,插入多行不能返回多个 rowid,只返回最后一个。
void
或 int
。如果是int
,则该值是受此查询影响的行数。修改和删除其实没啥区别,只是注释改成了 Update
,同样是根据主键来确定更新的主体:
代码略
我们经常会有一个对象的属性是另一个类的对象的情况,在 Room 中是明确禁止这样做的,具体原因看这里:
虽然不允许直接引用其他 Entity 对象,但是对于类似需求,Room 可以使用外键 @ForeignKey
来定义和其他 Entity 的联系:
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
public class Book {
@PrimaryKey
public int bookId;
public String title;
@ColumnInfo(name = "user_id")
public int userId;
}
在上面的代码中, Book
entity 有一个作者的外键引用 User
,可以通过 @ForeignKey
注解指定这个外键约束。
外键约束还可以通过 @ForeignKey
注解的 onDelete
和 onUpdate
属性指定级联操作,如级联更新和级联删除,@ForeignKey
注解中有两个属性 onDelete
和onUpdate
, 这两个属性对应 ForeignKey
中的 onDelete()
和 onUpdate()
, 通过这两个属性的值来设置当 User 对象被删除/更新时,Book 对象作出的响应。这两个属性的可选值如下:
CASCADE
:User 删除时对应 Book 一同删除; 更新时,关联的字段一同更新NO_ACTION
:User 删除时不做任何响应RESTRICT
:禁止 User 的删除/更新。当 User 删除或更新时,Sqlite 会立马报错。SET_NULL
:当 User 删除时, Book中 的 userId 会设为 NULLSET_DEFAULT
:与 SET_NULL
类似,当 User 删除时,Book 中的 userId 会设为默认值在某些情况下, 对于一张表中的数据我们会用多个 POJO 类来表示,在这种情况下可以用@Embedded
注解嵌套的对象,比如:
class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
以上代码所产生的 User 表中,Column 为id, firstName, street, state, city
还有 post_code
。
Room支持联表查询,接口定义上与其他查询差别不大, 主要还是 sql 语句的差别。
@Dao
public interface MyDao {
@Query("SELECT * FROM book "
+ "INNER JOIN loan ON loan.book_id = book.id "
+ "INNER JOIN user ON user.id = loan.user_id "
+ "WHERE user.name LIKE :userName")
public List<Book> findBooksBorrowedByNameSync(String userName);
}
Room 中 DataBase 类似 SQLite API 中 SQLiteOpenHelper,是提供 DB 操作的切入点,但是除了持有 DB 外, 它还负责持有相关数据表(Entity)的数据访问对象(DAO), 所以 Room 中定义 Database 需要满足三个条件:
RoomDataBase
,并且是一个抽象类@Database
注解,并定义相关的 entity 对象, 当然还有必不可少的数据库版本信息@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
创建好以上 Room 的三大组件后, 在代码中就可以通过以下代码创建 Database 实例。
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
在使用 SQLite 的时候,当需要升级数据库需要修改数据库的 version,当 version 大于现存数据库的 version 的时候,会执行 SQLiteOpenHelper 的 onUpgrade 方法里面的升级 SQL 语句。
Room 提供了 Migration 类来实现数据库的升级:
Room.databaseBuilder(getApplicationContext(), MyDb.class, "database-name")
.addMigrations(MIGRATION_1_2, MIGRATION_2_3).build();
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("CREATE TABLE `Fruit` (`id` INTEGER, "
+ "`name` TEXT, PRIMARY KEY(`id`))");
}
};
static final Migration MIGRATION_2_3 = new Migration(2, 3) {
@Override
public void migrate(SupportSQLiteDatabase database) {
database.execSQL("ALTER TABLE Book "
+ " ADD COLUMN pub_year INTEGER");
}
};
在创建Migration类时需要指定 startVersion
和 endVersion
, 代码中 MIGRATION_1_2
和 MIGRATION_2_3
的 startVersion 和 endVersion 是递增的, Migration 其实是支持从版本 1 直接升到版本 3,只要其 migrate()
方法里执行的语句正常即可。那么 Room 是怎么实现数据库升级的呢?其实本质上还是调用 SQLiteOpenHelper.onUpgrade
,Room 中自己实现了一个 SQLiteOpenHelper
, 在 onUpgrade()
方法被调用时触发 Migration
,当第一次访问数据库时,Room 做了以下几件事:
SQLiteOpenHelper.onUpgrade
被调用,并且触发 Migration