4. 一对一关系映射定义与实现

一对一关系是数据库关系映射中最基础的形式。在 Room 中实现一对一关系需要掌握四个核心元素:外键约束用于强制引用完整性,嵌套对象用于承载关联数据,@Relation 注解用于声明关系类型,DAO 查询方法用于返回组合后的数据对象。本章通过一个用户与其地址的经典案例,带您完成从实体定义到查询返回的完整闭环。

核心要点:一对一映射的关键在于“拥有表”通过外键引用“被拥有表”,而 Room 通过 @Relation 注解在运行时自动构建嵌套对象的赋值逻辑。

4.1 实体定义与外键约束

以 User 和 Address 为例,每个用户只能拥有一个地址。在 Address 实体中通过外键引用 User 的主键,并设置级联删除策略。

@Entity
public class User {
    @PrimaryKey(autoGenerate = true)
    public long userId;
    public String name;
    public int age;
}

@Entity(
    foreignKeys = @ForeignKey(
        entity = User.class,
        parentColumns = "userId",
        childColumns = "userOwnerId",
        onDelete = ForeignKey.CASCADE
    ),
    indices = @Index(value = "userOwnerId")
)
public class Address {
    @PrimaryKey(autoGenerate = true)
    public long addressId;
    public String street;
    public String city;
    public long userOwnerId;  // 外键,关联 User.userId
}
外键索引:为外键列 userOwnerId 创建索引可显著提升关联查询性能,同时避免 Room 在编译时发出警告。

4.2 定义关系嵌套对象

创建一个 POJO 类,包含主实体 User 和关联实体 Address,通过 @Relation 注解声明一对一关系。Room 要求被关联的字段类型必须是实体类或实体类的集合,且注解中需要指定 parentColumn 和 entityColumn。

public class UserWithAddress {
    @Embedded
    public User user;

    @Relation(
        parentColumn = "userId",
        entityColumn = "userOwnerId"
    )
    public Address address;
}
注解参数 说明 一对一场景值
parentColumn 主实体(拥有方)的主键列 User.userId
entityColumn 关联实体中被拥有的外键列 Address.userOwnerId
associateBy 用于间接关联(一对一通常不直接用) —

4.3 DAO 查询方法编写

Room 会识别 @Relation 注解并生成内部 JOIN 查询,开发者只需编写返回关系 POJO 的方法即可。注意:查询语句必须使用标准的 SQL JOIN 语法,Room 无法自动推断关联条件。

@Dao
public interface UserDao {
    // 一对一:查询所有用户及其地址
    @Transaction
    @Query("SELECT * FROM User")
    List<UserWithAddress> getUsersWithAddresses();

    // 通过用户 ID 查询单个用户及其地址
    @Transaction
    @Query("SELECT * FROM User WHERE userId = :userId")
    UserWithAddress getUserWithAddress(long userId);
}
@Transaction 的必要性:当返回类型包含 @Relation 字段时,Room 需要执行两条 SQL(先查主表,再查关联表),因此必须在 DAO 方法上添加 @Transaction 注解,否则编译时会抛出异常。

4.4 编译时验证与常见错误

Room 在编译阶段会严格检查一对一映射的正确性:

  • 外键列类型不匹配:parentColumn 与 entityColumn 必须类型兼容(如 long 与 Long 可以,但 long 与 int 会报错)。
  • 缺少 @Transaction:若返回关系 POJO 的方法未标注 @Transaction,编译时提示 relationship without @Transaction。
  • 关联字段为 null:若某用户没有地址,返回的 UserWithAddress.address 字段为 null,需要在业务层处理空值。

4.5 数据迁移与性能优化

当表结构变更(如新增外键列)时,需编写 Migration 脚本。一对一场景中,外键约束可能影响数据库写入顺序,需确保先插入主实体再插入关联实体。

static final Migration MIGRATION_2_3 = new Migration(2, 3) {
    @Override
    public void migrate(@NonNull SupportSQLiteDatabase database) {
        database.execSQL("ALTER TABLE Address ADD COLUMN userOwnerId INTEGER NOT NULL DEFAULT 0");
        database.execSQL("CREATE INDEX IF NOT EXISTS index_address_userOwnerId ON Address(userOwnerId)");
        // 注意:若已有数据,需手动修复外键关系
    }
};
性能优化建议:
· 为外键列创建索引(已在实体定义中通过 @Index 实现)。
· 避免在 @Relation 查询中返回过多列,若不需要全部字段,可定义更小的投影 POJO。
· 使用 @Transaction 时注意事务粒度,避免在大列表查询中产生长事务。

4.6 一对一关系映射全景图

下方 SVG 展示了从实体定义到查询返回的完整数据流,包括编译时验证、外键关系、@Relation 注解映射以及 DAO 执行过程。

一对一关系映射流程图(User ↔ Address) User (Entity) @PrimaryKey userId (long) name (String) age (int) Address (Entity) @PrimaryKey addressId (long) street, city (String) @ForeignKey → User.userId FK UserWithAddress (POJO) @Embedded User user @Relation( parentColumn="userId", entityColumn="userOwnerId") Address address @Embedded @Relation UserDao(DAO 接口) @Transaction @Query(“SELECT * FROM User”) List<UserWithAddress> getUsersWithAddresses() 内部执行:先查 User,再对每个 User 查 Address 编译时识别 @Relation 返回结果 UserWithAddress{ user=User{...}, address=Address{...} } 查询返回 编译时验证检查点 外键类型匹配 / @Transaction 存在 / parentColumn 与 entityColumn 对应 / 索引创建
课后自查清单:
✅ 确认外键列在子实体中正确声明,且与主实体主键类型一致。
✅ 关系 POJO 的 @Relation 注解参数填写无误,且方法标注 @Transaction。
✅ 数据库迁移脚本包含新外键列和索引的 DDL。
✅ 业务层处理关联实体为 null 的情况(如刚注册但未填写地址的用户)。