-
Notifications
You must be signed in to change notification settings - Fork 1.4k
/
associations.ex
401 lines (314 loc) · 11.3 KB
/
associations.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
import Ecto.Query, only: [from: 2, join: 4, distinct: 3, select: 3]
defmodule Ecto.Associations do
@moduledoc """
Documents the functions required for associations to implement
in order to work with Ecto query mechanism.
This module contains documentation for those interested in
understanding how Ecto associations work internally. If you are
interested in an overview about associations in Ecto, you should
look into the documentation for `Ecto` and `Ecto.Schema`
modules.
## Associations
Associations work in Ecto via behaviours. Anyone can add new
associations to Ecto as long as they implement the callbacks
specified in this module.
Note though that, since the associations API is in development,
existing callbacks signature and new callbacks can be added
in upcoming Ecto releases.
"""
@type t :: %{__struct__: atom, cardinality: :one | :many,
field: atom, owner_key: atom, owner: atom}
use Behaviour
@doc """
Builds the association struct.
The struct must be defined in the module that implements the
callback and it must contain at least the following keys:
* `:cardinality` - tells if the association is one to one
or one/many to many
* `:field` - tells the field in the owner struct where the
association should be stored
* `:owner` - the owner module of the association
* `:owner_key` - the key in the owner with the association value
"""
defcallback struct(module, field :: atom, opts :: Keyword.t) :: t
@doc """
Builds a model for the given association.
The struct to build from is given as argument in case default values
should be set in the struct.
Invoked by `Ecto.Model.build/2`.
"""
defcallback build(t, Ecto.Model.t) :: Ecto.Model.t
@doc """
Returns an association join query.
This callback receives the association struct and it must return
a query that retrieves all associated objects using joins up to
the owner association.
For example, a `has_many :comments` inside a `Post` module would
return:
from c in Comment, join: p in Post, on: c.post_id == p.id
Note all the logic must be expressed inside joins, as fields like
`where` and `order_by` won't be used by the caller.
This callback is invoked when `join: assoc(p, :comments)` is used
inside queries.
"""
defcallback joins_query(t) :: Ecto.Query.t
@doc """
Returns the association query.
This callback receives the association struct and it must return
a query that retrieves all associated objects with the given
values for the owner key.
This callback is used by `Ecto.Model.assoc/2`.
"""
defcallback assoc_query(t, values :: [term]) :: Ecto.Query.t
@doc """
Returns information used by the preloader.
"""
defcallback preload_info(t) ::
{:assoc, t, atom} | {:through, t, [atom]}
@doc """
Retrieves the association from the given model.
"""
def association_from_model!(model, assoc) do
model.__schema__(:association, assoc) ||
raise ArgumentError, "model #{inspect model} does not have association #{inspect assoc}"
end
@doc """
Returns the association key for the given module with the given prefix.
## Examples
iex> Ecto.Associations.association_key(Hello.World, :id)
:world_id
iex> Ecto.Associations.association_key(Hello.HTTP, :id)
:http_id
iex> Ecto.Associations.association_key(Hello.HTTPServer, :id)
:http_server_id
"""
def association_key(module, suffix) do
prefix = module |> Module.split |> List.last |> underscore
:"#{prefix}_#{suffix}"
end
defp underscore(""), do: ""
defp underscore(<<h, t :: binary>>) do
<<to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<h, t, rest :: binary>>, _) when h in ?A..?Z and not t in ?A..?Z do
<<?_, to_lower_char(h), t>> <> do_underscore(rest, t)
end
defp do_underscore(<<h, t :: binary>>, prev) when h in ?A..?Z and not prev in ?A..?Z do
<<?_, to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<?-, t :: binary>>, _) do
<<?_>> <> do_underscore(t, ?-)
end
defp do_underscore(<< "..", t :: binary>>, _) do
<<"..">> <> underscore(t)
end
defp do_underscore(<<?.>>, _), do: <<?.>>
defp do_underscore(<<?., t :: binary>>, _) do
<<?/>> <> underscore(t)
end
defp do_underscore(<<h, t :: binary>>, _) do
<<to_lower_char(h)>> <> do_underscore(t, h)
end
defp do_underscore(<<>>, _) do
<<>>
end
defp to_lower_char(char) when char in ?A..?Z, do: char + 32
defp to_lower_char(char), do: char
end
defmodule Ecto.Associations.NotLoaded do
@moduledoc """
Struct returned by one to one associations when there are not loaded.
The fields are:
* `__field__` - the association field in `__owner__`
* `__owner__` - the model that owns the association
"""
defstruct [:__field__, :__owner__]
defimpl Inspect do
def inspect(not_loaded, _opts) do
msg = "association #{inspect not_loaded.__field__} is not loaded"
~s(#Ecto.Associations.NotLoaded<#{msg}>)
end
end
end
defmodule Ecto.Associations.Has do
@moduledoc """
The association struct for `has_one` and `has_many` associations.
Its fields are:
* `cardinality` - The association cardinality
* `field` - The name of the association field on the model
* `owner` - The model where the association was defined
* `assoc` - The model that is associated
* `owner_key` - The key on the `owner` model used for the association
* `assoc_key` - The key on the `associated` model used for the association
"""
@behaviour Ecto.Associations
defstruct [:cardinality, :field, :owner, :assoc, :owner_key, :assoc_key]
@doc false
def struct(module, name, opts) do
ref =
cond do
ref = opts[:references] ->
ref
primary_key = Module.get_attribute(module, :primary_key) ->
elem(primary_key, 0)
true ->
raise ArgumentError, "need to set :references option for " <>
"association #{inspect name} when model has no primary key"
end
unless Module.get_attribute(module, :ecto_fields)[ref] do
raise ArgumentError, "model does not have the field #{inspect ref} used by " <>
"association #{inspect name}, please set the :references option accordingly"
end
assoc = Keyword.fetch!(opts, :queryable)
unless is_atom(assoc) do
raise ArgumentError, "association queryable must be a module, got: #{inspect assoc}"
end
%__MODULE__{
field: name,
cardinality: Keyword.fetch!(opts, :cardinality),
owner: module,
assoc: assoc,
owner_key: ref,
assoc_key: opts[:foreign_key] || Ecto.Associations.association_key(module, ref)
}
end
@doc false
def build(%{assoc: assoc, owner_key: owner_key, assoc_key: assoc_key}, struct) do
Map.put apply(assoc, :__struct__, []), assoc_key, Map.get(struct, owner_key)
end
@doc false
def joins_query(refl) do
from o in refl.owner,
join: q in ^refl.assoc,
on: field(q, ^refl.assoc_key) == field(o, ^refl.owner_key)
end
@doc false
def assoc_query(refl, values) do
from x in refl.assoc,
where: field(x, ^refl.assoc_key) in ^values
end
@doc false
def preload_info(refl) do
{:assoc, refl, refl.assoc_key}
end
end
defmodule Ecto.Associations.HasThrough do
@moduledoc """
The association struct for `has_one` and `has_many` through associations.
Its fields are:
* `cardinality` - The association cardinality
* `field` - The name of the association field on the model
* `owner` - The model where the association was defined
* `owner_key` - The key on the `owner` model used for the association
* `through` - The through associations
"""
@behaviour Ecto.Associations
defstruct [:cardinality, :field, :owner, :owner_key, :through]
@doc false
def struct(module, name, opts) do
through = Keyword.fetch!(opts, :through)
refl =
case through do
[h,_|_] ->
Module.get_attribute(module, :ecto_assocs)[h]
_ ->
raise ArgumentError, ":through expects a list with at least two entries: " <>
"the association in the current module and one step through, got: #{inspect through}"
end
unless refl do
raise ArgumentError, "model does not have the association #{inspect hd(through)} " <>
"used by association #{inspect name}, please ensure the association exists and " <>
"is defined before the :through one"
end
%__MODULE__{
field: name,
cardinality: Keyword.fetch!(opts, :cardinality),
through: through,
owner: module,
owner_key: refl.owner_key,
}
end
@doc false
def build(%{field: name}, %{__struct__: struct}) do
raise ArgumentError,
"cannot build through association #{inspect name} for #{inspect struct}. " <>
"Instead build the intermediate steps explicitly."
end
@doc false
def preload_info(refl) do
{:through, refl, refl.through}
end
@doc false
def joins_query(%{owner: owner, through: through}) do
joins_query(through, owner) |> elem(0)
end
@doc false
def assoc_query(%{owner: owner, through: [h|t]}, values) do
refl = owner.__schema__(:association, h)
{query, counter} = joins_query(t, refl.__struct__.assoc_query(refl, values))
query |> distinct([x: counter], x) |> select([x: counter], x)
end
defp joins_query(through, query) do
Enum.reduce(through, {query, 0}, fn current, {acc, counter} ->
{join(acc, :inner, [x: counter], assoc(x, ^current)), counter + 1}
end)
end
end
defmodule Ecto.Associations.BelongsTo do
@moduledoc """
The association struct for a `belongs_to` association.
Its fields are:
* `cardinality` - The association cardinality
* `field` - The name of the association field on the model
* `owner` - The model where the association was defined
* `assoc` - The model that is associated
* `owner_key` - The key on the `owner` model used for the association
* `assoc_key` - The key on the `assoc` model used for the association
"""
@behaviour Ecto.Associations
defstruct [:cardinality, :field, :owner, :assoc, :owner_key, :assoc_key]
@doc false
def struct(module, name, opts) do
ref =
cond do
ref = opts[:references] ->
ref
primary_key = Module.get_attribute(module, :primary_key) ->
elem(primary_key, 0)
true ->
raise ArgumentError, "need to set :references option for " <>
"association #{inspect name} when model has no primary key"
end
assoc = Keyword.fetch!(opts, :queryable)
unless is_atom(assoc) do
raise ArgumentError, "association queryable must be a module, got: #{inspect assoc}"
end
%__MODULE__{
field: name,
cardinality: :one,
owner: module,
assoc: assoc,
owner_key: Keyword.fetch!(opts, :foreign_key),
assoc_key: ref
}
end
@doc false
def build(%{assoc: assoc}, _struct) do
apply(assoc, :__struct__, [])
end
@doc false
def joins_query(refl) do
from o in refl.owner,
join: q in ^refl.assoc,
on: field(q, ^refl.assoc_key) == field(o, ^refl.owner_key)
end
@doc false
def assoc_query(refl, values) do
from x in refl.assoc,
where: field(x, ^refl.assoc_key) in ^values
end
@doc false
def preload_info(refl) do
{:assoc, refl, refl.assoc_key}
end
end