mirror of
https://github.com/ash-project/ash.git
synced 2024-09-20 13:33:20 +12:00
docs: improve relationships topic guide (#521)
This commit is contained in:
parent
6a95ae388a
commit
360d72d506
1 changed files with 173 additions and 102 deletions
|
@ -1,101 +1,73 @@
|
||||||
# Relationships
|
# Relationships
|
||||||
|
|
||||||
Relationships are a core component of Ash. They provide a mechanism to describe the relationships between your resources, and through those relationships you can do things like
|
Relationships describe the connections between resources and are a core component of Ash. Defining relationships enables you to do things like
|
||||||
|
|
||||||
- Loading related data
|
- Loading related data
|
||||||
- Filtering on related data
|
- Filtering on related data
|
||||||
- Managing related records through changes on a single resource
|
- Managing related records through changes on a single resource
|
||||||
- Authorizing based on the state of related data
|
- Authorizing based on the state of related data
|
||||||
|
|
||||||
## Customizing default belongs_to attribute type
|
## Relationships Basics
|
||||||
|
|
||||||
By default, we assume foreign keys that we add by default (for `belongs_to` relationships) should be `:uuid`. To change this default, set the following configuration:
|
A relationship exists between a source resource and a destination resource. These are defined in the `relationships` block of the source resource. For example, if `MyApp.Tweet` is the source resource, and `MyApp.User` is the destination resource, we could define a relationship called `:owner` like this:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
config :ash, :default_belongs_to_type, :integer
|
defmodule MyApp.Tweet do
|
||||||
```
|
use Ash.Resource,
|
||||||
|
data_layer: my_data_layer
|
||||||
|
|
||||||
## Loading related data
|
attributes do
|
||||||
|
uuid_primary_key :id
|
||||||
|
attribute :body, :string
|
||||||
|
end
|
||||||
|
|
||||||
Loading relationships is a very common use case. There are two ways to load relationships, in the query, and on records.
|
relationships do
|
||||||
|
belongs_to :owner, MyApp.User
|
||||||
### On records
|
end
|
||||||
|
end
|
||||||
Given a set of records, like `[user1, user2]`, you can load their relationships by calling your Ash Api's `load` function.
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
YourApi.load(users, :friends)
|
|
||||||
```
|
|
||||||
|
|
||||||
This will fetch the friends of each user, and set them in the corresponding `friends` key.
|
|
||||||
|
|
||||||
### In the query
|
|
||||||
|
|
||||||
Loading in the query is currently pretty much the same as loading on records, but eventually data layers will be able to optimize these loads, potentially including them as joins in the main query, for example. The following will return the list of users with their friends loaded, as the above example.
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
User
|
|
||||||
|> Ash.Query.load(:friends)
|
|
||||||
|> YourApi.read()
|
|
||||||
```
|
|
||||||
|
|
||||||
### More complex data loading
|
|
||||||
|
|
||||||
Multiple relationships can be loaded at once, i.e
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
YourApi.load(users, [:friends, :enemies])
|
|
||||||
```
|
|
||||||
|
|
||||||
Nested relationships can be loaded:
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
YourApi.load(users, friends: [:friends, :enemies])
|
|
||||||
```
|
|
||||||
|
|
||||||
The queries used for loading can be customized by providing a query as the value.
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
friends = Ash.Query.sort(User, social_score: :asc)
|
|
||||||
|
|
||||||
YourApi.load(users, friends: friends)
|
|
||||||
```
|
|
||||||
|
|
||||||
Nested loads will be included in the parent load.
|
|
||||||
|
|
||||||
```elixir
|
|
||||||
friends =
|
|
||||||
User
|
|
||||||
|> Ash.Query.sort(social_score: :asc)
|
|
||||||
|> Ash.Query.load(:friends)
|
|
||||||
|
|
||||||
# Will load friends and friends of those friends
|
|
||||||
YourApi.load(users, friends: friends)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Managing related data
|
## Managing related data
|
||||||
|
|
||||||
See [Managing Relationships](/documentation/topics/managing-relationships.md) for more information.
|
See [Managing Relationships](/documentation/topics/managing-relationships.md) for more information.
|
||||||
|
|
||||||
|
|
||||||
## Relationships Basics
|
|
||||||
|
|
||||||
All relationships have a `source` and a `destination`, as well as a corresponding `source_attribute` and `destination_attribute`. Many to many relationships have additional fields which are discussed below. Relationships will validate at compile time that their configured attributes exist. You don't need to have a corresponding "reverse" relationship for every relationship, i.e if you have a `MyApp.Tweets` resource with `belongs_to :user, User` you aren't required to have a `has_many :tweets, MyApp.Tweet`. All that is required is that the attributes used by the relationship exist.
|
|
||||||
|
|
||||||
## Kinds of relationships
|
## Kinds of relationships
|
||||||
|
|
||||||
|
There are four kinds of relationships:
|
||||||
|
|
||||||
|
- [`belongs_to`](#belongs-to)
|
||||||
|
- [`has_one`](#has-one)
|
||||||
|
- [`has_many`](#has-many)
|
||||||
|
- [`many_to_many`](#many-to-many)
|
||||||
|
|
||||||
|
Each of these relationships has a `source` resource and a `destination` resource with a corresponding attribute on the source resource (`source_attribute`), and destination resource (`destination_attribute`). Relationships will validate that their configured attributes exist at compile time.
|
||||||
|
|
||||||
|
You don't need to have a corresponding "reverse" relationship for every relationship, i.e if you have a `MyApp.Tweet` resource with `belongs_to :user, MyApp.User` you aren't required to have a `has_many :tweets, MyApp.Tweet` on `MyApp.User`. All that is required is that the attributes used by the relationship exist.
|
||||||
|
|
||||||
### Belongs To
|
### Belongs To
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
|
# on MyApp.Tweet
|
||||||
belongs_to :owner, MyApp.User
|
belongs_to :owner, MyApp.User
|
||||||
```
|
```
|
||||||
|
|
||||||
A `belongs_to` relationship means that there is an attribute (`source_attribute`) on the source resource that uniquely identifies a record with a matching `destination_attribute` in the destination. In the example above, the source attribute would be `owner_id`, and if you wanted to change the owner, you'd modify the `owner_id` to point to a different `MyApp.User`
|
A `belongs_to` relationship means that there is an attribute (`source_attribute`) on the source resource that uniquely identifies a record with a matching attribute (`destination_attribute`) in the destination. In the example above, the source attribute on `MyApp.Tweet` is `:owner_id` and the destination attribute on `MyApp.User` is `:id`.
|
||||||
|
|
||||||
#### Belongs to Source Attribute
|
#### Attribute Defaults
|
||||||
|
|
||||||
The `destination_attribute` defaults to `:id`.
|
By default, the `source_attribute` is defined as `:<relationship_name>_id` of the type `:uuid` on the source resource and the `destination_attribute` is assumed to be `:id`. You can override the attribute names by specifying the `source_attribute` and `destination_attribute` options like so:
|
||||||
By default, a belongs_to relationship will define an attribute called `<relationship_name>_id` of type `:uuid` on the resource. To configure this, use options like:
|
|
||||||
|
```elixir
|
||||||
|
belongs_to :owner, MyApp.User do
|
||||||
|
# defaults to :<relationship_name>_id (i.e. :owner_id)
|
||||||
|
source_attribute :custom_attribute_name
|
||||||
|
|
||||||
|
# defaults to :id
|
||||||
|
destination_attribute :custom_attribute_name
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
You can further customize the `source_attribute` using options such as:
|
||||||
|
|
||||||
- `d:Ash.Resource.Dsl.relationships.belongs_to|define_attribute?` to define it yourself
|
- `d:Ash.Resource.Dsl.relationships.belongs_to|define_attribute?` to define it yourself
|
||||||
- `d:Ash.Resource.Dsl.relationships.belongs_to|attribute_type` to modify the default type
|
- `d:Ash.Resource.Dsl.relationships.belongs_to|attribute_type` to modify the default type
|
||||||
|
@ -126,6 +98,14 @@ relationships do
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Customizing default belongs_to attribute type
|
||||||
|
|
||||||
|
Destination attributes that are added by default are assumed to be `:uuid`. To change this, set the following configuration in `config.exs`:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
config :ash, :default_belongs_to_type, :integer
|
||||||
|
```
|
||||||
|
|
||||||
See the docs for more: `d:Ash.Resource.Dsl.relationships.belongs_to`
|
See the docs for more: `d:Ash.Resource.Dsl.relationships.belongs_to`
|
||||||
|
|
||||||
### Has One
|
### Has One
|
||||||
|
@ -135,68 +115,159 @@ See the docs for more: `d:Ash.Resource.Dsl.relationships.belongs_to`
|
||||||
has_one :profile, MyApp.Profile
|
has_one :profile, MyApp.Profile
|
||||||
```
|
```
|
||||||
|
|
||||||
A `has_one` is similar to a `belongs_to` except the "reference" attribute is on
|
A `has_one` relationship means that there is a unique attribute (`destination_attribute`) on the destination resource that identifies a record with a matching unique attribute (`source_resource`) in the source. In the example above, the source attribute on `MyApp.User` is `:id` and the destination attribute on `MyApp.Profile` is `:user_id`.
|
||||||
the destination resource, instead of the source. In the example above, we'd expect a `profile_id` to be on `MyApp.Profile`, and that it is unique.
|
|
||||||
|
|
||||||
#### Has One Attribute Defaults
|
A `has_one` is similar to a `belongs_to` except the reference attribute is on
|
||||||
|
the destination resource, instead of the source.
|
||||||
|
|
||||||
By default, the `source_attribute` is assumed to be `:id`, and `destination_attribute` defaults to `<snake_cased_last_part_of_module_name>_id`. In the above example, it would default `destination_attribute` to `user_id`.
|
#### Attribute Defaults
|
||||||
|
|
||||||
|
By default, the `source_attribute` is assumed to be `:id`, and `destination_attribute` defaults to `<snake_cased_last_part_of_module_name>_id`.
|
||||||
|
|
||||||
See the docs for more: `d:Ash.Resource.Dsl.relationships.has_one`
|
See the docs for more: `d:Ash.Resource.Dsl.relationships.has_one`
|
||||||
|
|
||||||
### Has Many
|
### Has Many
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
# on MyApp.Post
|
# on MyApp.User
|
||||||
has_many :comments, Comment
|
has_many :tweets, MyApp.Tweet
|
||||||
```
|
```
|
||||||
|
|
||||||
A `has_many` relationship is similar to a `has_one` in that the reference attribute is on the destination resource. The only difference between this and `has_one` is that it does not expect the destination attribute is unique on the destination, and therefore will produce a list of related items.
|
A `has_many` relationship means that there is a non-unique attribute (`destination_attribute`) on the destination resource that identifies a record with a matching unique attribute (`source_resource`) in the source. In the example above, the source attribute on `MyApp.User` is `:id` and the destination attribute on `MyApp.Tweet` is `:user_id`.
|
||||||
|
|
||||||
#### Has Many Attribute Defaults
|
A `has_many` relationship is similar to a `has_one` because the reference attribute exists on the destination resource. The only difference between this and `has_one` is that the destination attribute is not unique, and therefore will produce a list of related items. In the example above, `:tweets` corresponds to a list of `MyApp.Tweet` records.
|
||||||
|
|
||||||
By default, the `source_attribute` is assumed to be `:id`, and `destination_attribute` defaults to `<snake_cased_last_part_of_module_name>_id`. In the above example, it would default `destination_attribute` to `post_id`.
|
#### Attribute Defaults
|
||||||
|
|
||||||
|
By default, the `source_attribute` is assumed to be `:id`, and `destination_attribute` defaults to `<snake_cased_last_part_of_module_name>_id`.
|
||||||
|
|
||||||
See the docs for more: `d:Ash.Resource.Dsl.relationships.has_many`
|
See the docs for more: `d:Ash.Resource.Dsl.relationships.has_many`
|
||||||
## Many To Many Relationships
|
|
||||||
|
|
||||||
Lets say that individual todo items in our app can be added to multiple lists, and every list has multiple todo items. This is a great case for `many_to_many` relationships.
|
### Many To Many
|
||||||
|
|
||||||
For example, we could define the following `many_to_many` relationship:
|
A `many_to_many` relationship can be used to relate many source resources to many destination resources. To achieve this, the `source_attribute` and `destination_attribute` are defined on a join resource. A `many_to_many` relationship can be thought of as a combination of a `has_many` relationship on the source/destination resources and a `belongs_to` relationship on the join resource.
|
||||||
|
|
||||||
|
For example, consider two resources `MyApp.Tweet` and `MyApp.Hashtag` representing tweets and hashtags. We want to be able to associate a tweet with many hashtags, and a hashtag with many tweets. To do this, we could define the following `many_to_many` relationship:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
# on MyApp.TodoList
|
# on MyApp.Tweet
|
||||||
many_to_many :todo_items, MyApp.TodoItem do
|
many_to_many :hashtags, MyApp.Tweet do
|
||||||
through MyApp.TodoListItem
|
through MyApp.TweetHashtag
|
||||||
source_attribute_on_join_resource :list_id
|
source_attribute_on_join_resource :tweet_id
|
||||||
destination_attribute_on_join_resource :item_id
|
destination_attribute_on_join_resource :hashtag_id
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
And then we could define the "join resource" to connect everything: `MyApp.TodoListItem`
|
The `through` option specifies the "join" resource that will be used to store the relationship. We need to define this resource as well:
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule MyApp.TodoListItem do
|
defmodule MyApp.TweetHashtag do
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
data_layer: your_data_layer
|
data_layer: your_data_layer
|
||||||
|
|
||||||
attributes do
|
|
||||||
uuid_primary_key :id
|
|
||||||
end
|
|
||||||
|
|
||||||
relationships do
|
relationships do
|
||||||
belongs_to :todo_list, MyApp.TodoList do
|
belongs_to :tweet, MyApp.Tweet, primary_key?: true, allow_nil?: false
|
||||||
allow_nil? false
|
belongs_to :hashtag, MyApp.Hashtag, primary_key?: true, allow_nil?: false
|
||||||
end
|
|
||||||
|
|
||||||
belongs_to :item, MyApp.TodoItem do
|
|
||||||
allow_nil? false
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
It is convention to name this resource `<source_resource_name><destination_resource_name>` however this is not required. The attributes on the join resource must match the `source_attribute_on_join_resource` and `destination_attribute_on_join_resource` options on the `many_to_many` relationship. The relationships on the join resource are standard `belongs_to` relationships, and can be configured as such. In this case, we have specified that the `:tweet_id` and `:hashtag_id` attributes form the primary key for the join resource, and that they cannot be `nil`.
|
||||||
|
|
||||||
Now that we have a resource with the proper attributes, Ash will use this automatically under the hood when
|
Now that we have a resource with the proper attributes, Ash will use this automatically under the hood when
|
||||||
performing the relationship operations detailed above, like filtering and loading.
|
performing relationship operations like filtering and loading.
|
||||||
|
|
||||||
See the docs for more: `d:Ash.Resource.Dsl.relationships.many_to_many`
|
See the docs for more: `d:Ash.Resource.Dsl.relationships.many_to_many`
|
||||||
|
|
||||||
|
### Relationships across APIs
|
||||||
|
|
||||||
|
You will need to specify the `api` option in the relationship if the destination resource is part of a different API:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
many_to_many :authors, MyApp.OtherApi.Resource do
|
||||||
|
api MyApp.OtherApi
|
||||||
|
...
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Loading related data
|
||||||
|
|
||||||
|
There are two ways to load relationships:
|
||||||
|
|
||||||
|
- in the query using `c:Ash.Query.load/2`
|
||||||
|
- directly on records using `c:Ash.Api.load/3`
|
||||||
|
|
||||||
|
### On records
|
||||||
|
|
||||||
|
Given a single record or a set of records, it is possible to load their relationships by calling the `load` function on the record's parent API. For example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
# user = %User{...}
|
||||||
|
YourApi.load(user, :tweets)
|
||||||
|
|
||||||
|
# users = [%User{...}, %User{...}, ....]
|
||||||
|
YourApi.load(users, :tweets)
|
||||||
|
```
|
||||||
|
|
||||||
|
This will fetch the tweets for each user, and set them in the corresponding `tweets` key.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
%User{
|
||||||
|
...
|
||||||
|
tweets: [
|
||||||
|
%Tweet{...},
|
||||||
|
%Tweet{...},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See `c:Ash.Api.load/3` for more information.
|
||||||
|
|
||||||
|
### In the query
|
||||||
|
|
||||||
|
The following will return a list of users with their tweets loaded identically to the previous example:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
User
|
||||||
|
|> Ash.Query.load(:tweets)
|
||||||
|
|> YourApi.read()
|
||||||
|
```
|
||||||
|
|
||||||
|
At present, loading relationships in the query is fundamentally the same as loading on records. Eventually, data layers will be able to optimize these loads (potentially including them as joins in the main query).
|
||||||
|
|
||||||
|
See `c:Ash.Query.load/2` for more information.
|
||||||
|
|
||||||
|
### More complex data loading
|
||||||
|
|
||||||
|
Multiple relationships can be loaded at once, i.e
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
YourApi.load(users, [:tweets, :followers])
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested relationships can be loaded:
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
YourApi.load(users, followers: [:tweets, :followers])
|
||||||
|
```
|
||||||
|
|
||||||
|
The queries used for loading can be customized by providing a query as the value.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
followers = Ash.Query.sort(User, follower_count: :asc)
|
||||||
|
|
||||||
|
YourApi.load(users, followers: followers)
|
||||||
|
```
|
||||||
|
|
||||||
|
Nested loads will be included in the parent load.
|
||||||
|
|
||||||
|
```elixir
|
||||||
|
followers =
|
||||||
|
User
|
||||||
|
|> Ash.Query.sort(follower_count: :asc)
|
||||||
|
|> Ash.Query.load(:followers)
|
||||||
|
|
||||||
|
# Will load followers and followers of those followers
|
||||||
|
YourApi.load(users, followers: followers)
|
||||||
|
```
|
||||||
|
|
Loading…
Reference in a new issue