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

如何通过Ecto向PostgreSQL存储JSONB数据并对接GraphQL?

解决Ecto + PostgreSQL JSONB 结合 GraphQL 参数的完整方案

我来帮你一步步搞定这个需求——从数据库迁移、Ecto Schema定义,到GraphQL参数解析的全流程都给你理清楚:

1. 先修正你的数据库迁移

你当前用了:map类型,虽然Ecto在PostgreSQL环境下会默认把:map映射为jsonb,但为了更明确、避免后续歧义,建议直接指定:jsonb类型,另外把字段名从json改成json_data(json是Elixir关键字,用起来容易踩坑):

defmodule MyApp.CreateJsonTable do
  use Ecto.Migration

  def change do
    create table(:geodata) do
      add(:json_data, :jsonb) # 明确指定jsonb类型,字段名避开关键字
      timestamps(type: :utc_datetime)
    end
  end
end

2. 定义Ecto Schema(两种可选方案)

根据你的JSON数据是否有固定结构,有两种实现方式:

方案A:自由格式JSONB(无需Embedded Schema)

如果你的JSON是灵活的无固定结构,不需要字段级验证,直接用:map类型就行,Ecto会自动处理和PostgreSQL jsonb的转换:

defmodule MyApp.Geodata do
  use MyApp, :schema
  import Ecto.Changeset

  schema "geodata" do
    field :json_data, :map # 自动映射为PostgreSQL的jsonb
    timestamps(type: :utc_datetime)
  end

  # 可选:加个变更集,确保传入的是合法的Map/JSON
  def changeset(geodata, attrs) do
    geodata
    |> cast(attrs, [:json_data])
    |> validate_required([:json_data])
    |> validate_valid_json(:json_data)
  end

  # 自定义验证:确保字段是合法的Map(也就是能转成JSON的结构)
  defp validate_valid_json(changeset, field) do
    case get_change(changeset, field) do
      nil -> changeset
      value when is_map(value) -> changeset
      _ -> add_error(changeset, field, "必须是合法的JSON对象/Map")
    end
  end
end

方案B:结构化JSONB(用Embedded Schema做验证)

如果你的JSON有固定结构(比如地理数据必须包含经纬度),那用Embedded Schema来定义结构,Ecto会帮你做严格的字段验证:

# 先定义Embedded Schema,对应JSON的结构
defmodule MyApp.GeodataJson do
  use Ecto.Schema
  import Ecto.Changeset

  embedded_schema do
    field :latitude, :float
    field :longitude, :float
    field :metadata, :map # 嵌套的自由格式JSON
  end

  def changeset(json_data, attrs) do
    json_data
    |> cast(attrs, [:latitude, :longitude, :metadata])
    |> validate_required([:latitude, :longitude])
    # 验证经纬度的合法范围
    |> validate_number(:latitude, greater_than_or_equal_to: -90, less_than_or_equal_to: 90)
    |> validate_number(:longitude, greater_than_or_equal_to: -180, less_than_or_equal_to: 180)
  end
end

# 主Schema引用这个Embedded Schema
defmodule MyApp.Geodata do
  use MyApp, :schema
  import Ecto.Changeset

  schema "geodata" do
    # 指定用Embedded Schema,同时映射为jsonb类型
    field :json_data, Ecto.Embedded, schema: MyApp.GeodataJson, type: :jsonb
    timestamps(type: :utc_datetime)
  end

  def changeset(geodata, attrs) do
    geodata
    |> cast(attrs, [:json_data])
    |> cast_embed(:json_data) # 自动触发Embedded Schema的验证
    |> validate_required([:json_data])
  end
end

3. 处理GraphQL参数解析

假设你用的是Absinthe(Elixir最常用的GraphQL库),分两种情况处理参数:

情况1:前端传JSON字符串

如果前端只能传JSON字符串,你需要在Resolver里把字符串解析成Elixir Map,再传入Ecto变更集:

第一步:定义GraphQL输入类型

defmodule MyApp.Schema.Types.GeodataInput do
  use Absinthe.Schema.Notation

  input_object :geodata_input do
    field :json_data, :string # 接收JSON字符串
  end
end

第二步:Resolver里解析JSON

defmodule MyApp.Resolvers.GeodataResolver do
  alias MyApp.Geodata
  alias MyApp.Repo

  def create_geodata(_parent, %{input: input}, _context) do
    # 用Jason解析JSON字符串(Elixir官方推荐的JSON库,替换成Poison也可以)
    with {:ok, json_map} <- Jason.decode(input.json_data) do
      %Geodata{}
      |> Geodata.changeset(%{json_data: json_map})
      |> Repo.insert()
    else
      {:error, _reason} -> {:error, "传入的JSON字符串不合法"}
    end
  end
end

注:如果项目里用的是Poison,把Jason.decode换成Poison.decode就行,记得确保依赖里有对应的库。

情况2:前端传结构化参数(更友好)

如果你的JSON结构固定,直接在GraphQL里定义结构化的输入类型,前端可以传对象而非字符串,Absinthe会自动转成Elixir Map:

定义GraphQL输入类型

defmodule MyApp.Schema.Types.GeodataInput do
  use Absinthe.Schema.Notation

  input_object :geodata_json_input do
    field :latitude, :float
    field :longitude, :float
    field :metadata, :map
  end

  input_object :geodata_input do
    field :json_data, :geodata_json_input
  end
end

Resolver代码简化

def create_geodata(_parent, %{input: input}, _context) do
  %Geodata{}
  |> Geodata.changeset(input)
  |> Repo.insert()
end

4. 验证数据是否正确存储

插入数据后,你可以用Ecto查询验证:

geodata = Repo.get(Geodata, 1)
IO.inspect(geodata.json_data) # 会输出Elixir Map或者Embedded Schema结构体

也可以直接连PostgreSQL执行SQL查询:

SELECT json_data FROM geodata WHERE id = 1;

返回的就是合法的JSONB数据啦。

内容的提问来源于stack exchange,提问作者Mark Karavan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.26 09:28:06