Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Insert Ecto model with already existing model as an association

I have 2 models, entries:

schema "entries" do
  belongs_to :exception, Proj.Exception
  field :application, :string
end

And exceptions:

schema "exceptions" do
  field :name, :string
end

The migration script:

def change do
  create table(:exceptions) do
    add :name, :string, null: false
  end

  create table(:entries) do
    add :exception_id, references(:exceptions), null: false
    add :application, :string, null: false
  end
end

My goal is to store exceptions that happen in another system. I want the project to be able to store each exception in the second table exception if they are not already there and then store the application name and the exception id in the first table entries. There will be 1000s of records in entries and a handful in exceptions.

Assuming entry_params uses this JSON format:

{
  exception: "NPE",
  application: "SomeApp"
}

the method that should create the entries:

def create(conn, %{"entry" => entry_params}) do
  exception = Repo.get_by(Exception, name: entry_params["exception"]) || 
    Repo.insert!(%Exception{name: entry_params["exception"]})

  changeset =
    Entry.changeset(%Entry{}, entry_params)
    |> Ecto.Changeset.put_assoc(:exception, exception)

  Repo.insert!(changeset)
end

This will print out:

** (ArgumentError) unknown assoc `exception` in `put_assoc`

If I change the entries model to use has_one instead of belongs_to (and I think belongs_to "feels" bad here. An entry does not belong to an exception, it just has an exception) it throws the following:

** (Postgrex.Error) ERROR (not_null_violation): null value in column "exception_id" violates not-null constraint

     table: entries
     column: exception_id

What I want basically to first create an Exception (if it does not exist) and than create a new Entry of a system error and put the previously begotten Exception in the entry as an association.

What is wrong here?

like image 569
SLOBY Avatar asked Mar 28 '16 02:03

SLOBY


1 Answers

  • Typo. belongs_to :exception, Proj.Exception should be belongs_to :exceptions, Proj.Exception
  • Association. Based on the data model in the question, I think the put_assoc is the wrong way around because in the data schema in the question, an exception has_many entries and an entry belongs_to exceptions. Ecto.Changeset.put_assoc(entries_changeset, :exception, exception) should be Ecto.Changeset.put_assoc(exception_changeset, :entries, entries)

Attempted solution:

entries schema:

schema "entries" do
  field :application, :string
  belongs_to :exceptions, Proj.Exception, on_replace: :nilify
end

exceptions schema:

schema "exceptions" do
  field :name, :string
  has_many :entry, Proj.Entry, on_delete: :delete_all, on_replace: :delete
end

migration script:

def change do
  create table(:exceptions) do
    add :name, :string, null: false
  end

  create table(:entries) do
    add :application, :string, null: false
    add :exception_id, references(:exceptions)
  end
end

Assuming entry_params uses this JSON format:

{
  exception: "NPE",
  application: "SomeApp"
}

create or update the exceptions and the associated entries:

def create(conn, %{"entry" => entry_params}) do
  new_entry = Entry.changeset(%Entry{}, entry_params)

  changeset = 
  case Repo.get_by(Exception, name: entry_params["exception"]) do
    :nil -> 
      exception = %Exception{name: entry_params["exception"]} |> Repo.insert!
      Ecto.Changeset.build_assoc(exception, :entries, [new_entry])
    struct -> 
      changeset = Ecto.Changeset.change(struct) 
      data = Ecto.Changeset.preload(changeset, :entries) |> Map.get(:model) # Ecto 1.x
      # data = Ecto.Changeset.preload(changeset, :entries) |> Map.get(:data) # Ecto 2.0.x
      Ecto.Changeset.put_assoc(changeset, :entries, [new_entry | data.entries])
  end 

  Repo.insert!(changeset)
end
like image 81
stephen_m Avatar answered Sep 29 '22 13:09

stephen_m