Ecto association to more than one schemes - associations

Let's say I have these schemas:
defmodule Sample.Post do
use Ecto.Schema
schema "post" do
field :title
has_many :comments, Sample.Comment
end
end
defmodule Sample.User do
use Ecto.Schema
schema "user" do
field :name
has_many :comments, Sample.Comment
end
end
defmodule Sample.Comment do
use Ecto.Schema
schema "comment" do
field :text
belongs_to :post, Sample.Post
belongs_to :user, Sample.User
end
end
My questions is how can I use Ecto.build_assoc to save a comment?
iex> post = Repo.get(Post, 13)
%Post{id: 13, title: "Foo"}
iex> comment = Ecto.build_assoc(post, :comments)
%Comment{id: nil, post_id: 13, user_id: nil}
So far it's ok, all I need to do is use the same function to set the user_id in my Comment struct, however since the return value of build_assoc is Comment struct, I can not use the same function
iex> user = Repo.get(User, 1)
%User{id: 1, name: "Bar"}
iex> Ecto.build_assoc(user, :comment, comment)
** (UndefinedFunctionError) undefined function: Sample.Comment.delete/2
...
I have two options but neither of them looks good to me:
First one is to set user_id manually!
iex> comment = %{comment| user_id: user.id}
%Comment{id: nil, post_id: 13, user_id: 1}
Second one is to convert the struct to map and ... I don't even want to go there
Any suggestion?

Why don't you want convert struct to map? It is really easy.
build_assoc expects map of attributes as last value. Internally it tries to delete key :__meta__. Structs have compile time guarantees, that they will contain all defined fields, so you are getting:
** (UndefinedFunctionError) undefined function: Sample.Comment.delete/2
But you can just write:
comment = Ecto.build_assoc(user, :comment, Map.from_struct comment)
and everything will work fine.

Just pass it along with build_assoc
iex> comment = Ecto.build_assoc(post, :comments, user_id: 1)
%Comment{id: nil, post_id: 13, user_id: 1}
Check here for more details.

Related

How to check if the key exists in a deeply nested Map in Elixir

I have a map object I need to check if it contain a given key. I tried like below but it always return false. Also how to pull value on inside map replyMessage,
map=%{
Envelope: %{
Body: %{
replyMessage: %{
cc: %{
"amount" => "100.00",
"reasonCode" => "100",
},
decision: "ACCEPT",
invalidField: nil,
purchaseTotals: %{"currency" => "CAD"},
reasonCode: "100",
}
}
}
}
Map.has_key?(map, %{Envelope: %{Body: %{replyMessage: replyMessage}}})= false
You have two possibilities: Kernel.match?/2 to check whether the key exists and/or Kernel.get_in/2 to deeply retrieve the value.
iex|1 ▶ match?(%{Envelope: %{Body: %{replyMessage: _}}}, map)
#⇒ true
iex|2 ▶ get_in(map, [:Envelope, :Body, :replyMessage])
#⇒ %{
# cc: %{"amount" => "100.00", "reasonCode" => "100"},
# decision: "ACCEPT",
# invalidField: nil,
# purchaseTotals: %{"currency" => "CAD"},
# reasonCode: "100"
# }
Beware of Kernel.get_in/2 would return nil if the key exists, but has nil value, as well as if the key does not exist.
Map.has_key?/2 is not recursive by any mean, it works with the keys of the map, passed as an argument; that said, the first level only.
Sidenote: one might easily build a recursive solution themselves, based on Map.has_key/2
defmodule Deep do
#spec has_key?(map :: map(), keys :: [atom()]) :: boolean()
def has_key?(map, _) when not is_map(map), do: false
def has_key?(map, [key]), do: Map.has_key?(map, key)
def has_key?(map, [key|tail]),
do: Map.has_key?(map, key) and has_key?(map[key], tail)
end
Deep.has_key?(map, [:Envelope, :Body, :replyMessage])
#⇒ true

Why is DynamoDB adding a NULL suffix to attribute names?

I've been working on a toy project and noticed that dynamo added a (NULL) suffix in two of my attribute names. I had not noticed before, so I assume it must have happened after one of my code changes. I could not find any reference to this behavior online.
The script I'm running is a simple PutItem got from the official Dynamodb documentation, where I insert a few mock users in a table.
func InsertModel(m interface{}) error {
av, err := dynamodbattribute.MarshalMap(m)
if err != nil {
return fmt.Errorf("handlers: Got error marshalling map: %v", err)
}
input := &dynamodb.PutItemInput{
Item: av,
TableName: aws.String(appTableName),
ConditionExpression: aws.String("attribute_not_exists(PK) AND attribute_not_exists(SK)"),
}
_, err = svc.PutItem(input)
if err != nil {
return fmt.Errorf("handlers: Got error calling PutItem: %v", err)
}
return nil
}
m (user mock data) has all fields type string:
UserModel{PK: "910cc6d8-b7e2-dfg6-d8d4-sh6d0e3fde6b", SK: "user_info", Name: "bla", ImageURI: "aaa"},
When I remove the fields "Name" and "ImageURI", the PutItem inserts a boolean true to the field value as seen below.
Here is the value in av after the MarshalMap operation.
with populated "Name" and "ImageURI" fields:
map[ImageURI:{
S: "aaa"
} Name:{
S: "bla"
} PK:{
S: "910cc6d8-b7e2-dfg6-d8d4-sh6d0e3fde6b"
} SK:{
S: "user_info"
}]
and here without "Name" and "ImageURI" as in UserModel{PK: "910cc6d8-b7e2-dfg6-d8d4-sh6d0e3fde6b", SK: "user_info"}
map[ImageURI:{
NULL: true
} Name:{
NULL: true
} PK:{
S: "910cc6d8-b7e2-dfg6-d8d4-sh6d0e3fde6b"
} SK:{
S: "user_info"
}]
I have tried to delete all the records from the table and insert again but the behavior continues. Also, I did the same process for an int type attribute (inserting the object with the int attribute populated and not populated) and I get 0 when it's not populated (which is what I'd expect). I tried replicating this with a different string type attribute, and I get the same behavior (true when empty), but the attribute name doesn't get the suffix NULL.
So in summary, it seems this behavior is mostly happening with type string in my case, and I only get a NULL suffix in the attributes "Name" and "ImageURI", but not on the other string attribute I've tried (nor the int one).
I had the same issue for one of the fields of my table items.
For that field I was doing an update using the NameMap option, useful when you want to use a name that, for some other reasons, is reserved by dynamo.
I just tried not to use the NameMap option, giving another name for my field, and that suffix disappeared.
I hope my experience could be somehow helpful.
Regards
Option A
The dynamodbattribute.MarshalMap method reads json struct tags if present. By adding omitempty it will leave the attribute off of the item. Then when reading it back later, it will default to the empty value in the struct.
type Foo struct {
Bar string `json:"bar,omitempty"`
}
Option B
You can explicitly set empty struct values by either creating your own dynamodb.AttributeValue or implement the marshaller interface on your struct. For example:
item := map[string]*dynamodb.AttributeValue{
"Foo": {
S: aws.String("")
}
}
Making the (NULL) suffix to go away
After deleting all the rows containing a NULL value in a column with the (NULL) suffix, it seems to take some time for the suffix to go away from the AWS UI. When I tested this, it took roughly 12 hours.
Discussion
Continue the discussion of null behaviors on github.
https://github.com/aws/aws-sdk-go/pull/2419
https://github.com/aws/aws-sdk-go/issues?q=MarshalMap+null

How do i do an type OR Type. String OR Int?

I would like to be able to allow a string or an integer in a field. How do I do this?
This is my current schema:
'minSize': {'type': 'any'},
I'm quoting the docs:
A list of types can be used to allow different values
>>> v.schema = {'quotes': {'type': ['string', 'list']}}
>>> v.validate({'quotes': 'Hello world!'})
True
>>> v.validate({'quotes': ['Do not disturb my circles!', 'Heureka!']})
True

Recursively convert Map to Keyword List

I found a function to turn maps into keyword lists on the internet, but it is not recursive:
def to_keyword_list(dict) do
Enum.map(dict, fn({key, value}) -> {String.to_atom(key), value} end)
end
I then made this one, but it gives me an error.
def tokey(dict) do
Enum.map(dict, fn({key, value}) ->
if is_map value do
{String.to_atom(key), tokey(value)}
else
{String.to_atom(key), value}
end
end)
end
Result of the first one:
["calig├╝eva": %{speeking: "speeeeeeee"}, test: "teet", tututu: "tururuuu"]
Result of the second one:
** (ArgumentError) argument error
:erlang.binary_to_atom(:speeking, :utf8)
code.exs:10: anonymous fn/1 in Util.tokey/1
(elixir) lib/enum.ex:1233: anonymous fn/3 in Enum.map/2
(stdlib) lists.erl:1263: :lists.foldl/3
(elixir) lib/enum.ex:1772: Enum.map/2
code.exs:8: anonymous fn/1 in Util.tokey/1
(elixir) lib/enum.ex:1233: anonymous fn/3 in Enum.map/2
(stdlib) lists.erl:1263: :lists.foldl/3
Is there an easier or more effective way of doing this? And why does it show that error? Can I not call a function from within itself?
Result of first one changing ü for u:
[caligueva: %{speeking: "speeeeeeee"}, test: "teet", tututu: "tururuuu"]
Second one outputs the same error. This is the map I'm using:
map = %{
"test" => "teet",
"tututu" => "tururuuu",
"caligueva" => %{"speeking": "speeeeeeee"}
}
For recursively converting Maps to Keyword Lists in Elixir:
defmodule MyMap do
def to_keyword_list(map) do
Enum.map(map, fn {k,v} ->
v = cond do
is_map(v) -> to_keyword_list(v)
true -> v
end
{String.to_atom("#{k}"), v}
end)
end
end
But as #Dogbert already mentioned, "Pure" Atoms cannot contain codepoints above 255, so your map keys should be simple Strings / Atoms:
iex(1)> MyMap.to_keyword_list(%{"caligueva" => %{speeking: "speeeeeeee"}, "test" => "teet", "tututu" => "tururuuu"})
[caligueva: [speeking: "speeeeeeee"], test: "teet", tututu: "tururuuu"]
Here is the refactored code for conversion:
def map_to_keyword_list(map), do: convert(map)
defp convert(map) when is_map(map), do: Enum.map(map, fn {k,v} ->{String.to_atom(k),convert(v)} end)
defp convert(v), do: v

Sinatra + SQLite + ActiveRecord (strings can not be saved)

I have a problem with saving strings in database. Integers are going well. I have a model Paymentwith one field :command(String). This is from console:
p = Payment.new
p.command = 'test'
=> "test"
p.save
=> D, [2014-10-20T15:30:43.383504 #2229] DEBUG -- : (0.2ms) begin transaction
=> D, [2014-10-20T15:30:43.386985 #2229] DEBUG -- : (0.1ms) commit transaction
=> true
Payment.last
=> #<Payment id: 1, command: nil>
My app file
require 'sinatra'
require 'activerecord'
require 'sinatra/activerecord'
set :database, 'sqlite3:qiwi_exline_development.sqlite3'
get '/' do
#payments = Payment.all
haml :index
end
class Payment < ActiveRecord::Base
include ActiveModel::Model
attr_accessor :command
end
My schema
ActiveRecord::Schema.define(version: 20141020092721) do
create_table "payments", force: true do |t|
t.string "command"
end
end
Interesting moment: If i change string to integer in schema -> integer values are saved without any problems.
Remove attr_accessor from model. In the case of ActiveRecord models, getters and setters are already generated by ActiveRecord for your data columns. attr_accessor is not needed. attr_accessor — is a deprecated method of controlling mass assignment within ActiveRecord models.

Resources