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

使用Fluent向PostgreSQL提交multipart/form-data时无法自动生成ID

解决Vapor Fluent自动生成ID失败问题

环境

MacOS 13.5.1、Vapor 4.77

场景

尝试通过Vapor 4.77向PostgreSQL数据库提交结构为{"title":"String", "description":"String", "Info":"Data"}的数据,其中info字段支持application/JSON或image/png类型。

问题

Fluent无法自动生成ID,报错提示缺少id字段。手动初始化ID后仍收到错误:

Response String: {"error":true,"reason":"Value required for key at path 'id'. No value associated with key \"id\"."}

相关代码

1. 服务器依赖包配置

dependencies: [
    // 💧 A server-side Swift web framework.
    .package(url: "https://github.com/vapor/vapor.git", from: "4.77.1"),
    // 🗄 An ORM for SQL and NoSQL databases.
    .package(url: "https://github.com/vapor/fluent.git", from: "4.8.0"),
    // 🐘 Fluent driver for Postgres.
    .package(url: "https://github.com/vapor/fluent-postgres-driver.git", from: "2.7.2"),
    .package(url: "https://github.com/vapor/multipart-kit.git", from: "4.5.4"),
],

2. 客户端构建multipart/form-data请求代码

// ...
let title = "Frederick C. Lee"
let description = "Mother had a feeling, I might be too appealing."

var body = Data()
let boundary = "Boundary-\(UUID().uuidString)"

// 构造请求
var request = URLRequest(url: serverURL)
request.httpMethod = "POST"
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")

// 三个表单字段:1) 标题 2) 描述 3) 信息(Data类型)
// 注意:多个'\r\n'是必要的语法

// 1) 标题
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"title\"\r\n".data(using: .utf8)!)
body.append("Content-Type: text/plain\r\n\r\n".data(using: .utf8)!)
body.append("\(title)\r\n".data(using: .utf8)!)
// -----------------
// 2) 描述
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"description\"\r\n".data(using: .utf8)!)
body.append("Content-Type: text/plain\r\n\r\n".data(using: .utf8)!)
body.append("\(description)\r\n".data(using: .utf8)!)
// -----------------
// 3) 信息(Data类型)
body.append("--\(boundary)\r\n".data(using: .utf8)!)
body.append("Content-Disposition: form-data; name=\"info\"\r\n".data(using: .utf8)!)
body.append("Content-Type: application/json\r\n\r\n".data(using: .utf8)!) // 可替换为image/png
body.append(infoData!) // 替换为实际的Data数据
body.append("\r\n".data(using: .utf8)!)

// 追加结束边界
body.append("--\(boundary)--\r\n".data(using: .utf8)!)
request.httpBody = body
return request
}

3. TurkeyGizzard模型代码

import Fluent
import Vapor

final class TurkeyGizzard: Model, Content {
    static let schema = "turkeygizzard"

    @ID(key: .id)
    var id: UUID?

    @Field(key: "title")
    var title: String

    @Field(key: "description")
    var description: String

    @Field(key: "info")
    var info: Data?

    init() {}

    init(title: String, description: String, info: Data) {
         self.id = UUID()
         self.title = title
         self.info = info
    }
}

4. 迁移代码

struct CreateTurkeyGizzards: AsyncMigration {
    func prepare(on database: FluentKit.Database) async throws {
        try await database.schema("turkeygizzard")
            .id()
            .field("title", .string)
            .field("description", .string)
            .create()
    }

    func revert(on database: FluentKit.Database) async throws {
        try await database.schema("turkeygizzard").delete()
    }
}

解决方案

方案1:修复Fluent自动生成ID的问题

报错核心原因是通过Content协议解析multipart请求时,Fluent的@ID字段默认被视为必填项,但客户端请求未传入id,且手动初始化的构造器不会被解析逻辑调用(解析时使用的是默认init())。

解决步骤:

  1. 修改模型的@ID字段配置,明确由数据库自动生成UUID,同时允许解析时忽略必填校验:

    @ID(key: .id, generatedBy: .database)
    var id: UUID?
    
  2. 更新迁移代码,补全info字段:
    原迁移代码未添加info字段,会导致数据库表结构不匹配,必须补上:

    try await database.schema("turkeygizzard")
        .id(custom: "id", type: .uuid, generatedBy: .database)
        .field("title", .string, .required)
        .field("description", .string, .required)
        .field("info", .data)
        .create()
    
  3. 调整控制器创建逻辑:
    直接解析请求数据为模型实例,调用save()即可让Fluent自动生成ID:

    func createGizzard(req: Request) async throws -> HTTPStatus {
        let gizzard = try req.content.decode(TurkeyGizzard.self)
        try await gizzard.save(on: req.db)
        return .created
    }
    

方案2:不使用Fluent,直接操作PostgreSQL

若不想依赖Fluent,可直接用Vapor的PostgreSQL驱动执行SQL:

func createGizzard(req: Request) async throws -> HTTPStatus {
    let title = try req.content.get(String.self, at: "title")
    let description = try req.content.get(String.self, at: "description")
    let info = try req.content.get(Data?.self, at: "info")

    try await req.db.raw("""
        INSERT INTO turkeygizzard (title, description, info)
        VALUES (?, ?, ?)
        """)
        .bind(title)
        .bind(description)
        .bind(info)
        .run()

    return .created
}

额外注意事项

  • 确保客户端请求的字段名info与模型、数据库字段名大小写一致(PostgreSQL默认区分大小写)。
  • 手动构造multipart请求时,需严格遵循格式规范,包括边界、换行符的正确使用,避免解析失败。

内容的提问来源于stack exchange,提问作者Frederick C. Lee

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 09:43:10