PJUllrich
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
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
It is not possible because this will lead to inconsistencies.
Let’s check out all possible scenarios
- 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).
- You update structure, call changeset and then use updated changeset. With this approach you’d have to use
get_fieldand 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
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)
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #javascript
- #code-sync
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #advent-of-code
- #elixirconf-us
- #distillery
- #processes
- #forms
- #api
- #metaprogramming
- #security
- #performance








