Multiple schemas in a liveview form

I have two schemas:

schema "templates" do
  field :name, :string
  field :description, :string

  [...]
schema "template_version" do
  field :version, :integer
  embeds_many :fields, Field, on_replace: :delete do
    field :type, :string
    field :label, :string
    field :required, :boolean
  end
  
  belongs_to :template ...

I’m trying to create a form that lets you create/edit a “template”. A new template would create both records and an edit would update the template record and create a new template version (those records are immutable).

I was thinking along the lines of having an embedded schema as follows

defmodule TemplateForm do
  embedded_schema do
    field :name, :string
    field :description, :string
    embeds_many :fields, Field .... etc
  end
end

What i’m not sure is how I can map between Template and TemplateVersion structs and TemplateForm structs/changesets etc. And if this is the right approach, is there a better way to achieve this? Or should I be just nesting the two schemas in an embedded schema instead of trying to compose the parts together?

Another side question is if it’s possible to detect if the fields have been changed before creating a new record (e.g if just the name is updated I don’t need a new version).

Thanks

Using an embedded schema or schemaless changeset is common for forms. The UI doesn’t always map 1:1 with the database representation.

You can write a function that passes the form struct to a function that checks for changes and maybe creates the new version. Ecto changesets have a changes field which you can use.

Are there any good examples?

I’m not sure how I should be turning a Template and TemplateVersion structs into a single TemplateForm struct. Is there a sensible way to cast values over or do I explicitly construct a new map with the attributes from each struct?

No need for a TemplateForm, a quick idea about how I would do it.

Add to the schemas current_version field, add a has_many :versions TemplateVersion and also add a :template_version that is virtual.

Now build two changesets, one for form validation (will use the :template_version) and other for the repo operations.

Now it is where is a bit more complex because you will have to manipulate the params received from the form (the virtual field) and pass it to the repo operations correctly, you can use a cast_assoc on the changeset or a Ecto.Multi to insert/update whatever you want.

I have an example but don’t have time right now, if I have time and you didn’t manage to figure it out I can try to help you.

I don’t completely follow, a concrete example would be very helpful when you have time.

You can explicitly create the struct, use struct! or use the changeset functions (cast*, apply* etc).

Sure, a bit long but here we go.

defmodule Example.Templates.Template do
  use Ecto.Schema
  import Ecto.Changeset
  schema "templates" do
    field :name, :string
    field :description, :string
    has_many :versions, Example.Templates.TemplateVersion
    timestamps(type: :utc_datetime)
  end
  def changeset(template, attrs) do
    template
    |> cast(attrs, [:name, :description])
    |> validate_required([:name, :description])
  end
end

defmodule Example.Templates.TemplateVersion do
  use Ecto.Schema
  import Ecto.Changeset
  schema "template_version" do
    field :version, :integer
    belongs_to :template, Example.Templates.Template
    timestamps(type: :utc_datetime)
  end
  def changeset(template, attrs) do
    template
    |> cast(attrs, [:version])
    |> validate_required([:version])
  end
end

defmodule Example.Templates do
  @moduledoc """
  The Templates context.
  """

  import Ecto.Query, warn: false
  alias Example.Repo

  alias Example.Templates.Template
  alias Example.Templates.TemplateVersion

 # preload because we're going to need it to update the version
  def get_template!(id) do
    version_query =
      from tv in TemplateVersion,
        order_by: [desc: tv.version],
        limit: 1

    q =
      from t in Template,
        where: t.id == ^id,
        preload: [versions: ^version_query]

    Repo.one!(q)
  end

  def create_template(attrs \\ %{}) do
    Ecto.Multi.new()
    |> Ecto.Multi.insert(:template, Template.changeset(%Template{}, attrs))
    |> Ecto.Multi.insert(
      :version,
      fn %{template: %Template{id: template_id}} ->
        %TemplateVersion{}
        |> TemplateVersion.changeset(%{template_id: template_id, version: 0})
      end
    )
    |> Repo.transaction()
  end

  def update_template(%Template{} = template, attrs) do
    Ecto.Multi.new()
    |> Ecto.Multi.update(:template, Template.changeset(templateattrs))
    |> Ecto.Multi.insert(
      :version,
      fn %{template: %Template{id: template_id, versions: versions}} ->
        version =
          case List.first(versions, 0) do
            0 -> 0
            v -> v.version + 1
          end

        %TemplateVersion{}
        |> TemplateVersion.changeset(%{template_id: template_id, version: version})
      end
    )
    |> Repo.transaction()
  end
end

Zero changes needed in your forms, every time you create a new Template it will get a template version and every time you update it will get a new one with a bigger version number.

Probably, but you can figure that out :slight_smile:

Another side question is if it’s possible to detect if the fields have been changed before creating a new record (e.g if just the name is updated I don’t need a new version).

https://hexdocs.pm/ecto/Ecto.Changeset.html#get_change/3