数据库开发之Room

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. 值为二进制大对象数据(图片,文件等),存储的内容与输入时完全相同

类型映射:

JavaSQLiteType Affinity
byteINTEGER1
shortINTEGER1
intINTEGER1
longINTEGER1
floatREAL4
doubleREAL4
charTEXT2
StringTEXT2
booleanINTEGER1

编写迁移代码:

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

感谢大家的支持,如有错误请指正,如需转载请标明原文出处!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值