Repo.update/3 touched `updated_at` even when changeset has no change

255 views
Skip to first unread message

Son Tran-Nguyen

unread,
Dec 26, 2015, 8:30:31 PM12/26/15
to elixir-ecto
I asked in #elixir-lang but got no answer, so I post it here.

I have a classic relation models: Product, Order, LineItem. LineItem belongs to a Product and an Order, etc...

defmodule MyApp.LineItem do
   use MyApp.Web, :model

  schema "line_items" do
    field :variant, :string
    field :quantity, :integer
    field :price, :float
    field :options, :string

    belongs_to :product, Product
    belongs_to :order, Order

    timestamps
    # When the item is purchased from store.
    field :paid_at, Ecto.DateTime
    # When the item is shipped from store.
    field :shipped_at, Ecto.DateTime
    # When the item is delivered to origin endpoint.
    field :delivered_at, Ecto.DateTime
  end

  @required_fields ~w(product_id variant quantity price)
  @optional_fields ~w(options paid_at shipped_at delivered_at)

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_inclusion(:quantity, 1..10)
  end
end

defmodule MyApp.Order do
  use MyApp.Web, :model
  alias MyApp.Order

  schema "orders" do
    # Billing info, including name, phone, email, physical address.
    field :billing_address, :string
    # Shipping info, including name, phone, email, physical address.
    field :shipping_address, :string
    # Note from customer
    field :notes, :string
    # When customer pays for the order.
    field :paid_at, Ecto.DateTime
    # When the order is sent by courier.
    field :sent_at, Ecto.DateTime
    # When the order arrives at destination endpoint.
    field :arrived_at, Ecto.DateTime
    # When the order is shipped to customer.
    field :shipped_at, Ecto.DateTime
    # When the customer receives the order.
    field :delivered_at, Ecto.DateTime

    # Generate inserted_at and updated_at.
    # These fields can determined the intial status of the order.
    # If inserted_at equals updated_at, the order is in cart.
    # Otherwise, the order is placed.
    timestamps

    has_many :items, MyApp.LineItem, on_delete: :delete_all
  end

  @required_fields ~w(billing_address shipping_address)
  @optional_fields ~w(notes paid_at sent_at arrived_at shipped_at delivered_at)

  @doc """
  Creates a changeset based on the `model` and `params`.

  If no params are provided, an invalid changeset is returned
  with no validation performed.
  """
  def changeset(model, params \\ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    # Ensure there are items in order. Delete old line items on replacing.
    |> cast_assoc(:items, required: true, on_replace: :delete)
  end

  def all() do
    query = from order in Order,
        join: item in assoc(order, :items),
        join: product in assoc(item, :product),
        where: (order.updated_at != order.inserted_at) or not is_nil(item.id),
        preload: [items: {item, product: product}]
    Repo.all query
  end

  def get!(id) do
    Repo.get!(Order, id) |> Repo.preload([items: [:product]])
  end
end



On a `PATCH` route to update an Order, I only pass `items` in the params, with no other Order-related properties.

  def update(conn, %{"id" => id, "order" => order_params}) do
    order = Order.get!(id)
    changeset = Order.changeset(order, order_params)
    case Repo.update(changeset) do
      {:ok, order} ->
        render(conn, "show.json", order: order)
      {:error, changeset} ->
        conn
        |> put_status(:unprocessable_entity)
        |> render(MyApp.ChangesetView, "error.json", changeset: changeset)
    end
  end


The items in the order are updated correctly, but since this update operation on the order is basically a no-op, why `updated_at` is touched?

Also, as a side question: my updated order does not load the `:product` association from inserted items. Is there a way to do that?

José Valim

unread,
Dec 27, 2015, 4:20:05 AM12/27/15
to elixi...@googlegroups.com
The items in the order are updated correctly, but since this update operation on the order is basically a no-op, why `updated_at` is touched?

Yes, this is a bug. It is fixed in master by accident, so I will make sure to write a test. :)
 
Also, as a side question: my updated order does not load the `:product` association from inserted items. Is there a way to do that?

I am not sure I understood the question but is the answer possibly Repo.preload(order, :product)? 

Thank you for the report :)

Son Tran-Nguyen

unread,
Dec 27, 2015, 10:41:16 AM12/27/15
to elixir-ecto, jose....@plataformatec.com.br


On Sunday, 27 December 2015 03:20:05 UTC-6, José Valim wrote:
Also, as a side question: my updated order does not load the `:product` association from inserted items. Is there a way to do that?

I am not sure I understood the question but is the answer possibly Repo.preload(order, :product)? 

The `:product` association is from individual line item, not directly from order.

Usually when I `get` an order, I would do: `Repo.get!(Order, id) |> Repo.preload([items: [:product]])`

But newly inserted item from an order's changeset won't have `:product` association loaded, due to the item being already loaded. 

Son Tran-Nguyen

unread,
Dec 27, 2015, 11:10:15 AM12/27/15
to elixir-ecto, jose....@plataformatec.com.br

Junaid Farooq

unread,
Aug 9, 2017, 5:45:27 AM8/9/17
to elixir-ecto, jose....@plataformatec.com.br
Hi there, Just as a question, Is there any possibility that we have some monitoring on that if something changed or not.. 

for example: as mentioned above if there will be something new then the updated_at will be touched other wise its not, but do we have something else to check this? 

as {:ok, user} = Repo.update(user_changeset)..

it will just return user and I can check from the updated at whether there is some change or not BUT, I think its not a good way to monitor the change, do we have some other options to that? to see if the Repo.update really changed something or not? 

Ben Wilson

unread,
Aug 9, 2017, 5:20:59 PM8/9/17
to elixir-ecto, jose....@plataformatec.com.br
Hey Junaid. You may want to make a new post in the future because this really is a different question from the original poster.

There is no built in way to subscribe to changes with Ecto. The underlying database may offer some way of tracking changes live, and there are various solutions for doing so at the API level, but there isn't any particular ecto feature for this, nor do I see how there could easily be one.
Reply all
Reply to author
Forward
0 new messages