第4章 字段约束与索引:@ColumnInfo、非空、唯一约束、@Index注解、复合索引
4.1 为什么需要字段约束与索引
数据库表中的字段约束用于保证数据的完整性和一致性,而索引则是提升查询性能的核心手段。Room 通过注解机制提供了声明式的约束和索引配置,让开发者无需手写 SQL 即可在编译期生成正确的表定义。本章涵盖 @ColumnInfo 的详细配置、非空(NOT NULL)和唯一(UNIQUE)约束的声明方式、@Index 注解实现单列索引和复合索引,以及实际开发中的调试与优化要点。
CREATE TABLE 语句中。尽量在数据库设计阶段就规划好约束与索引,避免后期迁移。
4.2 @ColumnInfo 注解详解
@ColumnInfo 用于控制实体字段到数据库列的映射细节,包括列名、类型、是否允许为空、默认值等。常用参数如下:
| 参数 | 类型 | 说明 | 示例 |
|---|---|---|---|
name | String | 自定义列名,默认与字段名相同 | @ColumnInfo(name = "user_name") |
typeAffinity | 枚举 | SQLite 列类型(TEXT/INTEGER/REAL/BLOB) | typeAffinity = ColumnInfo.TEXT |
index | boolean | 是否为该列创建单列索引,默认 false | index = true |
defaultValue | String | 列默认值(SQL 表达式) | defaultValue = "0" |
// 完整的 @ColumnInfo 示例
@Entity
public class User {
@PrimaryKey
@ColumnInfo(name = "user_id", typeAffinity = ColumnInfo.INTEGER)
public int id;
@ColumnInfo(name = "user_name", index = true)
public String name;
@ColumnInfo(name = "email", defaultValue = "unknown@example.com")
public String email;
}
4.3 非空约束(@NonNull)
Room 使用 androidx.annotation.NonNull 标记字段不允许为 null,对应 SQL 中的 NOT NULL。该注解需要添加到字段声明上,编译期检查 null 安全性,并生成带 NOT NULL 的 DDL。
// 非空约束示例
@Entity
public class Product {
@PrimaryKey
@NonNull
public String sku; // NOT NULL
@NonNull
public String name; // NOT NULL
public Double price; // 可空
}
生成的 CREATE TABLE 语句:
CREATE TABLE Product (
sku TEXT NOT NULL,
name TEXT NOT NULL,
price REAL,
PRIMARY KEY(sku)
)
4.4 唯一约束(@Unique)
唯一约束可以通过两种方式实现:在 @ColumnInfo 中设置 unique = true(仅适用于单个字段),或者使用 @Index(unique = true) 同时创建唯一索引。后者更灵活,可覆盖单列和复合列。
// 方式一:在字段上声明唯一约束
@Entity
public class Account {
@PrimaryKey
public int id;
@ColumnInfo(name = "email", unique = true)
public String email; // 唯一
}
// 方式二:通过 @Index 声明唯一索引(推荐)
@Entity(indices = {@Index(value = "email", unique = true)})
public class Account {
@PrimaryKey
public int id;
public String email;
}
4.5 @Index 注解:单列索引
@Index 定义在 @Entity 的 indices 属性中,用于创建普通索引或唯一索引。其原理是在数据库层生成 CREATE INDEX 语句,加速基于该列的查询(如 WHERE、JOIN)。
// 单列索引示例
@Entity(indices = {@Index(value = "lastName")})
public class Person {
@PrimaryKey
public int personId;
public String firstName;
public String lastName;
}
生成的 SQL:
CREATE INDEX index_Person_lastName ON Person (lastName)
4.6 复合索引
对于多列组合查询(例如 WHERE department = ? AND level = ?),使用复合索引能显著提升性能。通过 @Index 的 value 数组指定多列,列顺序决定了索引的匹配规则(最左前缀原则)。
// 复合索引示例
@Entity(indices = {
@Index(value = {"department", "level"}, unique = false, name = "idx_dep_level")
})
public class Employee {
@PrimaryKey
public int empId;
public String department;
public int level;
public String name;
}
生成的索引:
CREATE INDEX idx_dep_level ON Employee (department, level)
department 几乎总是出现在 WHERE 中,则把它放在索引的第一位。索引不是越多越好,注意维护成本和写入性能影响。
4.7 实现流程与步骤
- 定义实体类:使用
@Entity注解,设置表名(可选)。 - 添加字段与约束:为关键字段添加
@NonNull、@ColumnInfo(设置唯一、索引)。 - 配置索引:在
@Entity(indices = {...})中声明单列或复合索引,指定索引名称(可选)。 - 编译项目:Room 会在编译时解析注解,生成带
NOT NULL、UNIQUE、CREATE INDEX的 DDL。 - 验证生成的结构:运行 App 后,通过
Database Inspector查看实际表结构和索引列表。 - 性能测试:针对高频查询运行
EXPLAIN QUERY PLAN确认索引是否被正确使用。
4.8 调试与验证
Android Studio 内置的 Database Inspector 可以直接查看数据库表结构、索引信息。也可以在 DAO 中执行原生 SQL 来验证:
// 在 DAO 中定义调试查询
@Query("PRAGMA table_info(user)")
LiveData> getTableInfo();
@Query("SELECT name FROM sqlite_master WHERE type = 'index' AND tbl_name = 'user'")
LiveData> getIndexes();
// 使用 EXPLAIN QUERY PLAN 查看索引是否生效
@Query("EXPLAIN QUERY PLAN SELECT * FROM user WHERE name = :name")
List explainQueryPlan(String name);
build.gradle 中启用 Room 的 schema 导出,可在 schemas/ 目录下看到完整的 JSON 格式数据库描述文件,其中包含索引明细。
4.9 常见错误
- 索引名称冲突:同一张表内的索引名称必须唯一,Room 默认索引名基于表和列名生成,自定义名称时需避免重复。
- 复合索引列顺序不当:索引未按查询条件最左列匹配导致索引失效,查询变慢。设计复合索引前建议先分析
WHERE子句的使用频率。 - 非空约束与默认值冲突:若字段声明
@NonNull同时又设置了@ColumnInfo(defaultValue = "NULL"),编译时不会报错但运行时可能插入失败。 - 唯一约束与已存在重复数据:在迁移后添加唯一约束时,若数据库已有重复值,迁移会失败。建议先清理数据或使用迁移策略。
- 忽略
@Index的unique属性:只设置@Index但不设unique = true,无法确保数据唯一性。
4.10 性能与稳定性优化
- 选择性优先:为区分度高的列(如邮箱、身份证号)建立索引,避免在布尔字段或仅有极少取值的字段上建索引。
- 复合索引列数控制:复合索引的列数建议不超过 3~4 列,否则更新开销会过大。
- 避免冗余索引:如果已有复合索引 (A, B),单独再建索引 (A) 通常是不必要的,因为复合索引可以覆盖单列 A 的查询。
- 批量插入时暂时禁用索引:对于大量初始数据导入,可考虑先移除索引,导入完成后再重建,以减少索引维护开销。
- 监控数据库增长:索引会增加数据库文件大小,定期通过
PRAGMA page_count和PRAGMA page_size评估存储影响。 - 使用 WAL 模式:在写多读少的场景下,启用 WAL(Write-Ahead Logging)能提升索引更新时的并发性能。
INSERT/UPDATE/DELETE 时都需要同步维护,可能成为性能瓶颈。建议先通过慢查询日志定位真实需求,再有针对性地添加索引。