You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 06:52:05