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 执行过程。
课后自查清单:
✅ 确认外键列在子实体中正确声明,且与主实体主键类型一致。
✅ 关系 POJO 的
✅ 数据库迁移脚本包含新外键列和索引的 DDL。
✅ 业务层处理关联实体为
✅ 确认外键列在子实体中正确声明,且与主实体主键类型一致。
✅ 关系 POJO 的
@Relation 注解参数填写无误,且方法标注 @Transaction。✅ 数据库迁移脚本包含新外键列和索引的 DDL。
✅ 业务层处理关联实体为
null 的情况(如刚注册但未填写地址的用户)。