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

Vapor框架中如何利用父模型User的ID关联子模型Pers的数据?

问题描述

我在使用Vapor + Fluent构建一对多关联(User为父模型,Pers为子模型)时遇到了API调用报错的问题。目前的实现方式感觉用两个ID操作不太合理,而且路由配置里也没做好子模型和父模型的关联配置。想请教如何通过User的ID来正确关联并操作Pers的数据?

控制器代码

InformationController

final class InformationController{ 
    func create (_ req: Request) throws -> EventLoopFuture<Pers>{ 
        let perinfo=try req.content.decode(Pers.self) 
        return perinfo.save(on: req.db).map {perinfo} 
    } 
    func putpers (_ req: Request) throws -> EventLoopFuture<HTTPStatus> { 
        let information = try req.content.decode(Pers.self) 
        return Pers.find(information.$user.id, on: req.db) 
            .unwrap(or: Abort(.notFound)) 
            .flatMap{ 
                $0.name=information.name 
                $0.account=information.account 
                $0.picture=information.picture 
                $0.age=information.age 
                $0.birth=information.birth 
                $0.city=information.city 
                $0.hobby=information.hobby 
                return $0.update(on: req.db).transform(to: .ok) 
            } 
    } 
} 

UserController

final class UserController{ 
    func signup (_ req: Request) throws -> EventLoopFuture<User>{ 
        try User.Create.validate(content: req) 
        // 從request body中decode出User.Create 
        let newUser = try req.content.decode(User.Create.self) 
        // 先檢查密碼是否==確認密碼 
        guard newUser.password == newUser.confirmPassword else { 
            throw Abort(.badRequest, reason: "Passwords did not match") 
        } 
        // 將User.Create 轉成 User 
        let user = try User(account: newUser.account, password: Bcrypt.hash(newUser.password)) 
        // 儲存User並回傳 
        return user.save(on: req.db).map { user } 
    } 
}

路由代码

import Fluent 
import Vapor 
func routes(_ app: Application) throws { 
    let userController = UserController() 
    let informationController=InformationController() 
    let passwordProtected = app.grouped(User.authenticator(),User.guardMiddleware()) 
    let tokenProtected = app.grouped(UserToken.authenticator(),UserToken.guardMiddleware()) 
    //修改密碼 
    app.put("user","userId", use:userController.update)// 
    //第一次上傳個人資料 
    app.post("information", use:informationController.create)// 
    //更新個人資料 
    app.put("information", use:informationController.new) 
    //註冊 
    app.post("signup", use:userController.signup)//ok 
    //登陸 
    passwordProtected.post("login",use: userController.login)// 
    //登陸後收到個人資料 
    tokenProtected.get("getperinfo",use: userController.getperinfo)// 
    //收到他人資料 
    app.get("otherperinfo","userId",use:informationController.otherperinfo)// 
    //登出 
    app.delete("logout","user_tokensId", use: userController.logout)// 
}

模型代码

父模型 User

import Foundation 
import Fluent 
import Vapor 
import FluentPostgresDriver 
final class User:Model,Content{ 
    static let schema = "user" 
    @ID(key: .id) var id:UUID? 
    @Field(key:"帳號") var account:String 
    @Field(key: "密碼") var password:String 
    @Children(for: \.$user) var perinfo: [Pers] 
    init() {} 
    init(id: UUID?=nil, account:String, password:String){ 
        self.id=id 
        self.account=account 
        self.password=password 
    } 
}

子模型 Pers

import Foundation 
import Fluent 
import Vapor 
import FluentPostgresDriver 
final class Pers:Model,Content{ 
    static let schema = "pers" 
    @ID(key: .id) var id:UUID? 
    @Field(key: "姓名") var name: String 
    @Field(key: "IG帳號") var account: String 
    @Field(key: "頭像") var picture: String 
    @Field(key: "年紀") var age: String 
    @Field(key: "生日") var birth: String 
    @Field(key: "居住城市") var city: String 
    @Field(key: "興趣") var hobby : String 
    @Parent(key: "user_id") var user: User 
    init(){}
    init(id:UUID?=nil, name:String, account:String, picture:String ,age:String, birth:String, city:String, hobby:String, userId:UUID){ 
        self.id=id 
        self.name=name 
        self.account=account 
        self.picture=picture 
        self.age=age 
        self.birth=birth 
        self.city=city 
        self.hobby=hobby 
        self.$user.id=userId 
    } 
}
解决方案

我帮你梳理了问题所在,下面是具体的调整方案:

1. 路由配置优化:明确父子资源关联

现在的路由没有体现Pers是User的子资源,建议调整路由结构,要么通过Token自动获取当前登录用户ID,要么通过URL参数传入目标用户ID,避免手动传参出错:

场景1:登录用户操作自己的个人资料(Token保护)

// 登录用户专属的个人资料路由
tokenProtected.group("user") { userGroup in
    // 创建自己的个人资料
    userGroup.post("information", use: informationController.createForCurrentUser)
    // 更新自己的个人资料
    userGroup.put("information", use: informationController.updateForCurrentUser)
    // 获取自己的个人资料
    userGroup.get("information", use: informationController.getCurrentUserInfo)
}

// 获取他人的个人资料(公开或权限控制)
app.get("user", ":userId", "information", use: informationController.getOtherUserInfo)

场景2:通过URL参数指定用户操作Pers(适用于管理员场景)

app.group("user", ":userId") { userGroup in
    userGroup.post("information", use: informationController.createForUser)
    userGroup.put("information", use: informationController.updateForUser)
}

2. 控制器方法修改:基于User ID关联操作Pers

新增「当前用户创建个人资料」方法

func createForCurrentUser(_ req: Request) throws -> EventLoopFuture<Pers> {
    // 从Token中自动获取当前登录用户
    let currentUser = try req.auth.require(User.self)
    // 解码请求体中的资料数据(不需要传userId)
    let perInfoData = try req.content.decode(Pers.Create.self)
    // 创建Pers实例并关联当前用户ID
    let perInfo = Pers(
        name: perInfoData.name,
        account: perInfoData.account,
        picture: perInfoData.picture,
        age: perInfoData.age,
        birth: perInfoData.birth,
        city: perInfoData.city,
        hobby: perInfoData.hobby,
        userId: currentUser.id!
    )
    return perInfo.save(on: req.db).map { perInfo }
}

新增「当前用户更新个人资料」方法

func updateForCurrentUser(_ req: Request) throws -> EventLoopFuture<HTTPStatus> {
    let currentUser = try req.auth.require(User.self)
    let updateData = try req.content.decode(Pers.Update.self)
    
    // 通过用户ID查询对应的Pers记录
    return Pers.query(on: req.db)
        .filter(\.$user.id == currentUser.id!)
        .first()
        .unwrap(or: Abort(.notFound, reason: "未找到个人资料"))
        .flatMap { perInfo in
            // 仅更新传入的非空字段
            if let name = updateData.name { perInfo.name = name }
            if let account = updateData.account { perInfo.account = account }
            if let picture = updateData.picture { perInfo.picture = picture }
            if let age = updateData.age { perInfo.age = age }
            if let birth = updateData.birth { perInfo.birth = birth }
            if let city = updateData.city { perInfo.city = city }
            if let hobby = updateData.hobby { perInfo.hobby = hobby }
            
            return perInfo.update(on: req.db).transform(to: .ok)
        }
}

补充:创建DTO(数据传输对象)

为了避免直接暴露模型内部字段,建议创建专门的Create/Update结构体,还能添加验证规则:

extension Pers {
    struct Create: Content, Validatable {
        var name: String
        var account: String
        var picture: String
        var age: String
        var birth: String
        var city: String
        var hobby: String
        
        static func validations(_ validations: inout Validations) {
            // 添加字段验证规则,比如姓名不能为空
            validations.add("name", as: String.self, is: !.empty)
        }
    }
    
    struct Update: Content {
        // 用可选类型支持部分字段更新
        var name: String?
        var account: String?
        var picture: String?
        var age: String?
        var birth: String?
        var city: String?
        var hobby: String?
    }
}

3. 修复原代码中的错误

  • 原putpers方法里用Pers.find(information.$user.id, on: req.db)是错误的:information.$user.id是用户ID,而Pers.find是按Pers自身的主键查询,应该用filter(\.$user.id == xxx)来关联用户ID查询。
  • 原路由中app.put("information", use:informationController.new)的new方法不存在,需要替换为修改后的update方法。

4. 确保数据库迁移正确

确认Pers表的user_id字段是关联User表的外键,迁移文件示例:

struct CreatePers: Migration {
    func prepare(on database: Database) -> EventLoopFuture<Void> {
        return database.schema("pers")
            .id()
            .field("姓名", .string, .required)
            .field("IG帳號", .string, .required)
            .field("頭像", .string)
            .field("年紀", .string)
            .field("生日", .string)
            .field("居住城市", .string)
            .field("興趣", .string)
            .field("user_id", .uuid, .required, .references("user", "id"))
            .create()
    }
    
    func revert(on database: Database) -> EventLoopFuture<Void> {
        return database.schema("pers").delete()
    }
}

内容的提问来源于stack exchange,提问作者蔡濡安

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 05:02:38