AE
Ash Elixir•3y ago
dj_goku

Postgres Table(s)

I have a user resource and I need to assign a user to one_on_one resource. The one_on_one resource has two arguments user_1 and user_2. I am at a loss on how I should define the relationship. I know how I want the table design to be: Where user_id_1 can't be the same as user_id_2. Anyone else struggle with this part of Ecto?
user:
- id
- name

one_on_one:
- user_id_1
- user_id_2
user:
- id
- name

one_on_one:
- user_id_1
- user_id_2
20 Replies
ZachDaniel
ZachDaniel•3y ago
So when you say one_on_one you mean like a join resource that a many_to_many might have, but there is only one for any given user? Or is there one for any given user combination?
dj_goku
dj_gokuOP•3y ago
one_on_one_meeting is likely a better name. many_to_many is what I am thinking.
defmodule Mod.OneOnOne do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

postgres do
table("one_on_one")
repo(Mod.Repo)
end

actions do
defaults([:create, :read, :update, :destroy])

update :assign do
accept([])

argument :user_1_id, :uuid do
allow_nil?(false)
end

argument :user_2_id, :uuid do
allow_nil?(false)
end

# change(manage_relationship(:user_1_id, :user, type: :append_and_remove))
# change(manage_relationship(:user_2_id, :user, type: :append_and_remove))
end
end

attributes do
uuid_primary_key(:id)

attribute(:meeting_description, :string) do
allow_nil?(false)
end

attribute(:meeting_title, :string) do
allow_nil?(false)
end

attribute(:user_1_id, :uuid)
attribute(:user_2_id, :uuid)
end
end
defmodule Mod.OneOnOne do
use Ash.Resource,
data_layer: AshPostgres.DataLayer

postgres do
table("one_on_one")
repo(Mod.Repo)
end

actions do
defaults([:create, :read, :update, :destroy])

update :assign do
accept([])

argument :user_1_id, :uuid do
allow_nil?(false)
end

argument :user_2_id, :uuid do
allow_nil?(false)
end

# change(manage_relationship(:user_1_id, :user, type: :append_and_remove))
# change(manage_relationship(:user_2_id, :user, type: :append_and_remove))
end
end

attributes do
uuid_primary_key(:id)

attribute(:meeting_description, :string) do
allow_nil?(false)
end

attribute(:meeting_title, :string) do
allow_nil?(false)
end

attribute(:user_1_id, :uuid)
attribute(:user_2_id, :uuid)
end
end
This is what I came up with. No relationships here or in the user resource.
ZachDaniel
ZachDaniel•3y ago
Why not use relationships? manage_relationship won't work without a relationship
dj_goku
dj_gokuOP•3y ago
I want to use relationships. Just going from table schema -> ecto schema has always been an issue for me. I haven't done them enough to have them committed to memory. I was trying to use the helpdesk example for my example, but it didn't fit 100% since I have more than one id vs just a single id. So this is interesting. I setup the belongs_to on OneToOne resource. I setup the has_many on the user resource and set destination_attribute(:user_1_id) and it looks like ash figured out that :user_1_id are foreign keys. But what is weird is if I create an one_on_one resource either with ids set, nothing gets set for both ids, and the same thing happens if I try to update an existing one_on_one resource. OH! I needed to do Ash.Changeset.manage_relationship/4 and also add on_lookup: :relate!!!! Now I can see the IDs as part of OneOnOne resource! zach: I hope my comments are enough that you can make sense of it. LOL
ZachDaniel
ZachDaniel•3y ago
You can do that 🙂 You can also do this:
belongs_to :user, User do
attribute_writable? true
end
belongs_to :user, User do
attribute_writable? true
end
That will make your actions accept user_id without all of the manage_relationship stuff
dj_goku
dj_gokuOP•3y ago
OH! Hopefully I can pass the struct too.
ZachDaniel
ZachDaniel•3y ago
🤔 I don't believe you can
dj_goku
dj_gokuOP•3y ago
What is the best way to do database transactions? Oh, so then it would be better to do manage_relationship right?
ZachDaniel
ZachDaniel•3y ago
🤔 I'd probably just do it with the ids personally
dj_goku
dj_gokuOP•3y ago
Ok, it is hard to use the wrong uuid.
ZachDaniel
ZachDaniel•3y ago
you could do something like this:
create :blah do
argument :user, User do
allow_nil? false
end

change fn changeset, _ ->
Ash.Changeset.change_attribute(changeset, :user_id, changeset.arguments.user.id)
end
end
create :blah do
argument :user, User do
allow_nil? false
end

change fn changeset, _ ->
Ash.Changeset.change_attribute(changeset, :user_id, changeset.arguments.user.id)
end
end
If you wanted to accept a %User{} and not a user.id
dj_goku
dj_gokuOP•3y ago
Thanks! you were right though, I couldn't use %User{}
ZachDaniel
ZachDaniel•3y ago
For transactions, it depends on what exactly you're trying to do. 1. if you need to explicitly start a transaction, you can do YourApp.YouRepo.transaction(fn -> ...do your work here end) 2. there is a transaction lifecycle for all create/update/destroy actions that use ash_postgres. By default they all start a transaction to do there work in unless you specify transaction? false in the action. You have hooks to add behavior for before_transaction -> before_action -> <actual action at data layer happens here> -> after_action -> after_transaction
dj_goku
dj_gokuOP•3y ago
I want to commit 3 resources since I don't want something dangling if one of the 3 resources aren't committed.
ZachDaniel
ZachDaniel•3y ago
For example:
create :create do
change fn changeset, _ ->
changeset
|> Ash.Changeset.before_transaction(fn changeset ->
IO.inspect("hello from before the transaction")
changeset
end)
|> Ash.Changeset.before_action(fn changeset ->
IO.inspect("hello from the transaction, before the action")
changeset
end)
|> Ash.Changeset.after_action(fn changeset, successful_result ->
IO.inspect("hello from the transaction, after the action has succeeded. This doesn't happen if the action fails")
{:ok, successful_result}
end)
|> Ash.Changeset.after_transaction(fn changeset, success_or_failure_result ->
# success_or_failure_result is `{:ok, result}` or `{:error, error}`
IO.inspect("hello from after the transaction. This happens on success or failure")
success_or_failure_result
end)
end
end
create :create do
change fn changeset, _ ->
changeset
|> Ash.Changeset.before_transaction(fn changeset ->
IO.inspect("hello from before the transaction")
changeset
end)
|> Ash.Changeset.before_action(fn changeset ->
IO.inspect("hello from the transaction, before the action")
changeset
end)
|> Ash.Changeset.after_action(fn changeset, successful_result ->
IO.inspect("hello from the transaction, after the action has succeeded. This doesn't happen if the action fails")
{:ok, successful_result}
end)
|> Ash.Changeset.after_transaction(fn changeset, success_or_failure_result ->
# success_or_failure_result is `{:ok, result}` or `{:error, error}`
IO.inspect("hello from after the transaction. This happens on success or failure")
success_or_failure_result
end)
end
end
You can add multiple hooks for each part of the lifecycle, that is just an example change that uses each hook once
dj_goku
dj_gokuOP•3y ago
phew, that is a lot to digest. 😄
ZachDaniel
ZachDaniel•3y ago
So, if you do your work inside of before_action or after_action hooks, then it will all automatically happen in a database transaction
ZachDaniel
ZachDaniel•3y ago
You can read more about the action lifecycle here 🙂 https://ash-hq.org/docs/guides/ash/latest/topics/actions#action-lifecycle
Ash HQ
Guide: Actions
Read the "Actions" guide on Ash HQ
dj_goku
dj_gokuOP•3y ago
I have been all over the site tonight. LOL I have seen this page at least once. I can open a new support if you want. But I want to have I think a calculation where I sum one query, sum another query and then subtract sum_one minus sum_two. I would normally do this separately in a context with 2 functions. One that sums and then another than calls the sum function but then subtracts the two sums.
ZachDaniel
ZachDaniel•3y ago
yes, please open another support issue if you don't mind 😄

Did you find this page helpful?