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,提问作者蔡濡安
相关产品推荐
相关产品推荐

