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

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).

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.

Related

Self referencing association in Phoenix using Ecto

So I've just started fiddeling around with Phoenix, and Elixir. So I have reached the point were I am trying to get a working rest-api endpoint to work with a prerequisite JSON.
So I have this module:
defmodule MyApp.Housing.Part do
use Ecto.Schema
import Ecto.Changeset
#primary_key {:id, :integer, []}
schema "parts" do
field :level, :integer
field :title, :string
belongs_to :parent, MyApp.Housing.Part
has_many :children, MyApp.Housing.Part, foreign_key: :parent_id
timestamps()
end
def changeset(part, params \\ %{}) do
part
|> cast(params, [:title, :level, :id, :parent_id])
|> put_assoc(:children, required: false)
|> put_assoc(:parent, required: false)
|> validate_required([:title, :level, :id])
end
end
And the module in which the table is created
defmodule MyApp.Repo.Migrations.CreateParts do
use Ecto.Migration
def change do
create table(:parts, primary_key: false) do
add :id, :integer, primary_key: true
add :title, :string
add :level, :integer
add :parent_id, references(:parts)
add :children, references(:parts)
timestamps()
end
create index(:parts, [:children])
create index(:parts, [:parent_id])
end
end
The inteded functionallity is for a part to be able to have multiple children but only one parent. And these are defined in a JSON like this:
{"id": 10,
"title": "Matt",
"level": 0,
"children": [],
"parent_id": null}
So my problem is the following:
"changeset" requires that the incoming object looks like {"part":{}}otherwise the ActionClauseError gets thrown.
When defining the object as above I get the error children":["is invalid"]. And I can't figure out how to get a valid one, if I did I probably could figure out the problem.
I might take the wrong approach here but would gladly accept any help.
As mentioned by #steve-pallen, it's not necessary to store any references to children in the database. Determining whether or not a Part is a parent or child, as well as which Parts are its children or which Part is its parent can be determined fully by the parent_id field.
You described in your question that each Part "can only have one parent, but multiple children". It's not explicit in your question how many levels the relationship allows: i.e. can a Part be both a parent and a child? In which case, there would be potentially infinite levels of nesting:
part1
|- part2
|- part3
|- part4
In this case, part1 is the parent of part2, part2 is itself the parent of part3, etc. I'm going to assume for my answer that there is no limit to the amount of nesting.
Given this case, your schema definition is 100% correct:
belongs_to :parent, MyApp.Housing.Part
has_many :children, MyApp.Housing.Part, foreign_key: :parent_id
I think the primary issue is with your changeset function. Remember that with put_assoc/3, it's expected that all the models referenced by parent and children already exist in the DB (see the docs for cast_assoc/3). For simplicity I suggest that you don't use put_assoc or cast_assoc and instead manage each model in isolation. If you change your changeset function to this (I've removed id since it's not necessary):
def changeset(part, params \\ %{}) do
part
|> cast(params, [:title, :level, :parent_id])
|> validate_required([:title, :level])
end
Then you can build the nested relationship I showed above by doing 4 inserts in isolation (much easier to reason about, and probably more in line with how you'd handle DB updates from a form or script):
part1 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part1", level: 0, parent_id: nil})
|> Repo.insert!()
part2 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part2", level: 0, parent_id: part1.id})
|> Repo.insert!()
part3 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part3", level: 0, parent_id: part2.id})
|> Repo.insert!()
part4 =
MyApp.Housing.Part.changeset(%MyApp.Housing.Part{}, %{title: "part4", level: 0, parent_id: part3.id})
|> Repo.insert!()
Assuming we want to get part2, we can load it along with its parent and children like this:
part2 = Repo.preload(part2, [:parent, :children])
# part2.parent == %MyApp.Housing.Part{title: "part1", ...}
# part2.children == [%MyApp.Housing.Part{title: "part3", ...}]
Hope this helps!
The first thing you need to fix is the migration. You don't want the children field since that is a has_many relationship and is handled by the parent_id field in the children. It should look like this:
defmodule MyApp.Repo.Migrations.CreateParts do
use Ecto.Migration
def change do
create table(:parts, primary_key: false) do
add :id, :integer, primary_key: true
add :title, :string
add :level, :integer
add :parent_id, references(:parts)
timestamps()
end
create index(:parts, [:parent_id])
end
end
Handling the children in the changeset depends on a couple things.
What does the incoming payload look like when there are children?
Will there be new children in the children list or just existing children?

Ecto insert_or_update creating multiple inserts?

My code is resulting in a rare double or triple insert into the database and I am at a loss as to why. It is very difficult to reproduce but I can look at the timestamps to see the created at time is basically the same when it happens. I believe it only occurs when the CardMeta is not already found.
I figure I need to add a unique key or wrap it in a transaction.
def get_or_create_meta(user, card) do
case Repo.all(from c in CardMeta, where: c.user_id == ^user.id,
where: c.card_id == ^card.id) do
[] ->
%CardMeta{}
metas ->
hd metas
end
end
def bury(user, card) do
get_or_create_meta(user, card)
|> Repo.preload([:card, :user])
|> CardMeta.changeset(%{last_seen: DateTime.utc_now(), user_id: user.id, card_id: card.id,
learning: false, known: false, prev_interval: 0})
|> Repo.insert_or_update
end
Edit: adding changeset source
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:last_seen, :difficulty, :prev_interval, :due, :known, :learning,
:user_id, :card_id])
|> assoc_constraint(:user)
|> assoc_constraint(:card)
end
Calling bury from the controller
def update(conn, %{"currentCardId" => card_id, "command" => command}) do
# perform some update on card
card = Repo.get!(Card,card_id)
user = Guardian.Plug.current_resource(conn)
case command do
"fail" ->
SpacedRepetition.fail(user, card)
"learn" ->
SpacedRepetition.learn(user, card)
_ ->
SpacedRepetition.bury(user, card)
end
sendNextCard(conn, user)
end
Edit:
I noticed the last_seen field is microseconds different between duplicated rows, whereas the create_at field does not have that resolution. Thus I suspect the insert_or_update call is fine, but the controller is firing twice before the DB updates. This could be something on the client side, which I don't want to think about. So I am just going to add a unique key.
As an alternative to #aliCna's answer, if you don't want to change the primary key on CardMeta, you can put a unique index constraint in the database with a migration:
defmodule YourApp.Repo.Migrations.AddCardMetaUniqueIndex do
use Ecto.Migration
def change do
create unique_index(
:card_meta,
[:card_id, :user_id],
name: :card_meta_unique_index)
end
end
Which you can then handle in your changeset to produce nice errors if conflicts occur:
def changeset(struct, params \\ %{}) do
struct
|> cast(params, [:last_seen, :difficulty, :prev_interval, :due, :known, :learning,
:user_id, :card_id])
|> assoc_constraint(:user)
|> assoc_constraint(:card)
|> unique_constraint(:user_id, name: :card_meta_unique_index)
end
I believe you can solve this by adding a composite primary key on user_id and card_id
defmodule Anything.CardMeta do
use Anything.Web, :model
#primary_key false
schema "card_meta" do
field :user_id, :integer, primary_key: true
field :card_id, :integer, primary_key: true
. . .
timestamps()
end
end
If this does't solve your problem please add your data model here!

How to update one sub-document in an embedded list with Ecto?

I have a document with an embedded list of sub-docs. How do I update/change one particular document in the embedded list with Ecto?
defmodule MyApp.Thing do
use MyApp.Model
schema "things" do
embeds_many :users, User
end
end
defmodule MyApp.User do
use MyApp.Model
embedded_schema do
field :name, :string
field :email, :string
field :admin, :boolean, default: false
end
end
defmodule MyApp.Model do
defmacro __using__(_) do
quote do
use MyApp.Web, :model
#primary_key {:id, :binary_id, autogenerate: true}
#foreign_key_type :binary_id # For associations
end
end
end
My solution so far is to generate a list of all users except the one I want to update and make a new list of the one user's changeset and the other users and then put_embed this list on the thing. It works but it feels like there must be a more elegant solution to this.
user = Enum.find(thing.users, fn user -> user.id == user_id end)
other_users = Enum.filter(thing.users, fn user -> user.id != user_id end)
user_cs = User.changeset(user, %{email: email})
users = [user_cs | other_users]
thing
|> Ecto.Changeset.change
|> Ecto.Changeset.put_embed(:users, users)
|> Repo.update
EDIT: I just discovered a serious pitfall with this "solution". The untouched users get updated as well which can be a problem with concurring calls (race condition). So there has to be another solution.

How do I create optional references/embeds in Mongoid?

We have an Item database in Mongoid where we store contextual, optional extra fields for different items, eg:
class Item
include Mongoid::Document
field :name, type: String
end
so I can do something like:
a = Item.new
a.name = "Potato Chips"
a[:flavor] = "Barbecue"
a.save
b = Item.new
b.name = "Underwear"
b[:size] = "XL"
b.save
> Item.first.flavor
=> "Barbecue"
> Item.last.size
=> "XL"
However, say we wanted to do:
class Flavor
include Mongoid::Document
field :name, type: String
field :ingredients, type: Array
end
If you did:
f = Flavor.create({name: "Barbecue", ingredients: ["salt", "sugar"]})
a[:flavor] = f
a.save
You get:
NoMethodError: undefined method `__bson_dump__' for #<Flavor:0x007fb34d3c1718>
How do I make it so I can go:
Item.first.flavor.ingredients[0]

'validates_uniqueness_of' - how to get error message

I have model:
# encoding: utf-8
class Tag
include Mongoid::Document
field :name, type: String
field :count, type: Integer
index :name, unique: true
validates_uniqueness_of :name
def self.create_tag(name)
tag = Tag.new
tag.name = name
tag.count = 0
tag.save
end
def self.find_by_name(name)
Tag.where(name: name).entries
end
end
And I have test for the model:
describe Tag, "# simple database operations" do
it " - insert test records" do
Tag.create_tag("joe")
Tag.create_tag("joe")
p Tag.find_by_name("joe")
end
end
If I look at collection after test execution I'll find only one record, but I want to catch exception in the case of duplicate record insertion.
Is it possible?
By default Mongoid writes in "fire and forget" mode. It sends a write and returns immediately. To check for error, you should write in "safe mode". Try this.
def self.create_tag(name)
tag = Tag.new
tag.name = name
tag.count = 0
tag.safely.save! # <= note the 'safely' here. Also bang version of save is used.
end
Or, better yet,
def self.create_tag(name)
Tag.safely.create!(name: name, count: 0)
end
See the doc here.