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?
Related
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 only one key in map, I would like to perform it by jsonb_set like here: stackoverflow example or in transaction to avoid potential conflicts in database, is it possible with Ecto?
defmodule MySuperApp.Profile do
use MySuperApp.Model
schema "profiles" do
field :name, :string
embeds_one :settigns, MySuperApp.Settigns
end
def changeset(struct, params) do
struct
|> change
|> put_embed(:settigns, MySuerApp.Settigns.changeset(model, params))
end
end
defmodule MySuperApp.Settigns do
use MySuperApp.Model
#settigns %{socket: true, page: true, android: false, ios: false}
embedded_schema do
field :follow, :boolean
field :action, :map, default: #settigns
end
def changeset(struct, _params) do
# I would like to update only web key and leave old keys
model |> change(action: %{web: false}) # this will override old map -> changes: %{action: %{web: false}
end
end
No. Ecto currently does not support partial updates of the embeds with the high-level API (like changesets).
You could achieve this by using raw SQL queries through Ecto.Adapters.SQL.query/4 or in more recent versions Repo.query/3.
I'm not clear on how to index embedded structs stored as JSONB with Ecto2/Postgres 9.4+
I have a schema with two embedded structs using embeds_one and embeds_many. They are ecto :map fields represented in Postgres as JSONB. I am wondering how I can be sure they are indexed (using Gin?) for speedy queries? I am not sure if this happens automagically, if I need to add an index to my migration or if I need to do it manually using psql etc..
Just looking for clarification on how this works.
Thanks!
defmodule App.Repo.Migrations.CreateClient
def change do
create table(:clients) do
add :name, :string
add :settings, :map
add :roles, {:array, :map}, default: []
timestamps()
end
// This works for normal schema/model fields
create index(:clients, [:name], unique: true, using: :gin)
// BUT CAN I INDEX MY EMBEDS HERE?
// GUESS:
create index(:clients, [:settings], using: :gin)
end
end
defmodule App.Client do
schema "client" do
field :name, :string
embeds_one :settings, Settings // single fixed schema "Settings" model
embeds_many :roles, Role // array of "Role" models
end
end
defmodule Settings do
use Ecto.Model
embedded_schema do // ALSO
field :name, :string // are types relevant?
field :x_count, :integer // stored as strings (INDEXED?)
field :is_active, :boolean // deserialized via cast?
end
end
defmodule Role do
use Ecto.Model
embedded_schema do
field :token
field :display_english
field :display_spanish
end
end
I think you just need to add this:
create index(:clients, [:name], unique: true, using: :gin)
to your migration file.
Or if the index sql statement is gonna be complicated, you could do it with execute so it would be something like this:
execute("CREATE INDEX clients_name_index ON clients USING GIN (name)")
I have not tested it but I believe it should work.
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.
I want to modify Devise to make it work with a users table with a UUID primary key with PostgreSQL.
Here is the migration:
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table :users, id: false do |t|
t.uuid :uuid, null: false
# ...
end
change_table :users do |t|
t.index :uuid, unique: true
# ...
end
end
def migrate(direction)
super
if direction == :up
# This is only necessary because the following does not work:
# t.uuid :uuid, primary: true, null: false
execute "ALTER TABLE users ADD PRIMARY KEY (uuid);"
end
end
end
Here is the User model:
class User < ActiveRecord::Base
primary_key = :uuid
devise :database_authenticatable, :recoverable, :registerable,
:rememberable, :trackable, :validatable
validates :uuid, presence: true
before_validation :ensure_uuid
def ensure_uuid; self.uuid ||= SecureRandom.uuid end
end
Here is the error:
PG::Error: ERROR: operator does not exist: uuid = integer
LINE 1: ...ECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "users".* FROM "users" WHERE "users"."uuid" = 1 ORDER BY "users"."uuid" ASC LIMIT 1
Extracted source (around line #5):
1 .navbar-inner
2 .container
3 = a_nav_tag "App", root_path
4 - if user_signed_in?
5 %ul.nav.pull-right
6 %li.dropdown#user_menu
7 %a.dropdown-toggle(data-toggle="dropdown" href="#")
As you can see above, user_signed_in? is broken. I expect there are several changes needed to move from a 'normal' auto-incrementing ID to a UUID.
For now, I'm just posting the question. I'll take a swing at this later today. If you happen to know how to do this -- or know of a Devise fork, I'd appreciate it.
I've done this in Rails 4 simply by making the id column a uuid data type when creating the table, and no other configuration changes whatsoever. ie. do not create a column named 'uuid', just change the type of the 'id' column to uuid.
Just clear your browser's cookie for the web app (in my case, localhost). The error above is caused because the session was retaining the old user primary key, 1.
After that, things work in my testing. I hope this isn't just luck, it would be a good design if Devise was agnostic about the primary key. (In Devise's code, I saw no use of .id except in some tests.)
2020 answer:
when creating the users table, set the ID as uuid
def change
enable_extension 'pgcrypto' # needed if not already enabled
create_table :users, id: :uuid do |t|
t.string :email,
...