1stSolo

1stSolo

Share `lockVersion` between parent and child?

I have a parent-table and a child-table; they are 1:1 - the “child” could have been a set of fields on the parent but it was better to decompose that into two separate tables to better handle some special cases (e.g. some “parents” have two children in the “child” tables).
I originally had lock_version on both parent and child tables, but can I instead have lock_version only on the parent?
The GraphQL API we have allows to update the child only through parent, e.g.
updateParent(parentId:<id>, data: %{child: %{description: "new value"}}, but in this case when the arguments are passed into Ecto changset it doesn’t actually increment lock_version on the parent because none of the parent’s fields changed. If I could somehow detect a change of the child and trigger 'lock_version increment on the parent then I would not need the child to have its own lock_version. Anyone done this before?
The only downside here is that if “child” is modified by any other means other than the above API endpoint, e.g. by batch or trigger then optimistic lock won’t fail because Ecto is not aware of the changes in the db until it fetches the data.
But the basic idea in the setup here is that parent and child only happen to be decomposed into 1:1 tables, but they are virtually one table and thus not necessarily need a lock_version (or own timestamps, for that matter) on the child.
Alternatively, one could always set child’s lock_version to the parent’s lock_version when creating the changset for the child?

Marked As Solved

1stSolo

1stSolo

I have a solution. Turns out, Changeset.cast_assoc is extremely useful when parent and child are updated together as a single unit as is my use-case. Even though the data is decomposed into two related tables, semantically the dependent/child is operated on as if it were a property of the parent module.
As the result:

  1. Don’t need to have lock_version on the child since parent’s lock_version is updated if cast_assoc detects a change in the child
  2. Requires to preload the assoc for cast_assoc to be able to detect changes
  3. For cast_assoc to behave properly and perform insert/update/delete based on the existing state of the dependent object, id of the child needs to be provided (it can be copied from Parent..id) or else Changeset will raise because the default for :on_replace of the association is :raise.

Example:

defmodule Project do
  use Ecto.Schema
  import Ecto.Changeset
  import Ecto.Query
  ...
  schema "projects" do
    field(:name, :string)
    field(:description, :string)
    field(:version, :string, default: "2.0")
    field(:owner_id, Ecto.UUID)

    has_one(:design, Design, foreign_key: :project_id)

    field :lock_version, :integer, default: 1
    timestamps(type: :utc_datetime)
  end

  def changeset(project, attrs \\ %{}) do
    supplied_design = attrs[:design]
    attrs =
      if not is_nil(supplied_design) and not is_nil(project.design.id) do
        Map.put(attrs, :design, Map.put(supplied_design, :id, project.design.id))
      else
        attrs
      end

    project
    |> cast(attrs, @cast_attrs)
    |> validate_required(@rqrd_attrs)
    |> check_constraint(:name, name: :name_length, message: "Project name \"#{attrs[:name]}\" is too long.")
    |> unique_constraint(:name, name: :project_name_unique, message: "Project name \"#{attrs[:name]}\" must be unique")
    |> validate_text_length(:description)
    |> cast_assoc(:design)
    |> optimistic_lock(:lock_version)
  end
  ...
  def update(%Project{} = project, attrs) do
    Repo.transaction(fn ->
      project
      |> Repo.preload(:design)
      |> changeset(attrs)
      |> Repo.update()
      |> case do
        {:ok, project} -> project
        {:error, error} -> Repo.rollback(error)
      end
    end)
  end

So Project.get(<uuid>) |> Project.update(%{design: %{diagram: "{\"some_json\": \"here\"}"}}) is then all is needed and the lock_version will get incremented with either Project or Design had changes. This allows to update both parent and child with a single optimistic lock field; timestamps, however, will be updated in both tables if child has changes.

Where Next?

Popular in Questions Top

Kurisu
For example for a current url like http://localhost:4000/cosmetic/products?_utf8=✓&amp;query=perfume&amp;page=2, I would like to get: ...
New
shahryarjb
Hello, I get Persian date from my client and convert it to normal calendar like this: def jalali_string_to_miladi_english_number(persi...
New
senggen
Erlang/OTP 25 [erts-13.2.2] [source] [64-bit] [smp:8:8] [ds:8:8:10] [async-threads:1] 15:22:35.803 [error] gen_event {lager_file_backend...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New
JeremM34
Hello, how can I check the Phoenix version ? Thanks !
New
Emily
I have VueJS GUIs with the project generated using Webpack. I have Elixir modules that will need to be used by the VueJS GUIs. I forese...
New
freewebwithme
Using vs code and installed ElixirLS: support and debugger. And I got an error popped up on start up says Failed to run ‘elixir’ comma...
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
vonH
In asking this question I am more interested about the expressiveness of the language itself and less concerned about the availability of...
New

Other popular topics Top

aadeshere1
I have a another noob question about loop. Since elixir is immutable, while loop is not directly possible. total = 10 while total != 0 ...
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
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
johnnyicon
Hi all, I’ve just started learning Elixir and Phoenix Framework, so please pardon my n00bness at this stage. I’m trying to use Postgres...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
New
jay1
Why is it that the mnesia database isn’t the most preferred database for use in Elixir/Phoenix?
New
nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New
svb
Hi! Currently I want to submit a form by pressing the Enter key. However, since my input field is of type “textarea” this is just adds a...
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

We're in Beta

About us Mission Statement