Skip to content

Commit fbe5e73

Browse files
authored
Make more references into links in docs (#4642)
1 parent c9f2c10 commit fbe5e73

File tree

6 files changed

+21
-20
lines changed

6 files changed

+21
-20
lines changed

guides/howtos/Constraints and Upserts.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ In this guide we will learn how to use constraints and upserts. To showcase thos
44

55
## put_assoc vs cast_assoc
66

7-
Imagine we are building an application that has blog posts and such posts may have many tags. Not only that, a given tag may also belong to many posts. This is a classic scenario where we would use `many_to_many` associations. Our migrations would look like:
7+
Imagine we are building an application that has blog posts and such posts may have many tags. Not only that, a given tag may also belong to many posts. This is a classic scenario where we would use [`many_to_many`](`Ecto.Schema.many_to_many/3`) associations. Our migrations would look like:
88

99
```elixir
1010
create table(:posts) do
@@ -30,11 +30,11 @@ Note we added a unique index to the tag name because we don't want to have dupli
3030

3131
Now let's also imagine we want the user to input such tags as a list of words split by comma, such as: "elixir, erlang, ecto". Once this data is received in the server, we will break it apart into multiple tags and associate them to the post, creating any tag that does not yet exist in the database.
3232

33-
While the constraints above sound reasonable, that's exactly what put us in trouble with `cast_assoc/3`. The `cast_assoc/3` changeset function was designed to receive external parameters and compare them with the associated data in our structs. To do so correctly, Ecto requires tags to be sent as a list of maps. We can see an example of this in [Polymorphic associations with many to many](Polymorphic associations with many to many.md). However, here we expect tags to be sent in a string separated by comma.
33+
While the constraints above sound reasonable, that's exactly what put us in trouble with [`cast_assoc/3`](`Ecto.Changeset.cast_assoc/3`). The `cast_assoc/3` changeset function was designed to receive external parameters and compare them with the associated data in our structs. To do so correctly, Ecto requires tags to be sent as a list of maps. We can see an example of this in [Polymorphic associations with many to many](Polymorphic associations with many to many.md). However, here we expect tags to be sent in a string separated by comma.
3434

3535
Furthermore, `cast_assoc/3` relies on the primary key field for each tag sent in order to decide if it should be inserted, updated or deleted. Again, because the user is simply passing a string, we don't have the ID information at hand.
3636

37-
When we can't cope with `cast_assoc/3`, it is time to use `put_assoc/4`. In `put_assoc/4`, we give Ecto structs or changesets instead of parameters, giving us the ability to manipulate the data as we want. Let's define the schema and the changeset function for a post which may receive tags as a string:
37+
When we can't cope with `cast_assoc/3`, it is time to use [`put_assoc/4`](`Ecto.Changeset.put_assoc/4`). In `put_assoc/4`, we give Ecto structs or changesets instead of parameters, giving us the ability to manipulate the data as we want. Let's define the schema and the changeset function for a post which may receive tags as a string:
3838

3939
```elixir
4040
defmodule MyApp.Post do
@@ -74,17 +74,17 @@ end
7474

7575
In the changeset function above, we moved all the handling of tags to a separate function, called `parse_tags/1`, which checks for the parameter, breaks each tag apart via `String.split/2`, then removes any left over whitespace with `String.trim/1`, rejects any empty string and finally checks if the tag exists in the database or not, creating one in case none exists.
7676

77-
The `parse_tags/1` function is going to return a list of `MyApp.Tag` structs which are then passed to `put_assoc/4`. By calling `put_assoc/4`, we are telling Ecto those should be the tags associated to the post from now on. In case a previous tag was associated to the post and not given in `put_assoc/4`, Ecto will invoke the behaviour defined in the `:on_replace` option, which we have set to `:delete`. The `:delete` behaviour will remove the association between the post and the removed tag from the database.
77+
The `parse_tags/1` function is going to return a list of `MyApp.Tag` structs which are then passed to [`put_assoc/4`](`Ecto.Changeset.put_assoc/4`). By calling `put_assoc/4`, we are telling Ecto those should be the tags associated to the post from now on. In case a previous tag was associated to the post and not given in `put_assoc/4`, Ecto will invoke the behaviour defined in the `:on_replace` option, which we have set to `:delete`. The `:delete` behaviour will remove the association between the post and the removed tag from the database.
7878

79-
And that's all we need to use `many_to_many` associations with `put_assoc/4`. `put_assoc/4` is very useful when we want to have more explicit control over our associations and it also works with `has_many`, `belongs_to` and all others association types.
79+
And that's all we need to use [`many_to_many`](`Ecto.Schema.many_to_many/3`) associations with `put_assoc/4`. `put_assoc/4` is very useful when we want to have more explicit control over our associations and it also works with [`has_many`](`Ecto.Schema.has_many/3`), [`belongs_to`](`Ecto.Schema.belongs_to/3`) and all others association types.
8080

8181
However, our code is not yet ready for production. Let's see why.
8282

8383
## Constraints and race conditions
8484

8585
Remember we added a unique index to the tag `:name` column when creating the tags table. We did so to protect us from having duplicate tags in the database.
8686

87-
By adding the unique index and then using `get_by` with a `insert!` to get or insert a tag, we introduced a potential error in our application. If two posts are submitted at the same time with a similar tag, there is a chance we will check if the tag exists at the same time, leading both submissions to believe there is no such tag in the database. When that happens, only one of the submissions will succeed while the other one will fail. That's a race condition: your code will error from time to time, only when certain conditions are met. And those conditions are time sensitive.
87+
By adding the unique index and then using [`Repo.get_by`](`c:Ecto.Repo.get_by/3`) with a [`Repo.insert!`](`c:Ecto.Repo.insert!/2`) to get or insert a tag, we introduced a potential error in our application. If two posts are submitted at the same time with a similar tag, there is a chance we will check if the tag exists at the same time, leading both submissions to believe there is no such tag in the database. When that happens, only one of the submissions will succeed while the other one will fail. That's a race condition: your code will error from time to time, only when certain conditions are met. And those conditions are time sensitive.
8888

8989
Luckily Ecto gives us a mechanism to handle constraint errors from the database.
9090

@@ -105,7 +105,7 @@ defp get_or_insert_tag(name) do
105105
end
106106
```
107107

108-
Instead of inserting the tag directly, we now build a changeset, which allows us to use the `unique_constraint` annotation. Now if the `Repo.insert` operation fails because the unique index for `:name` is violated, Ecto won't raise, but return an `{:error, changeset}` tuple. Therefore, if `Repo.insert` succeeds, it is because the tag was saved, otherwise the tag already exists, which we then fetch with `Repo.get_by!`.
108+
Instead of inserting the tag directly, we now build a changeset, which allows us to use the `unique_constraint` annotation. Now if the [`Repo.insert`](`c:Ecto.Repo.insert/2`) operation fails because the unique index for `:name` is violated, Ecto won't raise, but return an `{:error, changeset}` tuple. Therefore, if `Repo.insert` succeeds, it is because the tag was saved, otherwise the tag already exists, which we then fetch with [`Repo.get_by!`](`c:Ecto.Repo.get_by!/3`).
109109

110110
While the mechanism above fixes the race condition, it is a quite expensive one: we need to perform two queries for every tag that already exists in the database: the (failed) insert and then the repository lookup. Given that's the most common scenario, we may want to rewrite it to the following:
111111

@@ -228,6 +228,6 @@ defmodule MyApp.Post do
228228
end
229229
```
230230

231-
Instead of getting and inserting each tag individually, the code above works on all tags at once, first by building a list of maps which is given to `insert_all`. Then we look up all tags with the given names. Regardless of how many tags are sent, we will perform only 2 queries - unless no tag is sent, in which we return an empty list back promptly. This solution is only possible thanks to the `:on_conflict` option, which guarantees `insert_all` won't fail in case a unique index is violated, such as from duplicate tag names. Remember, `insert_all` won't autogenerate values like timestamps. That's why we define a timestamp placeholder and reuse it across `inserted_at` and `updated_at` fields.
231+
Instead of getting and inserting each tag individually, the code above works on all tags at once, first by building a list of maps which is given to [`Repo.insert_all`](`c:Ecto.Repo.insert_all/3`). Then we look up all tags with the given names. Regardless of how many tags are sent, we will perform only 2 queries - unless no tag is sent, in which we return an empty list back promptly. This solution is only possible thanks to the `:on_conflict` option, which guarantees `insert_all` won't fail in case a unique index is violated, such as from duplicate tag names. Remember, `insert_all` won't autogenerate values like timestamps. That's why we define a timestamp placeholder and reuse it across `inserted_at` and `updated_at` fields.
232232

233233
Finally, keep in mind that we haven't used transactions in any of the examples so far. That decision was deliberate as we relied on the fact that getting or inserting tags is an idempotent operation, i.e. we can repeat it many times for a given input and it will always give us the same result back. Therefore, even if we fail to introduce the post to the database due to a validation error, the user will be free to resubmit the form and we will just attempt to get or insert the same tags once again. The downside of this approach is that tags will be created even if creating the post fails, which means some tags may not have posts associated to them. In case that's not desired, the whole operation could be wrapped in a transaction or modeled with `Ecto.Multi`.

guides/howtos/Dynamic queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ Post
120120
|> order_by(^order_by)
121121
```
122122

123-
The `dynamic` macro allows us to build dynamic expressions that are later interpolated into the query. `dynamic` expressions can also be interpolated into dynamic expressions, allowing developers to build complex expressions dynamically without hassle.
123+
The [`dynamic`](`Ecto.Query.dynamic/2`) macro allows us to build dynamic expressions that are later interpolated into the query. `dynamic` expressions can also be interpolated into dynamic expressions, allowing developers to build complex expressions dynamically without hassle.
124124

125125
By using dynamic fragments, we can decouple the processing of parameters from the query generation. Let's see a more complex example.
126126

lib/ecto.ex

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ defmodule Ecto do
238238
# Returns maps as defined in select
239239
Repo.all(query)
240240
241-
Queries are defined and extended with the `from` macro. The supported
241+
Queries are defined and extended with the [`from`](`Ecto.Query.from/2`) macro. The supported
242242
keywords are:
243243
244244
* `:distinct`
@@ -265,10 +265,10 @@ defmodule Ecto do
265265
from u in User, where: u.age > ^min
266266
end
267267
268-
Besides `Repo.all/1` which returns all entries, repositories also
269-
provide `Repo.one/1` which returns one entry or nil, `Repo.one!/1`
270-
which returns one entry or raises, `Repo.get/2` which fetches
271-
entries for a particular ID and more.
268+
Besides [`Repo.all/1`](`c:Ecto.Repo.all/2`) which returns all entries, repositories
269+
also provide [`Repo.one/1`](`c:Ecto.Repo.one/2`) which returns one entry or `nil`,
270+
[`Repo.one!/1`](`c:Ecto.Repo.one!/2`)) which returns one entry or raises,
271+
[`Repo.get/2`](`c:Ecto.Repo.get/3`) which fetches entries for a particular ID and more.
272272
273273
Finally, if you need an escape hatch, Ecto provides fragments
274274
(see `Ecto.Query.API.fragment/1`) to inject SQL (and non-SQL)

lib/ecto/query.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -593,7 +593,7 @@ defmodule Ecto.Query do
593593
594594
## `where`, `having` and a `join`'s `on`
595595
596-
The `dynamic` macro can be interpolated at the root of a `where`,
596+
The [`dynamic`](`dynamic/2`) macro can be interpolated at the root of a `where`,
597597
`having` or a `join`'s `on`.
598598
599599
For example, assuming the `conditions` variable defined in the
@@ -2870,7 +2870,7 @@ defmodule Ecto.Query do
28702870
28712871
## Dynamic preloads
28722872
2873-
Preloads can also be specified dynamically using the `dynamic` macro:
2873+
Preloads can also be specified dynamically using the [`dynamic`](`dynamic/2`) macro:
28742874
28752875
preloads = [comments: dynamic([comments: c], c)]
28762876

lib/ecto/queryable.ex

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ defprotocol Ecto.Queryable do
22
@moduledoc """
33
Converts a data structure into an `Ecto.Query`.
44
5-
This is used by `Ecto.Repo` and also `from` macro. For example, `Repo.all`
5+
This is used by `Ecto.Repo` and also by the [`from`](`Ecto.Query.from/2`) macro.
6+
For example, [`Repo.all`](`c:Ecto.Repo.all/2`)
67
expects any queryable as argument, which is why you can do `Repo.all(MySchema)`
78
or `Repo.all(query)`. Furthermore, when you write `from ALIAS in QUERYABLE`,
89
`QUERYABLE` accepts any data structure that implements `Ecto.Queryable`.

lib/ecto/schema.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ defmodule Ecto.Schema do
4040
end
4141
4242
By default, a schema will automatically generate a primary key which is named
43-
`id` and of type `:integer`. The `field` macro defines a field in the schema
43+
`id` and of type `:integer`. The [`field`](`field/3`) macro defines a field in the schema
4444
with given name and type. `has_many` associates many posts with the user
4545
schema. Schemas are regular structs and can be created and manipulated directly
4646
using Elixir's struct API:
@@ -180,8 +180,8 @@ defmodule Ecto.Schema do
180180
* `@field_source_mapper` - a function that receives the current field name
181181
and returns the mapping of this field name in the underlying source.
182182
In other words, it is a mechanism to automatically generate the `:source`
183-
option for the `field` macro. It defaults to `fn x -> x end`, where no
184-
field transformation is done;
183+
option for the [`field`](`field/3`) macro. It defaults to `fn x -> x end`,
184+
where no field transformation is done;
185185
186186
The advantage of configuring the schema via those attributes is
187187
that they can be set with a macro to configure application wide

0 commit comments

Comments
 (0)