Room 在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。
dependencies {
def room_version = "2.2.6"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
}
Room 包含 3 个主要组件:
-
数据库:包含数据库持有者,并作为应用已保留的持久关系型数据的底层连接的主要接入点
使用 @Database 注释的类应满足以下条件:- 继承RoomDatabase 的抽象类
- 在注释中添加与数据库关联的实体列表
- 包含无参数且返回使用@Dao注释的类的抽象方法
在运行时,可以通过调用Room.databaseBuilder()或Room.inMemoryDatabaseBuilder()获取Database的实例
-
Entity:表示数据库中的表
-
DAO:包含用于访问数据库的方法
应用使用Room数据库来获取与该数据库关联的数据访问对象 (DAO)。然后,应用使用每个DAO从数据库中获取实体,然后再将对这些实体的所有更改保存回数据库中。最后,应用使用实体来获取和设置与数据库中的表列相对应的值。
Room不同组件之间的关系图:
实践
定义实体
每个实体都会对应在Database创建一个表。
每个实体必须将至少1个字段定义为主键。即使只有1个字段,也需要为该字段添加@PrimaryKey注释。
如果想让Room为实体分配自动ID,则可以设置@PrimaryKey的autoGenerate属性。
如果实体具有复合主键,可以使用@Entity注释的primaryKeys属性。
默认情况下,Room会为实体中定义的每个字段创建一个列。如果某个实体中有不想保留的字段,则可以给字段添加@Ignore注释。如果实体继承了父实体的字段,则可以使用@Entity属性的ignoredColumns属性忽略字段
注意:必须通过Database类中的entities数组引用定义的实体类。
- Plant
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.PrimaryKey;
@Entity(tableName = "plants")
public class Plant {
@NonNull
@PrimaryKey
@ColumnInfo(name = "id")
String plantId;
String name;
String description;
int growZoneNumber;
int wateringInterval = 7; // how often the plant should be watered, in days
String imageUrl = "";
}
- GardenPlanting
import androidx.room.ColumnInfo;
import androidx.room.Entity;
import androidx.room.ForeignKey;
import androidx.room.Index;
import androidx.room.PrimaryKey;
import java.util.Calendar;
@Entity(
tableName = "garden_plantings",
foreignKeys = {
@ForeignKey(entity = Plant.class, parentColumns = {"id"}, childColumns = {"plant_id"}),
},
indices = {@Index("plant_id")}
)
public class GardenPlanting {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
long gardenPlantingId = 0;
@ColumnInfo(name = "plant_id")
String plantId;
@ColumnInfo(name = "plant_date")
Calendar plantDate = Calendar.getInstance();
@ColumnInfo(name = "last_watering_date")
Calendar lastWateringDate = Calendar.getInstance();
}
注意:SQLite中的表名称不区分大小写
复杂数据
Room提供了基本类型和包装类型之间进行转换的功能,但不允许在实体之间进行对象引用。如果需要支持自定义类型,需要提供一个 TypeConverter,它可以在自定义类型与Room可以保留的已知类型之间转换。
import androidx.room.TypeConverter;
import java.util.Calendar;
public class Converters {
@TypeConverter
public long calendarToDatestamp(Calendar calendar) {
return calendar.getTimeInMillis();
}
@TypeConverter
public Calendar datestampToCalendar(long value) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(value);
return calendar;
}
}
嵌套对象
使用@Embedded注解需要分解为表格中的子字段的对象。然后,就可以像查询列一样查询嵌套字段。
@Entity(tableName = "plants")
public class Plant {
@NonNull
@PrimaryKey
@ColumnInfo(name = "id")
String plantId;
String name;
String description;
int growZoneNumber;
int wateringInterval = 7; // how often the plant should be watered, in days
String imageUrl = "";
@Embedded public PlantValue plantValue;
}
@Entity
public class PlantValue {
@PrimaryKey public int id;
public String edibleValue;
public String rawMaterialValue;
}
嵌套字段还可以包含其他嵌套字段。如果实体具有相同类型的多个嵌套字段,可以通过@Embedded(prefix = “ecology”)设置prefix属性确保每个列的唯一性,这样Room会将提供的值添加到嵌套对象中每个列名称的前面。
字段唯一性
可以将数据库中的某些列编入索引,以加快查询速度。如果数据库中的某个字段或某些字段组合必须是唯一的。可以通过将**@Index注解的unique**属性设为true,强制指定唯一性。
@Entity(tableName = "file", indices = {@Index(value = {"file_path"}, unique = true)})
public class FileEntity {
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
long id;
@ColumnInfo(name = "file_path")
String filePath;
@ColumnInfo(name = "file_name")
String fileName;
@ColumnInfo(name = "create_time")
long createTime;
}
定义DAO
数据访问对象 (DAO)提供对应用数据库的抽象访问权限。DAO既可以是接口,也可以是抽象类。如果是抽象类,则DAO可以定义一个参数为RoomDatabase的构造函数。Room会在编译时实现DAO的方法。
- PlantDao
import androidx.room.Dao;
import androidx.room.Insert;
import androidx.room.OnConflictStrategy;
import androidx.room.Query;
import androidx.room.Update;
import java.util.List;
@Dao
public interface PlantDao {
@Query("SELECT * FROM plants ORDER BY name")
List<Plant> getPlants();
@Query("SELECT * FROM plants WHERE growZoneNumber = :growZoneNumber ORDER BY name")
List<Plant> getPlantsWithGrowZoneNumber(int growZoneNumber);
@Query("SELECT * FROM plants WHERE id = :plantId")
Plant getPlant(String plantId);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List<Plant> plants);
@Update
void updateAll(List<Plant> plants);
}
- GardenPlantingDao
import androidx.room.Dao;
import androidx.room.Delete;
import androidx.room.Insert;
import androidx.room.Query;
import androidx.room.Transaction;
import androidx.room.Update;
import java.util.List;
@Dao
public interface GardenPlantingDao {
@Query("SELECT * FROM garden_plantings")
List<GardenPlanting> getGardenPlantings();
@Query("SELECT EXISTS(SELECT 1 FROM garden_plantings WHERE plant_id = :plantId LIMIT 1)")
boolean isPlanted(String plantId);
/**
* 多表组合查询
*
* @return
*/
@Transaction
@Query("SELECT * FROM plants WHERE id IN (SELECT DISTINCT(plant_id) FROM garden_plantings)")
List<PlantAndGardenPlantings> getPlantedGardens();
@Insert
long insertGardenPlanting(GardenPlanting gardenPlanting);
@Delete
void deleteGardenPlanting(GardenPlanting gardenPlanting);
@Update
void updateGardenPlanting(GardenPlanting... gardenPlanting);
}
注意:除非对构建器调用allowMainThreadQueries(),否则Room不支持在主线程上访问数据库,因为Room可能会阻塞主线程
创建数据库
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import androidx.sqlite.db.SupportSQLiteDatabase;
@Database(entities = {GardenPlanting.class, Plant.class}, version = 1, exportSchema = false)
@TypeConverters(Converters.class)
public abstract class AppDatabase extends RoomDatabase {
public abstract PlantDao plantDao();
public abstract GardenPlantingDao gardenPlantingDao();
private static final String DATABASE_NAME = "garden_plant.db";
private static volatile AppDatabase instance = null;
public static AppDatabase getInstance(Context context) {
if (instance == null) {
synchronized (AppDatabase.class) {
if (instance == null) {
instance = buildDatabase(context);
}
}
}
return instance;
}
private static AppDatabase buildDatabase(Context context) {
return Room.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
}
}).build();
}
}
注意:每个RoomDatabase实例的成本相当高,如果应用在单进程中运行,在实例化AppDatabase对象时应遵循单例设计模式。
如果应用在多进程中运行,在构造数据库时调用enableMultiInstanceInvalidation()方法。这样,如果在每个进程中都有一个AppDatabase实例,可以在一个进程中使共享数据库文件失效,并且这种失效会自动传播到其他进程中AppDatabase的实例。
最后,通过AppDatabase单例对象获取到对应的DAO就可以进行增删改查操作了。
数据库迁移
SQLite数据库支持的存储类型如下:
- NULL. 值为NULL
- INTEGER. 值为有符号整数,根据值的大小存储在1、2、3、4、6或8个字节中
- REAL. 值为浮点数,存储为8字节IEEE浮点数
- TEXT. 值为文本字符串,使用数据库编码(UTF-8,UTF-16BE或UTF-16LE)存储
- BLOB. 值为二进制大对象数据(图片,文件等),存储的内容与输入时完全相同
类型映射:
Java | SQLite | Type Affinity |
---|---|---|
byte | INTEGER | 1 |
short | INTEGER | 1 |
int | INTEGER | 1 |
long | INTEGER | 1 |
float | REAL | 4 |
double | REAL | 4 |
char | TEXT | 2 |
String | TEXT | 2 |
boolean | INTEGER | 1 |
编写迁移代码:
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
//创建表
database.execSQL("CREATE TABLE users_new (userid TEXT NOT NULL, username TEXT, last_update INTEGER NOT NULL, PRIMARY KEY(userid))");
//创建表(主键自增)
database.execSQL("CREATE TABLE book (bookid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, bookname TEXT, last_update INTEGER NOT NULL)");
//添加表字段
database.execSQL("ALTER TABLE users ADD COLUMN last_update INTEGER NOT NULL");
//查询并插入数据
database.execSQL("INSERT INTO users_new (userid, username, last_update) SELECT userid, username, last_update FROM users");
//删除表
database.execSQL("DROP TABLE users");
//重命名表
database.execSQL("ALTER TABLE users_new RENAME TO users");
//创建索引
database.execSQL("CREATE INDEX index_users_userid ON users(userid)");
}
};
最后添加到RoomDatabase.Builder中:
private static AppDatabase buildDatabase(Context context) {
return Room.databaseBuilder(context, AppDatabase.class, DATABASE_NAME)
.addCallback(new Callback() {
@Override
public void onCreate(@NonNull SupportSQLiteDatabase db) {
super.onCreate(db);
}
})
.addMigrations(MIGRATION_1_2)
.addMigrations(MIGRATION_2_3)
.build();
}
注意:对于整型和浮点型数据,编写迁移语句的时候必须加非空NOT NULL
感谢大家的支持,如有错误请指正,如需转载请标明原文出处!