如何通过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
相关产品推荐
相关产品推荐

