PJUllrich

PJUllrich

Author of Building Table Views with Phoenix LiveView

How to update a Struct without explicit Attributes

Hey folks,

I wondered whether it is possible to update a struct without explicitly defining the attributes that were updated. So, something similar to Ruby where you can update fields on an object and then run:

user = User.find_by(name: 'David')
user.name = 'Dave'
user.save

Let me explain in code what I want to achieve:

# I have a struct that has only one field called "status"
defmodule Order do

  # The struct has a function that updates the status field based on some domain logic
  def refund(%{status: status} = order) when status != :refunded do
    {:ok, Map.merge(order, %{status: :refunded})}
  end
  
  def refund(_order), do: {:error, :order_already_refunded}

  # The struct/Ecto.Schema also has a changeset that does some casting and validating
  # I don't want to have to define which attributes should be updated here though.
  # The changeset (or the context below) should auto-magically find the fields that changed
  # OR simply update all fields except associations. This would work too because then
  # I'd simply overwrite all fields that didn't change with their current value.
  def changeset(order) do
    attrs = somehow_get_attrs_without_knowing_which_fields_changed(order)

    order
    |> cast(attrs, [:status])
    |> validate_required([:status])
  end
end

defmodule Orders do
  # I have a context with an update function that only uses the struct
  # This is different from the usual "update_order(order, attrs)" function.
  def update_order(order) do
    order
    |> Order.changeset()
    |> Repo.update()
  end
end

test "refunds an order" do
  order = %Order{status: :paid}

  {:ok, refunded_order} = Order.refund(order)
  assert refunded_order.status == :refunded

  {:ok, updated_order} = Orders.update_order(order)
  assert updated_order.status == :refunded
end

So, I want to update a struct without explicitly defining which attributes are updated. I still want to use the struct’s changeset to make sure that all required fields are set etc. Do you know a way I could achieve this?

I tried the following, but it didn’t pick up all changes:

defmodule Orders do
  def update_order(order) do
     attrs = order |> Map.from_struct() |> Map.drop([:id, :__meta__, :inserted_at, :updated_at])
    update_order(order, attrs)
  end

  def update_order(order, attrs) do
    order
    |> Order.changeset(attrs)
    |> Repo.update()
  end
end

The problem with the approach above was that the Order.changeset compares the order with the attrs and if they didn’t change, then it doesn’t update them. Since the attrs have the same values as the provided order, it never updated anything.

Most Liked

al2o3cr

al2o3cr

The Ecto equivalent of returning an unsaved ActiveRecord object is returning an Ecto.Changeset, not a struct that’s only updated in-memory - then you can pass that directly to functions like Repo.update.

hst337

hst337

It is not possible because this will lead to inconsistencies.

Let’s check out all possible scenarios

  1. You update structure, use updated structure and then call changeset. With this approach, when you use the updated structure, you could’ve used incorrectly set values (because changeset check was not performed yet).
  2. You update structure, call changeset and then use updated changeset. With this approach you’d have to use get_field and family of functions to get the actual data from the structure. But this data may be incorrect too, since it is missing some autogenerated-in-the-database fields, database hooks and all this stuff

So I’d suggest using the approach dedicated by Ecto library: record is separate from it’s changes, changes can be verified and the version of the record with all the changes applied is visible only after interaction with the database (because DBs have hooks, autogenerated fields, etc)

benwilson512

benwilson512

Author of Craft GraphQL APIs in Elixir with Absinthe

I don’t really recommend what you’re doing, but the closest thing I can think of would be to use postgres INSERT ON CONFLICT

order_struct
|> Repo.insert(on_conflict: :replace_all, conflict_target: :id)

Where Next?

Popular in Questions Top

rms.mrcs
Hi, I need to transform a list of numbers into a map where the keys are the indexes and the values are the original values of the list. ...
New
lucidguppy
I have a super simple question about elixir - how would I take a file like this foo bar baz and output a new file that enumerates th...
New
Qqwy
Original source of discussion: This topic on the Pragmatic Programmers’ Functional Web Development with Elixir, OTP, and Phoenix forum. ...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
lanycrost
Hi everyone! I need implement if…else if…else condition from my elixir code, and anymore of this control flow structures not work proper...
New
lessless
I believe there are people here who are dealing with CSV files import on the daily basis, and since Excel is a really popular tool there ...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
earth10
Hi, I’m just starting to build a side-project with Elixir and Phoenix and doing some basic test with Elixir alone. What strikes me is th...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New

Other popular topics Top

ovidiubadita
Hey all, I discovered Elixir and I love it. I always wanted to learn a functional programming and I intended to go for Haskell, but afte...
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
Darmani72
If I have a post route which an argument: post /my_post_route/:my_param1, MyController.my_post_handler How would get the post params ...
New
dogweather
I wrote this comment on r/haskell, and it’s not popular there. :wink: But I think I’m on to something… Haskell reminds me of Java, and e...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 30877 112
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
sergio_101
I am VERY much an elixir newbie. I have taken one elixir course and one phoenix course on Udemy. During that course, I saw the instructor...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New

We're in Beta

About us Mission Statement