第4章 字段约束与索引:@ColumnInfo、非空、唯一约束、@Index注解、复合索引

4.1 为什么需要字段约束与索引

数据库表中的字段约束用于保证数据的完整性和一致性,而索引则是提升查询性能的核心手段。Room 通过注解机制提供了声明式的约束和索引配置,让开发者无需手写 SQL 即可在编译期生成正确的表定义。本章涵盖 @ColumnInfo 的详细配置、非空(NOT NULL)和唯一(UNIQUE)约束的声明方式、@Index 注解实现单列索引和复合索引,以及实际开发中的调试与优化要点。

原则:在 Room 中,约束和索引都是在实体类的字段或类上通过注解声明的,编译后会自动反映在生成的 CREATE TABLE 语句中。尽量在数据库设计阶段就规划好约束与索引,避免后期迁移。

4.2 @ColumnInfo 注解详解

@ColumnInfo 用于控制实体字段到数据库列的映射细节,包括列名、类型、是否允许为空、默认值等。常用参数如下:

参数类型说明示例
nameString自定义列名,默认与字段名相同@ColumnInfo(name = "user_name")
typeAffinity枚举SQLite 列类型(TEXT/INTEGER/REAL/BLOB)typeAffinity = ColumnInfo.TEXT
indexboolean是否为该列创建单列索引,默认 falseindex = true
defaultValueString列默认值(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)
)
注意:基本类型(如 int、long)在 Room 中默认非空,但建议显式添加 @NonNull 以提高可读性。若使用包装类型(Integer、Long)且不添加 @NonNull,则列默认可为空。

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 中,则把它放在索引的第一位。索引不是越多越好,注意维护成本和写入性能影响。
字段约束与索引映射 字段约束与索引映射流程 实体类 User +id , +name , +email @ColumnInfo 注解 表 user id INT PK NOT NULL name TEXT NOT NULL email TEXT UNIQUE NOT NULL 约束 UNIQUE 约束 单列索引 (name) 复合索引 (dept, level) 约束与索引作用 • @NonNull → NOT NULL … 保证数据完整 • @Index(unique=true) → UNIQUE … 防止重复 • @Index → CREATE INDEX … 提升查询速度

4.7 实现流程与步骤

  1. 定义实体类:使用 @Entity 注解,设置表名(可选)。
  2. 添加字段与约束:为关键字段添加 @NonNull、@ColumnInfo(设置唯一、索引)。
  3. 配置索引:在 @Entity(indices = {...}) 中声明单列或复合索引,指定索引名称(可选)。
  4. 编译项目:Room 会在编译时解析注解,生成带 NOT NULL、UNIQUE、CREATE INDEX 的 DDL。
  5. 验证生成的结构:运行 App 后,通过 Database Inspector 查看实际表结构和索引列表。
  6. 性能测试:针对高频查询运行 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 时都需要同步维护,可能成为性能瓶颈。建议先通过慢查询日志定位真实需求,再有针对性地添加索引。