如何使用Vapor Fluent为PostgreSQL表建模复合主键?
问题描述
给定PostgreSQL表结构:
CREATE TABLE LinkedUserProfile ( user_profile_id int8 NOT NULL, target_user_profile_id int8 NOT NULL, createdDate timestamp with time zone NOT NULL, PRIMARY KEY (user_profile_id, target_user_profile_id));
在JPA中可以通过嵌入式主键轻松实现建模,代码如下:
@Embeddable public class LinkedUserProfileKey implements Serializable { private static final long serialVersionUID = 5324124962006505075L; @Column(name = "target_user_profile_id") private Long targetUserProfileID = null; @Column(name = "user_profile_id") private Long userProfileID = null; // 构造器、equals、hashCode等方法省略... } @Entity(name = "LinkedUserProfile") @Table(name = "LinkedUserProfile") public class LinkedUserProfileBean extends AbstractDomainEntity<LinkedUserProfileKey> { @Column @Temporal(TemporalType.TIMESTAMP) @NotNull private OffsetDateTime createdDate = null; @EmbeddedId private LinkedUserProfileKey id = null; @ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "target_user_profile_id", referencedColumnName = "id") @MapsId("targetUserProfileID") @NotNull private UserProfileBean targetUserProfile = null; @ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE }, fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "user_profile_id", referencedColumnName = "id") @MapsId("userProfileID") @NotNull private UserProfileBean userProfile = null; // 构造器、getter/setter等方法省略... }
但在Swift + Fluent环境下,未找到清晰文档说明如何建模该复合主键结构,使用@ID(custom:)属性包装器时多次报错,需解决方案及建模示例。
Fluent建模解决方案
1. 定义复合主键结构体
首先创建符合CompositeKey和Codable协议的结构体,用来表示复合主键:
import Fluent struct LinkedUserProfileID: CompositeKey, Codable { // 对应数据库int8类型,使用Swift的Int let userProfileID: Int let targetUserProfileID: Int // 复合主键初始化器 init(userProfileID: Int, targetUserProfileID: Int) { self.userProfileID = userProfileID self.targetUserProfileID = targetUserProfileID } // 映射Swift属性到数据库列名 static let keys: [PartialKeyPath<Self>: String] = [ \.userProfileID: "user_profile_id", \.targetUserProfileID: "target_user_profile_id" ] }
2. 实现LinkedUserProfile实体类
创建Model实体类,绑定复合主键、业务字段及关联关系:
import Fluent final class LinkedUserProfile: Model { // 指定数据库表名 static let schema = "LinkedUserProfile" // 绑定自定义复合主键 @ID(custom: "id") var id: LinkedUserProfileID? // 映射createdDate字段,自动适配PostgreSQL的timestamp with time zone @Field(key: "createdDate") var createdDate: Date // 关联发起关联的UserProfile(多对一) @Parent(key: "user_profile_id") var userProfile: UserProfile // 关联目标UserProfile(多对一) @Parent(key: "target_user_profile_id") var targetUserProfile: UserProfile // Fluent要求的空初始化器 init() {} // 通过主键直接初始化 init(id: LinkedUserProfileID, createdDate: Date) { self.id = id self.createdDate = createdDate // 关联字段会自动通过主键同步,无需手动赋值 } // 通过关联对象初始化(更常用) init(userProfile: UserProfile, targetUserProfile: UserProfile, createdDate: Date) throws { self.$userProfile.id = try userProfile.requireID() self.$targetUserProfile.id = try targetUserProfile.requireID() self.id = LinkedUserProfileID( userProfileID: try userProfile.requireID(), targetUserProfileID: try targetUserProfile.requireID() ) self.createdDate = createdDate } }
3. 添加迁移配置(可选)
如果用Fluent迁移系统管理表结构,添加以下扩展创建表及约束:
extension LinkedUserProfile: Migration { static func prepare(on database: Database) -> EventLoopFuture<Void> { database.schema(schema) // 定义字段类型及约束 .field("user_profile_id", .int64, .required) .field("target_user_profile_id", .int64, .required) .field("createdDate", .timestampWithTimeZone, .required) // 设置复合主键 .primaryKey("user_profile_id", "target_user_profile_id") // 添加外键约束,关联UserProfile表 .foreignKey("user_profile_id", references: UserProfile.schema, "id") .foreignKey("target_user_profile_id", references: UserProfile.schema, "id") .create() } static func revert(on database: Database) -> EventLoopFuture<Void> { database.schema(schema).delete() } }
4. UserProfile实体示例(参考)
确保关联的UserProfile实体主键类型与LinkedUserProfileID一致:
final class UserProfile: Model { static let schema = "UserProfile" @ID(key: "id") var id: Int? // 其他业务字段... init() {} init(id: Int) { self.id = id } }
关键注意事项
- 复合主键必须实现
CompositeKey协议,并通过keys属性明确映射Swift属性到数据库列名,这是使用@ID(custom:)报错的常见原因。 - 使用
@Parent处理多对一关联时,Fluent会自动维护外键字段,无需手动声明重复的外键属性。 - 数据库字段类型要与Swift类型匹配:PostgreSQL的
int8对应Swift的Int,timestamp with time zone对应Swift的Date。
内容的提问来源于stack exchange,提问作者Shadowman
相关产品推荐
相关产品推荐

