Kamrul Khan
Kamrul Khan

Reputation: 3350

Issue with updating JSON data into postgresql db: ** (Ecto.CastError) expected params to be a :map, got:)

Hi Im very new to Elixir/Phoenix. What Im trying to do is very simple. I have a table in my database with a jsonb column. I am trying to update the json data through Ecto. My model looks like this:

defmodule Setting.Setting do
  use Setting.Web, :model

  alias Setting.{VesselState, User, SettingData, FollowUp, LogEntry, VesselEvent, RiskModel, LegalExtract}

  schema "settings" do
    belongs_to :vessel_state, VesselState
    field :request_date, :date
    belongs_to :requester, User
    field :approved_date, :date
    belongs_to :manager, User
    field :analyst_feedback, :string
    belongs_to :analyst, User
    field :date, :utc_datetime
    field :status, :string
    field :points, :integer
    field :risk, :string
    field :verdict, :string
    embeds_one :data, SettingData, on_replace: :delete
    field :remarks, {:array, :string}
    embeds_many :follow_ups, FollowUp, on_replace: :delete
    embeds_many :log, LogEntry
    field :uuid, :string

    has_one :vessel_event, VesselEvent
    belongs_to :risk_model, RiskModel

    has_many :legal_extracts, LegalExtract

    timestamps()
  end

The data column in the schema is json and you see it is embedding another model (SettingData). If I fetch one record from the Setting table it will look like the follwing:

%Setting.Setting{
  __meta__: #Ecto.Schema.Metadata<:loaded, "settings">,
  analyst: #Ecto.Association.NotLoaded<association :analyst is not loaded>,
  analyst_feedback: "Approved",
  analyst_id: nil,
  approved_date: nil,
  data: %Setting.SettingData{
    id: "f25b6080-0751-4a65-8214-36e2810ea716",
    v1: %Setting.SettingDataV1{
      age_risk: %Setting.AgeRisk{
        age: 5.15,
        cif: false,
        id: "70b249f6-cef9-4473-a0ed-780e84cf6407",
      },
      class_society_risk: %Setting.ClassSocietyRisk{
        class_society: "Lloyds Register",
        iacs_member: true,
        id: "248bc975-5c39-4f92-afb6-a2b2365afa53",
        original_points: nil,
        overdue_class_published: false,
      }
   },
  date: ~U[2016-12-16 00:00:00.000000Z],
  follow_ups: [
    %Setting.FollowUp{
      action: nil,
      attachments: [],
      comment: nil,
      created_at: nil,
      file_reference: nil,
      id: "314b4445-b162-49f3-a1ca-f39a24af14bb",
    }
  ],
  id: 8,
  inserted_at: ~U[2017-01-02 08:25:58.913674Z],
  legal_extracts: #Ecto.Association.NotLoaded<association :legal_extracts is not loaded>,
  log: [],
  manager: #Ecto.Association.NotLoaded<association :manager is not loaded>,
  manager_id: nil,
  points: 15,
  remarks: [""],
  request_date: nil,
  requester: #Ecto.Association.NotLoaded<association :requester is not loaded>,
  requester_id: nil,
  risk: "Standard",
  risk_model: #Ecto.Association.NotLoaded<association :risk_model is not loaded>,
  risk_model_id: nil,
  status: "Completed",
  updated_at: ~U[2017-02-01 15:27:53.158000Z],
  uuid: nil,
  verdict: "Approved",
  vessel_event: #Ecto.Association.NotLoaded<association :vessel_event is not loaded>,
  vessel_state: #Ecto.Association.NotLoaded<association :vessel_state is not loaded>,
  vessel_state_id: 8
}

I simply want to change data.v1.age_risk.cif from false to true. I initially tried to create a change_set like,

cs = %{
  data: %{
    v1: %{
      age_risk: %{
        cif: true
      }
    }
  }
}

Setting.Setting.changeset(setting, cs) |> Setting.Repo.update()

This updates cif to true from false, but it sets all other elements in the json data to nil. So I figured Ill have to fetch the whole json, then update the element I want and then update the database with the new json. So basically I did something like this:

cs_new = %Setting.Setting{
  __meta__: #Ecto.Schema.Metadata<:loaded, "settings">,
  analyst: #Ecto.Association.NotLoaded<association :analyst is not loaded>,
  analyst_feedback: "Approved",
  analyst_id: nil,
  approved_date: nil,
  data: %Setting.SettingData{
    id: "f25b6080-0751-4a65-8214-36e2810ea716",
    v1: %Setting.SettingDataV1{
      age_risk: %Setting.AgeRisk{
        age: 5.15,
        cif: true,     #changed this to true
        id: "70b249f6-cef9-4473-a0ed-780e84cf6407",
      },
      class_society_risk: %Setting.ClassSocietyRisk{
        class_society: "Lloyds Register",
        iacs_member: true,
        id: "248bc975-5c39-4f92-afb6-a2b2365afa53",
        original_points: nil,
        overdue_class_published: false,
      }
   },
  date: ~U[2016-12-16 00:00:00.000000Z],
  follow_ups: [
    %Setting.FollowUp{
      action: nil,
      attachments: [],
      comment: nil,
      created_at: nil,
      file_reference: nil,
      id: "314b4445-b162-49f3-a1ca-f39a24af14bb",
    }
  ],
  id: 8,
  inserted_at: ~U[2017-01-02 08:25:58.913674Z],
  legal_extracts: #Ecto.Association.NotLoaded<association :legal_extracts is not loaded>,
  log: [],
  manager: #Ecto.Association.NotLoaded<association :manager is not loaded>,
  manager_id: nil,
  points: 15,
  remarks: [""],
  request_date: nil,
  requester: #Ecto.Association.NotLoaded<association :requester is not loaded>,
  requester_id: nil,
  risk: "Standard",
  risk_model: #Ecto.Association.NotLoaded<association :risk_model is not loaded>,
  risk_model_id: nil,
  status: "Completed",
  updated_at: ~U[2017-02-01 15:27:53.158000Z],
  uuid: nil,
  verdict: "Approved",
  vessel_event: #Ecto.Association.NotLoaded<association :vessel_event is not loaded>,
  vessel_state: #Ecto.Association.NotLoaded<association :vessel_state is not loaded>,
  vessel_state_id: 8
}

Setting.Setting.changeset(setting, cs_new) |> Setting.Repo.update()

This is now giving me error:

** (Ecto.CastError) expected params to be a :map, got: `%Setting.Setting ...

I tried converting this to map using Map.from_struct/1, but that only changes the outer layer. How should I fix this? or is there a different (and better) way of achieving what im trying to do?

Upvotes: 1

Views: 731

Answers (1)

Aleksei Matiushkin
Aleksei Matiushkin

Reputation: 121000

You are after Ecto.Changeset.cast_assoc/3.

Instead of updating the values directly, you should define a changeset for data and allow to do the rest for you.


Somewhat like this would do:

Ecto.Changeset.cast_assoc(
  setting, :data, with: &SettingData.changeset/2
)

Upvotes: 1

Related Questions