ash-project / ash-project/ash

Polymorphic relationships

Open
#563 68 comments 1 reaction 0 assignees View on GitHub
enhancement
Dominant language
Elixir
Stars
2.5k
Forks
422
Avg merge
23h 26m
Merged PRs (30d)
46

Description

I've had polymorphic relationships on my wishlist, and looked into the `Ash.Type.Union` + `calculation` approach and thought maybe things could go a bit deeper. Realistically, this would combine well with resource behaviors, which is a topic unto itself.

```elixir
defmodule App.User do
@payment_method_types [
App.Payment.Ideal
App.Payment.Swift
App.Payment.GooglePay
App.Payment.ApplePay
App.Payment.Stripe
]

relationships do
has_many :payment_methods, @payment_method_types
end

# ...
end

defmodule App.Payment.Ideal do
relationships do
belongs_to :user, App.User
end

# ...
end
```

In terms of an efficient `AshPostgres` implementation, it would not be dissimilar to existing relationships, but, in addition to `payment_method_id`, there would also be an enum `payment_method_type` to describe the possible types. `load` operations follow this pattern:

```sql
select * from users u
left join payment_ideal pi
on u.payment_method_type = 'App.Payment.Ideal' and u.id = pi.user_id
left join payment_swift ps
on u.payment_method_type = 'App.Payment.Swift' and u.id = ps.user_id
-- you get the idea
where u.id = 'a-nice-uuid'
or u.id = 'or-this-one'
or u.id = 'even-that-one';
```

The same transformation is applicable to `many_to_many` relationships, including those that are bidirectionally polymorphic.

Elixir is quite a good candidate for this kind of thing, because it is type-agnostic enough to allow you to return heterogeneous lists, but makes it easy to work with them by pattern-matching the struct. More than that, first-class support for polymorphism would be a huge boon to describing a lot of domain models.

Nested loads must be handled with care internally (resource behaviors addresses this), but the engine can complain if it doesn't have enough information to determine whether it can perform a further `load` on the result types, or different loads could be specified for different types, but this starts to get messy quickly.

A less-efficient (non-`join`ed) approach when the relationship is polymorphic might be a first stage strategy.

In any case, I wanted to open the discussion on this and see what immediate challenges and benefits come to mind.

### Other data-layers

- ETS doesn't support native joins anyways.
- Mnesia already supports 2-tuple identifiers which fit exactly the purpose.
- In general, when crossing data-layer boundaries the operations would need to be split anyways.

### Alternative `join` approach

If desired, an alternative approach is to use a column for each type:

```sql
select * from users u
left join payment_ideal pi
on u.payment_method_ideal_id = pi.user_id
left join payment_swift pi
on u.payment_method_swift_id = pi.user_id
--- etc.
```

Which would require a `constraint check` (for `allow_nil?: true`, this would be `1 >=`):

```sql
alter table users
add constraint check_one_payment_not_null check
(
1 =
(
case when payment_method_ideal_id is null then 0 else 1 end
+ case when payment_method_swift_id is null then 0 else 1 end
-- etc.
)
)
```

Contributor guide

Open the contributing guide

Research direction

The issue discusses Ash.Type.Union, calculations, resource behaviors, and AshPostgres, but names no files or tests. Start by tracing the existing relationship-loading and AshPostgres implementations, then review how nested loads and other data layers would be represented. Done requires an agreed design and implementation scope for polymorphic relationships, not just a query strategy.

Written by the indexing model from the issue text.

Assessment

Tech stack
elixir, postgresql, sql
Domain
backend-api-design, databases
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
20/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.