Search code examples
postgresqlelixirphoenix-frameworkecto

Ecto, Phoenix: How to update a model with an embeds_many declaration?


I have two models, Ownerand Property, where the schema for Ownerhas an embeds_many declaration, like this:

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

  alias MyApp.Property

  schema "owners" do
    field name, :string
    embeds_many :properties, Property
    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [])
    |> validate_required([])
  end
end

and this:

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

  embedded_schema do
    field :name, :string
    field :value, :float, default: 0
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :value])
    |> validate_required([:name])
  end
end

The migration I'm using is:

defmodule MyApp.Repo.Migrations.CreateOwner do
  use Ecto.Migration

  def down do
    drop table(:owners)
  end

  def change do
    drop_if_exists table(:owners)
    create table(:owners) do
      add :name, :string
      add :properties, :map
      timestamps()
    end
  end
end

And a possible seed is:

alias MyApp.{Repo, Owner, Property}

Repo.insert!(%Owner{
  name: "John Doe",
  properties: [
    %Property{
      name: "Property A"
    },
    %Property{
      name: "Property B",
      value: 100000
    },
    %Property{
      name: "Property C",
      value: 200000
    }
  ]
})

Finally, my questions are: how can I update John Doe's Property C's value from 200000to 300000? And if John Doe buys a Property D:

%Property{
  name: "Property D"
  value: 400000
}

How do I add that to his properties in the database? (I'm using Postgres).


Solution

  • The simplest way would be to fetch the record, update the properties list and save the changes:

    owner = Repo.get!(Owner, 1)
    properties = owner.properties
    
    # update all properties with name "Property C"'s value to 400000
    properties = for %Property{name: name} = property <- properties do
      if name == "Property C" do
        %{property | value: 400000}
      else
        property
      end
    end
    
    # add a property to the start
    properties = [%Property{name: "Property D", value: 400000} | properties]
    
    # or to the end
    # properties = properties ++ [%Property{name: "Property D", value: 400000}]
    
    # update
    Owner.changeset(owner, %{properties: properties})
    |> Repo.update!
    

    You can do some operations (at least inserting a property) using the JSON functions provided by PostgreSQL using fragment but I don't think you can search and conditionally update an item of an array using them.