|
1 | 1 | # Embedded Schemas |
2 | 2 |
|
3 | | -Embedded schemas are a feature in Ecto that allows you to define and validate structured data. This data can reside in-memory through structs or, with a migration, be stored in a database field. Embedded schemas work with maps and arrays. |
| 3 | +Embedded schemas allow you to define and validate structured data. This data can live in memory, or can be stored in the database. Some use cases for embedded schemas include: |
4 | 4 |
|
5 | | -They're great for: |
| 5 | +- You are maintaining intermediate-state data, like when UI form fields map onto multiple tables in a database. |
6 | 6 |
|
7 | | -- Storing additional data about an entity without modifying your schema. |
8 | | -- Storing additional data about an entity without tracking foreign key/many-to-many relationships |
9 | | -- Validating arbitrary data structures |
| 7 | +- You are working within a persisted parent schema and you want to embed data that is... |
10 | 8 |
|
11 | | -Example use cases: |
| 9 | + - simple, like a map of user preferences inside a User schema. |
| 10 | + - changes often, like a list of product images with associated structured data inside a Product schema. |
| 11 | + - requires complex tracking and validation, like an Address schema inside a User schema. |
12 | 12 |
|
13 | | -- User profiles: storing additional information like profile pictures, settings |
14 | | -- Shop products: storing additional product images |
| 13 | +- You are using a document storage database and you want to interact with and manipulate embedded documents. |
15 | 14 |
|
16 | | -While embedded schemas require an initial migration to create the field, any subsequent modifications to the data structure don't. |
| 15 | +## User Profile Example |
17 | 16 |
|
18 | | -This feature is great for situations where you'd like to store data associated to a specific entity, but a relation or new table would be overkill. You can even write changesets to perform validations on this data! The downside, however, is as you change the embedded schema overtime, early data won't have fields that you added later on. |
19 | | - |
20 | | -## Example |
21 | | - |
22 | | -An example of embedded schemas is to store additional information about the current user. The finished example for this guide's embedded schema will look like this: |
| 17 | +Let's explore an example where we have a User and want to store "profile" information about them. The data we want to store is UI-dependent information which is likely to change over time alongside changes in the UI. Also, this data is not necessarily important enough to warrant new User `field`s in the User schema, as it is not data that is fundamental to the User. An embedded schema is a good solution for this kind of data. |
23 | 18 |
|
24 | 19 | ```elixir |
25 | 20 | defmodule User do |
26 | 21 | use Ecto.Schema |
27 | 22 |
|
28 | 23 | schema "users" do |
29 | | - field :is_active, :boolean |
| 24 | + field :full_name, :string |
30 | 25 | field :email, :string |
| 26 | + field :avatar_url, :string |
31 | 27 | field :confirmed_at, :naive_datetime |
32 | 28 |
|
33 | | - embeds_one "profile" do |
34 | | - field :age, :integer |
35 | | - field :favorite_color, Ecto.Enum, values: [:red, :green, :blue, :pink, :black, :orange] |
36 | | - field :avatar_url, :string |
| 29 | + embeds_one :profile do |
| 30 | + field :online, :boolean |
| 31 | + field :dark_mode, :boolean |
| 32 | + field :visibility, Ecto.Enum, values: [:public, :private, :friends_only] |
37 | 33 | end |
38 | 34 |
|
39 | 35 | timestamps() |
40 | 36 | end |
41 | 37 | end |
42 | 38 | ``` |
43 | 39 |
|
44 | | -Let's kick things off with a tutorial to explain how to recreate this use case: building a solution for user profiles in an example app. |
45 | | - |
46 | | -### Writing the schema |
47 | | - |
48 | | -The first step is to write the structure of the data we're storing. In the case of our profile, we'd like to store the user's age, favorite color, and a profile picture. |
| 40 | +### Embeds |
49 | 41 |
|
50 | | -To begin writing this embedded schema, we must first think about what structure we want. Do we want to store an array of structures (using `embeds_many`) or just one (using `embeds_one`)? |
51 | | - |
52 | | -In this case, every user should have only one profile associated to them, so we'll begin by writing like any other Ecto schema: |
| 42 | +There are two ways to represent embedded data within a schema, `embeds_many`, which creates a list of embeds, and `embeds_one`, which creates only a single instance of the embed. Your choice here affects the behavior of embed-specific functions like `Ecto.Repo.put_embed/4` and `Ecto.Repo.cast_embed/4`, so choose whichever is most appropriate to your use case. In our example we are going to use `embeds_one` since users will only ever have one profile associated with them. |
53 | 43 |
|
54 | 44 | ```elixir |
55 | 45 | defmodule User do |
56 | 46 | use Ecto.Schema |
57 | 47 |
|
58 | 48 | schema "users" do |
59 | | - field :is_active, :boolean |
| 49 | + field :full_name, :string |
60 | 50 | field :email, :string |
| 51 | + field :avatar_url, :string |
61 | 52 | field :confirmed_at, :naive_datetime |
62 | 53 |
|
63 | 54 | embeds_one :profile do |
64 | | - field :age, :integer |
65 | | - field :favorite_color, Ecto.Enum, values: [:red, :green, :blue, :pink, :black, :orange] |
66 | | - field :avatar_url, :string |
| 55 | + field :online, :boolean |
| 56 | + field :dark_mode, :boolean |
| 57 | + field :visibility, Ecto.Enum, values: [:public, :private, :friends_only] |
67 | 58 | end |
68 | 59 |
|
69 | 60 | timestamps() |
70 | 61 | end |
71 | 62 | end |
72 | 63 | ``` |
73 | 64 |
|
74 | | -We can, however, clean this up a little. You can separate the profile to a distinct module and keep the `User` module tidy: |
| 65 | +### Extracting the embeds |
| 66 | + |
| 67 | +While the above User schema is simple and sufficient, we might want to work independently with the embedded profile struct. For example, if there was a lot of functionality devoted solely to manipulating the profile data, we'd want to consider extracting the embedded schema into its own module. |
75 | 68 |
|
76 | 69 | ```elixir |
77 | 70 | # user/user.ex |
78 | 71 | defmodule User do |
79 | 72 | use Ecto.Schema |
80 | 73 |
|
81 | 74 | schema "users" do |
82 | | - field :is_active, :boolean |
| 75 | + field :full_name, :string |
83 | 76 | field :email, :string |
| 77 | + field :avatar_url, :string |
84 | 78 | field :confirmed_at, :naive_datetime |
85 | 79 |
|
86 | 80 | embeds_one :profile, UserProfile |
| 81 | + |
87 | 82 | timestamps() |
88 | 83 | end |
89 | 84 | end |
|
92 | 87 | defmodule UserProfile do |
93 | 88 | use Ecto.Schema |
94 | 89 |
|
95 | | - embedded_schema "profile" do |
96 | | - field :age, :integer |
97 | | - field :favorite_color, Ecto.Enum, values: [:red, :green, :blue, :pink, :black, :orange] |
98 | | - field :avatar_url, :string |
| 90 | + embedded_schema do |
| 91 | + field :online, :boolean |
| 92 | + field :dark_mode, :boolean |
| 93 | + field :visibility, Ecto.Enum, values: [:public, :private, :friends_only] |
99 | 94 | end |
100 | 95 | end |
101 | 96 | ``` |
102 | 97 |
|
103 | | -Ta-da! Neat. Note we replaced the `embeds_one` macro by `embedded_schema`: `embeds_one` and `embeds_many` function like fields, similar to relations like `has_many`. |
| 98 | +It is important to remember that `embedded_schema` has many use cases independent of `embeds_one` and `embeds_many`. You can think of them as a persistence agnostic `schema`. This makes embedded schemas ideal for scenarios where you want to manage structured data without necessarily persisting it. For example, if you want to build a contact form, you still want to parse and validate the data, but the data is likely not persisted anywhere. Instead, it is used to send an email. Embedded schemas would be a good fit for such use case. |
104 | 99 |
|
105 | | -### Writing the migration |
| 100 | +### Migrations |
106 | 101 |
|
107 | | -To save this embedded schema to a database, we need to write a corresponding migration. Depending on whether you chose `embeds_one` or `embeds_many`, you must choose the corresponding `map` or `array` data type. |
108 | | - |
109 | | -We used `embeds_one`, so the migration should have a type of `map`. |
| 102 | +If you wish to save your embedded schema to the database, you need to write a migration to include the embedded data. |
110 | 103 |
|
111 | 104 | ```elixir |
112 | 105 | alter table("users") do |
113 | 106 | add :profile, :map |
114 | 107 | end |
115 | 108 | ``` |
116 | 109 |
|
117 | | -### Using changesets |
| 110 | +Whether you use `embeds_one` or `embeds_many`, it is recommended to use the `:map` data type (although `{:array, :map}` will work with `embeds_many` as well). The reason is that typical relational databases are likely to represent a `:map` as JSON (or JSONB in Postgres), allowing Ecto adapter libraries more flexibility over how to efficiently store the data. |
| 111 | + |
| 112 | +### Changesets |
118 | 113 |
|
119 | | -When it comes to validation, you can define a changeset function for each module. For example, the module may say that both `age` and `favorite_color` fields are required: |
| 114 | +Changeset functionality for embeds will allow you to enforce arbitrary validations on the data. You can define a changeset function for each module. For example, the UserProfile module could require the `online` and `visibility` fields to be present when generating a changeset. |
120 | 115 |
|
121 | 116 | ```elixir |
122 | 117 | defmodule UserProfile do |
123 | 118 | # ... |
124 | 119 |
|
125 | | - def changeset(profile, attrs \\ %{}) do |
| 120 | + def changeset(%UserProfile{} = profile, attrs \\ %{}) do |
126 | 121 | profile |
127 | | - |> cast(attrs, [:age, :favorite_color, :avatar_url]) |
128 | | - |> validate_required([:age, :favorite_color]) |
| 122 | + |> cast(attrs, [:online, :dark_mode, :visibility]) |
| 123 | + |> validate_required([:online, :visibility]) |
129 | 124 | end |
130 | 125 | end |
| 126 | + |
| 127 | +profile = %UserProfile{} |
| 128 | +UserProfile.changeset(profile, %{online: true, visibility: :public}) |
131 | 129 | ``` |
132 | 130 |
|
133 | | -On the user side, you also define a `changeset/2` function, and then you use `cast_embed/3` to invoke the `UserProfile` changeset: |
| 131 | +Meanwhile, the User changeset function can require its own validations without worrying about the details of the UserProfile changes because it can pass that responsibility to UserProfile via `cast_embed/3`. A validation failure in an embed will cause the parent changeset to be invalid, even if the parent changeset itself had no errors. |
134 | 132 |
|
135 | 133 | ```elixir |
136 | 134 | defmodule User do |
137 | | - use Ecto.Schema |
138 | | - |
139 | 135 | # ... |
140 | 136 |
|
141 | 137 | def changeset(user, attrs \\ %{}) do |
| 138 | + user |
| 139 | + |> cast(attrs, [:full_name, :email, :avatar_url]) |
| 140 | + |> cast_embed(:profile, required: true) |
| 141 | + end |
| 142 | +end |
| 143 | + |
| 144 | +changeset = User.changeset(%User{}, %{profile: %{online: true}}) |
| 145 | +changeset.valid? # => false; "visibility can't be blank" |
| 146 | +changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}}) |
| 147 | +changeset.valid? # => true |
| 148 | +``` |
| 149 | + |
| 150 | +In situations where you have kept the embedded schema within the parent module, e.g., you have not extracted a UserProfile, you can still have custom changeset functions for the embedded data within the parent schema. |
| 151 | + |
| 152 | +```elixir |
| 153 | +defmodule User do |
| 154 | + use Ecto.Schema |
| 155 | + |
| 156 | + schema "users" do |
| 157 | + field :full_name, :string |
| 158 | + field :email, :string |
| 159 | + field :avatar_url, :string |
| 160 | + field :confirmed_at, :naive_datetime |
| 161 | + |
| 162 | + embeds_one :profile, Profile do |
| 163 | + field :online, :boolean |
| 164 | + field :dark_mode, :boolean |
| 165 | + field :visibility, Ecto.Enum, values: [:public, :private, :friends_only] |
| 166 | + end |
| 167 | + |
| 168 | + timestamps() |
| 169 | + end |
| 170 | + |
| 171 | + def changeset(%User{} = user, attrs \\ %{}) do |
142 | 172 | user |
143 | 173 | |> cast(attrs, [:full_name, :email]) |
144 | | - # By default it calls UserProfile.changeset/2, pass the :with option to change it |
145 | | - |> cast_embed(:user_profile, required: true) |
| 174 | + |> cast_embed(:profile, required: true, with: &profile_changeset/2) |
| 175 | + end |
| 176 | + |
| 177 | + def profile_changeset(profile, attrs \\ %{}) do |
| 178 | + profile |
| 179 | + |> cast(attrs, [:online, :dark_mode, :visibility]) |
| 180 | + |> validate_required([:online, :visibility]) |
146 | 181 | end |
147 | 182 | end |
| 183 | + |
| 184 | +changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}}) |
| 185 | +changeset.valid? # => true |
148 | 186 | ``` |
| 187 | + |
| 188 | +### Querying embedded data |
| 189 | + |
| 190 | +Once you have written embedded data to the database, you can use it in queries on the parent schema. |
| 191 | + |
| 192 | +```elixir |
| 193 | +user_changeset = User.changeset(%User{}, %{profile: %{online: true, visibility: :public}}) |
| 194 | +{:ok, _user} = Repo.insert(user_changeset) |
| 195 | + |
| 196 | +(Ecto.Query.from u in User, select: {u.profile["online"], u.profile["visibility"]}) |> Repo.one |
| 197 | +# => {true, "public"} |
| 198 | + |
| 199 | +(Ecto.Query.from u in User, select: u.profile, where: u.profile["visibility"] == :public) |> Repo.all |
| 200 | +# => [ |
| 201 | +# %UserProfile{ |
| 202 | +# id: "...", |
| 203 | +# online: true, |
| 204 | +# dark_mode: nil, |
| 205 | +# visibility: :public |
| 206 | +# } |
| 207 | +#] |
| 208 | +``` |
| 209 | + |
| 210 | +In databases where `:map`s are stored as JSONB (like Postgres), Ecto constructs the appropriate jsonpath queries for you. More examples of embedded schema queries are documented in [`json_extract_path/2`](https://hexdocs.pm/ecto/Ecto.Query.API.html#json_extract_path/2). |
0 commit comments