Ecto changeset ignore nil fields in update

Hello All,

I want to add a field to an Ecto changeset provided the field is not nil. Besides, I do not want to update the value which is already in the database if this new value is nil.

I have tried various options but still not getting the desired result.

I would be very grateful for any help in this regard.

def update_device_data(params) do

    uniq_device_code=params[:uniq_device_code]
    
    
    manufacturerId=if !is_nil(params[:manufacturerId]) do
        params[:manufacturerId]
    end
    
    manufacturerCompanyName=if !is_nil(params[:manufacturerCompanyName]) do
        params[:manufacturerCompanyName]
    end
    
    deviceAliasName=if !is_nil(params[:deviceAliasName]) do
        params[:deviceAliasName]
    end
    
    deviceType=if is_nil(params[:deviceType]) do
        params[:deviceType]
    end
    
    modelTypeNumber=if !is_nil(params[:modelTypeNumber]) do
        params[:modelTypeNumber]
    end
    
    serialNumber=if !is_nil(params[:serialNumber]) do
        params[:serialNumber]
    end
    
    
    record = App.Repo.get_by(App.Schema.DeviceData, [device_id: uniq_device_code])
    changeset = App.Schema.DeviceData.changeset(record, %{device_serial_no: serialNumber, model_type_id: modelTypeNumber, device_type_code: deviceType, manufacturer_id: manufacturerId, manufacturer_company_name: manufacturerCompanyName, device_alias: deviceAliasName, updated_at: DateTime.truncate(Timex.now(), :second)})
    App.Repo.update(changeset)
end

This function is made far more complicated by accepting parameters with keys that don’t match your requirements. I would change your implementation to accept a map of params that match the keys of the DeviceData schema. If the function is being called by a dependency or something else that you can’t control then adding a separate function that transforms the keys to the expected format before passing the params to updated_device_data would work as well.

Then you can just filter any of the params that have a nil value like this:

def update_device_data(params) do
  alias App.Schema.DeviceData

  device_id = params[:device_id]

  params =
    params
    |> Enum.reject(fn {_, v} -> is_nil(v) end)
    |> Map.new()

  App.Repo.get_by(DeviceData, [device_id: device_id])
  |> DeviceData.changeset(params)
  |> App.Repo.update()
end
2 Likes

There is a syntax error as below:
** (SyntaxError) lib/app/func.ex:7: syntax error before: ‘->’

Too much javascript today. The anonymous function in Enum.reject is missing a few things. Edited the original answer.

1 Like

I’m a rather strong opponent of modifying user input before it has been cast. I’d rather do it that way:

changeset = cast(struct, params, fields)

changeset = 
  Enum.reduce(fields, changeset, fn field, changeset -> 
    case fetch_change(changeset, field) do
      {:ok, nil} -> delete_change(changeset, field)
      _ -> changeset
    end
  end)
4 Likes

haha… The reject function works now.

I am working on the translation of the keys and then I shall share the result here.

Thank you very much for simplifying the task for me.

1 Like

I was assuming since the params in the example have atom keys they couldn’t be user input.

If they were I would also prefer casting them first.

2 Likes

As @LostKobrakai mentioned if params is unvalidated user input you should cast it first. Checking for nil won’t prevent an empty string being set, if you cast it first then Ecto will transform empty strings to nil for you.

1 Like

Hi @joshdcuneo,

This is well noted.

Thanks a lot.

Thanks for the alternative provided, @LostKobrakai

I’m putting the code together to test.

Thanks @joshdcuneo. It works…

2 Likes