@orieljs/framework 0.1.0-alpha.1 → 0.1.0-alpha.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +7 -0
- package/README.md +10 -1
- package/TRADEMARKS.md +33 -0
- package/dist/{chunk-WI7E7Q46.js → chunk-24673JQF.js} +8 -4
- package/dist/chunk-24673JQF.js.map +1 -0
- package/dist/{chunk-K6MAA3RM.js → chunk-HTJKT2R2.js} +9 -1
- package/dist/chunk-HTJKT2R2.js.map +1 -0
- package/dist/chunk-KDE5BLI3.js +8776 -0
- package/dist/chunk-KDE5BLI3.js.map +1 -0
- package/dist/chunk-MURTCTRH.js +3253 -0
- package/dist/chunk-MURTCTRH.js.map +1 -0
- package/dist/contracts.d.ts +852 -168
- package/dist/contracts.js +9 -1
- package/dist/index.d-WQpfdIbG.d.ts +630 -0
- package/dist/index.d.ts +3015 -5160
- package/dist/index.js +7628 -2640
- package/dist/index.js.map +1 -1
- package/dist/{jsx-runtime-B8J6xq-7.d.ts → jsx-runtime-6um4Mwfb.d.ts} +99 -149
- package/dist/jsx-runtime.d.ts +3 -1
- package/dist/jsx-runtime.js +3 -3
- package/dist/testing-CKwfWJGn.d.ts +1586 -0
- package/dist/testing.d.ts +3 -1408
- package/dist/testing.js +29 -5
- package/docs/authentication.md +487 -0
- package/docs/authorization.md +435 -0
- package/docs/cache.md +647 -0
- package/docs/collections.md +2270 -0
- package/docs/configuration.md +334 -0
- package/docs/console.md +654 -0
- package/docs/container.md +430 -0
- package/docs/controllers.md +406 -0
- package/docs/csrf.md +126 -0
- package/docs/database-testing.md +144 -0
- package/docs/database.md +236 -0
- package/docs/deployment.md +371 -0
- package/docs/eloquent-collections.md +218 -0
- package/docs/eloquent-factories.md +255 -0
- package/docs/eloquent-mutators.md +254 -0
- package/docs/eloquent-observers.md +145 -0
- package/docs/eloquent-relationships.md +557 -0
- package/docs/eloquent.md +494 -0
- package/docs/encryption.md +104 -0
- package/docs/errors.md +318 -0
- package/docs/events.md +336 -0
- package/docs/export.md +93 -0
- package/docs/facades.md +278 -0
- package/docs/hashing.md +118 -0
- package/docs/helpers.md +1281 -0
- package/docs/http-tests.md +386 -0
- package/docs/inertia.md +485 -0
- package/docs/installation.md +219 -0
- package/docs/lifecycle.md +234 -0
- package/docs/live-tests.md +226 -0
- package/docs/live.md +910 -0
- package/docs/logging.md +255 -0
- package/docs/mail.md +619 -0
- package/docs/middleware.md +385 -0
- package/docs/migrations.md +416 -0
- package/docs/mocking.md +264 -0
- package/docs/navigation.json +277 -0
- package/docs/notifications.md +225 -0
- package/docs/pagination.md +247 -0
- package/docs/passwords.md +207 -0
- package/docs/providers.md +187 -0
- package/docs/queries.md +600 -0
- package/docs/queues.md +330 -0
- package/docs/requests.md +533 -0
- package/docs/responses.md +444 -0
- package/docs/routing.md +738 -0
- package/docs/scheduling.md +277 -0
- package/docs/seeding.md +122 -0
- package/docs/session.md +409 -0
- package/docs/starter-kits.md +399 -0
- package/docs/storage.md +192 -0
- package/docs/structure.md +264 -0
- package/docs/testing.md +165 -0
- package/docs/time-travel.md +138 -0
- package/docs/validation.md +1792 -0
- package/docs/verification.md +152 -0
- package/docs/views.md +566 -0
- package/docs/vite.md +400 -0
- package/package.json +37 -30
- package/dist/chunk-AI2RFMBT.js +0 -745
- package/dist/chunk-AI2RFMBT.js.map +0 -1
- package/dist/chunk-BZPCRKFQ.js +0 -2941
- package/dist/chunk-BZPCRKFQ.js.map +0 -1
- package/dist/chunk-K6MAA3RM.js.map +0 -1
- package/dist/chunk-WI7E7Q46.js.map +0 -1
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
# Eloquent: relationships
|
|
2
|
+
|
|
3
|
+
Tables relate to each other: a post has comments, a user has roles. Define each relation once, as a method of your
|
|
4
|
+
model, then query it, load it with its parent, and write through it.
|
|
5
|
+
|
|
6
|
+
## Introduction
|
|
7
|
+
|
|
8
|
+
A relation is a method of your model that returns the relation, such as `this.hasMany(Comment)`. Calling it gives a
|
|
9
|
+
query of the related models, with every [query builder](queries.md) method, already limited to this model's:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
declare const post: Post
|
|
13
|
+
// ---cut---
|
|
14
|
+
const comments = await post.comments().where('votes', '>', 5).get()
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
To have a relation's models with their parent, load it: `Post.with('comments')` (see [Eager loading](#eager-loading)).
|
|
18
|
+
|
|
19
|
+
## Defining relationships
|
|
20
|
+
|
|
21
|
+
### One to one / has one
|
|
22
|
+
|
|
23
|
+
A user has one phone. `hasOne()` takes the related model, and finds it by a `user_id` column on its table:
|
|
24
|
+
|
|
25
|
+
```ts title="app/Models/User.ts"
|
|
26
|
+
import { Model } from '@orieljs/framework'
|
|
27
|
+
import { Phone } from './Phone'
|
|
28
|
+
|
|
29
|
+
export class User extends Model {
|
|
30
|
+
/** Get the phone associated with the user. */
|
|
31
|
+
phone() {
|
|
32
|
+
return this.hasOne(Phone)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The foreign key is the model's name in snake case with `_id`. Give another as the second argument, and the column it
|
|
38
|
+
matches on this model, `id` unless you say, as the third: `this.hasOne(Phone, 'foreign_key', 'local_key')`.
|
|
39
|
+
|
|
40
|
+
#### Defining the inverse of the relationship
|
|
41
|
+
|
|
42
|
+
The phone belongs to its user. `belongsTo()` reads the phone's `user_id`, named after the related model:
|
|
43
|
+
|
|
44
|
+
```ts title="app/Models/Phone.ts"
|
|
45
|
+
import { Model } from '@orieljs/framework'
|
|
46
|
+
import { User } from './User'
|
|
47
|
+
|
|
48
|
+
export class Phone extends Model {
|
|
49
|
+
/** Get the user that owns the phone. */
|
|
50
|
+
user() {
|
|
51
|
+
return this.belongsTo(User)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`this.belongsTo(User, 'foreign_key', 'owner_key')` names the columns yourself.
|
|
57
|
+
|
|
58
|
+
> [!NOTE]
|
|
59
|
+
> Laravel names the foreign key after the method that calls `belongsTo()`, so an `author()` relation reads
|
|
60
|
+
> `author_id`. JavaScript can't see the calling method, so Oriel names it after the related model: an
|
|
61
|
+
> `author() { return this.belongsTo(User) }` reads `user_id`. Give the key: `this.belongsTo(User, 'author_id')`.
|
|
62
|
+
|
|
63
|
+
### One to many / has many
|
|
64
|
+
|
|
65
|
+
A post has many comments. `hasMany()` finds them by their `post_id`:
|
|
66
|
+
|
|
67
|
+
```ts title="app/Models/Post.ts"
|
|
68
|
+
import { Model } from '@orieljs/framework'
|
|
69
|
+
import { Comment } from './Comment'
|
|
70
|
+
|
|
71
|
+
export class Post extends Model {
|
|
72
|
+
/** Get the comments for the blog post. */
|
|
73
|
+
comments() {
|
|
74
|
+
return this.hasMany(Comment)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
const post = await Post.findOrFail(1)
|
|
81
|
+
|
|
82
|
+
const comments = await post.comments().get()
|
|
83
|
+
|
|
84
|
+
const comment = await post.comments().where('body', 'like', '%Oriel%').first()
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
#### One to many (inverse) / belongs to
|
|
88
|
+
|
|
89
|
+
```ts title="app/Models/Comment.ts"
|
|
90
|
+
import { Model } from '@orieljs/framework'
|
|
91
|
+
import { Post } from './Post'
|
|
92
|
+
|
|
93
|
+
export class Comment extends Model {
|
|
94
|
+
/** Get the post that owns the comment. */
|
|
95
|
+
post() {
|
|
96
|
+
return this.belongsTo(Post)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const comment = await Comment.findOrFail(1)
|
|
103
|
+
|
|
104
|
+
const post = await comment.post().first()
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
#### Automatically hydrating parent models on children
|
|
108
|
+
|
|
109
|
+
Loading a post's comments, then each comment's post, would query the post again for every comment. `chaperone()` on
|
|
110
|
+
the relation sets each loaded comment's `post` to the post it was loaded with, with no query:
|
|
111
|
+
|
|
112
|
+
```ts title="app/Models/Post.ts"
|
|
113
|
+
import { Model } from '@orieljs/framework'
|
|
114
|
+
import { Comment } from './Comment'
|
|
115
|
+
|
|
116
|
+
export class Post extends Model {
|
|
117
|
+
comments() {
|
|
118
|
+
return this.hasMany(Comment).chaperone()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Has many through
|
|
124
|
+
|
|
125
|
+
A has-many-through relation reaches models through another one. A project has environments, and each environment has
|
|
126
|
+
deployments, so a project's deployments are reached through its environments:
|
|
127
|
+
|
|
128
|
+
```ts title="app/Models/Project.ts"
|
|
129
|
+
// @filename: app/Models/Environment.ts
|
|
130
|
+
import { Model } from '@orieljs/framework'
|
|
131
|
+
export class Environment extends Model {}
|
|
132
|
+
// @filename: app/Models/Deployment.ts
|
|
133
|
+
import { Model } from '@orieljs/framework'
|
|
134
|
+
export class Deployment extends Model {}
|
|
135
|
+
// ---cut---
|
|
136
|
+
import { Model } from '@orieljs/framework'
|
|
137
|
+
import { Deployment } from './Deployment'
|
|
138
|
+
import { Environment } from './Environment'
|
|
139
|
+
|
|
140
|
+
export class Project extends Model {
|
|
141
|
+
/** Get all of the deployments for the project. */
|
|
142
|
+
deployments() {
|
|
143
|
+
return this.hasManyThrough(Deployment, Environment)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The keys are `project_id` on `environments` and `environment_id` on `deployments`; the next arguments name others.
|
|
149
|
+
|
|
150
|
+
## Many to many relationships
|
|
151
|
+
|
|
152
|
+
A user has many roles, and a role belongs to many users. The two tables meet in a third, the pivot table, named after
|
|
153
|
+
both models in alphabetical order: `role_user`, with `user_id` and `role_id`.
|
|
154
|
+
|
|
155
|
+
```ts title="app/Models/User.ts"
|
|
156
|
+
import { Model } from '@orieljs/framework'
|
|
157
|
+
import { Role } from './Role'
|
|
158
|
+
|
|
159
|
+
export class User extends Model {
|
|
160
|
+
/** The roles that belong to the user. */
|
|
161
|
+
roles() {
|
|
162
|
+
return this.belongsToMany(Role)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The role's side is the same: `this.belongsToMany(User)`. The next arguments name the pivot table and its keys:
|
|
168
|
+
`this.belongsToMany(Role, 'role_user', 'user_id', 'role_id')`.
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
declare const user: User
|
|
172
|
+
// ---cut---
|
|
173
|
+
const roles = await user.roles().orderBy('name').get()
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Retrieving intermediate table columns
|
|
177
|
+
|
|
178
|
+
Each model a many-to-many query returns has a `pivot`, a model with its pivot row's keys. `withPivot()` adds other
|
|
179
|
+
columns of the pivot table, and `withTimestamps()` its `created_at` and `updated_at`, which `attach()` then sets:
|
|
180
|
+
|
|
181
|
+
```ts title="app/Models/User.ts"
|
|
182
|
+
import { Model } from '@orieljs/framework'
|
|
183
|
+
import { Role } from './Role'
|
|
184
|
+
|
|
185
|
+
export class User extends Model {
|
|
186
|
+
roles() {
|
|
187
|
+
return this.belongsToMany(Role).withPivot('active', 'created_by').withTimestamps()
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```ts
|
|
193
|
+
declare const user: User
|
|
194
|
+
// ---cut---
|
|
195
|
+
for (const role of await user.roles().get()) {
|
|
196
|
+
console.log(role.pivot?.getAttribute('created_at'))
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Filtering queries via intermediate table columns
|
|
201
|
+
|
|
202
|
+
`wherePivot()` and `wherePivotIn()` filter by the pivot table's columns:
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
declare const user: User
|
|
206
|
+
// ---cut---
|
|
207
|
+
const active = await user.roles().wherePivot('active', 1).get()
|
|
208
|
+
|
|
209
|
+
const some = await user.roles().wherePivotIn('priority', [1, 2]).get()
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
> [!NOTE]
|
|
213
|
+
> Laravel's custom pivot models (`using()`), `as()` and `wherePivotBetween()` and its kin aren't in Oriel.
|
|
214
|
+
|
|
215
|
+
## Polymorphic relationships
|
|
216
|
+
|
|
217
|
+
A polymorphic relation lets a model belong to more than one kind of model. An image can belong to a user or to a
|
|
218
|
+
post: its `imageable_id` holds the owner's key, and `imageable_type` the owner's kind.
|
|
219
|
+
|
|
220
|
+
### One to one (polymorphic)
|
|
221
|
+
|
|
222
|
+
```ts title="app/Models/Image.ts"
|
|
223
|
+
import { Model } from '@orieljs/framework'
|
|
224
|
+
|
|
225
|
+
export class Image extends Model {
|
|
226
|
+
/** Get the parent imageable model (user or post). */
|
|
227
|
+
imageable() {
|
|
228
|
+
return this.morphTo('imageable')
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
```ts title="app/Models/Post.ts"
|
|
234
|
+
import { Model } from '@orieljs/framework'
|
|
235
|
+
import { Image } from './Image'
|
|
236
|
+
|
|
237
|
+
export class Post extends Model {
|
|
238
|
+
/** Get the post's image. */
|
|
239
|
+
image() {
|
|
240
|
+
return this.morphOne(Image, 'imageable')
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
The table needs the two columns, which `table.morphs('imageable')` adds in a migration. `morphTo()` takes the relation's
|
|
246
|
+
name, from which the columns are named. The name is required: Laravel reads it from the method that calls `morphTo()`,
|
|
247
|
+
which JavaScript can't see.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
const post = await Post.findOrFail(1)
|
|
251
|
+
|
|
252
|
+
const image = await post.image().first()
|
|
253
|
+
|
|
254
|
+
const owner = await (await Image.findOrFail(1)).imageable().first()
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### One to many (polymorphic)
|
|
258
|
+
|
|
259
|
+
`morphMany()` gives a model many of them: `this.morphMany(Comment, 'commentable')`.
|
|
260
|
+
|
|
261
|
+
### Custom polymorphic types
|
|
262
|
+
|
|
263
|
+
The `_type` column holds the owner's class name, `Post`, unless you map names to models. A map keeps your data from
|
|
264
|
+
depending on your class names:
|
|
265
|
+
|
|
266
|
+
```ts title="app/Providers/AppServiceProvider.ts"
|
|
267
|
+
import { Relation, ServiceProvider } from '@orieljs/framework'
|
|
268
|
+
import { Post } from '../Models/Post'
|
|
269
|
+
import { User } from '../Models/User'
|
|
270
|
+
|
|
271
|
+
export class AppServiceProvider extends ServiceProvider {
|
|
272
|
+
override boot(): void {
|
|
273
|
+
Relation.morphMap({ post: Post, user: User })
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
> [!NOTE]
|
|
279
|
+
> Laravel's many-to-many polymorphic relations (`morphToMany()`, `morphedByMany()`), `hasOneThrough()`, the
|
|
280
|
+
> "one of many" relations (`latestOfMany()`) and default models (`withDefault()`) aren't in Oriel.
|
|
281
|
+
|
|
282
|
+
## Querying relations
|
|
283
|
+
|
|
284
|
+
### Relationship methods vs. reading a relation
|
|
285
|
+
|
|
286
|
+
In Laravel, `$post->comments` reads a relation as a property. In Oriel, `post.comments` is the relation method
|
|
287
|
+
itself, as it is on any class, so reading through it, `post.author.name`, gives you a property of the function, with
|
|
288
|
+
no error. Read a relation one of two ways:
|
|
289
|
+
|
|
290
|
+
- **Query it:** call the method, and run the query: `await post.author().first()`, or
|
|
291
|
+
`await post.comments().get()`.
|
|
292
|
+
- **Load it first, then read it:** `Post.with('author')` or `post.load('author')`, then `post.getRelation('author')`,
|
|
293
|
+
typed from the method: a `User` or `null` for a belongs-to, a collection for a has-many.
|
|
294
|
+
|
|
295
|
+
```ts
|
|
296
|
+
const post = await Post.with('author').findOrFail(1)
|
|
297
|
+
|
|
298
|
+
const author = post.getRelation('author') // User | null
|
|
299
|
+
|
|
300
|
+
const comments = await post.comments().get()
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
`getRelation()` on a relation that isn't loaded throws a `RelationNotLoadedError`, which says how to load it.
|
|
304
|
+
|
|
305
|
+
> [!WARNING]
|
|
306
|
+
> Outside production, reading a property of a relation method, `post.author.name`, prints a warning once, with how
|
|
307
|
+
> to read the relation. `Model.warnOnRelationPropertyAccess(false)`, in a service provider's `boot()`, turns the
|
|
308
|
+
> warning off.
|
|
309
|
+
|
|
310
|
+
### Querying relationship existence
|
|
311
|
+
|
|
312
|
+
`has()` limits a query to the models that have related models, at least one or as many as you say. A dotted name
|
|
313
|
+
reaches through relations:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
const posts = await Post.has('comments').get()
|
|
317
|
+
|
|
318
|
+
const popular = await Post.has('comments', '>=', 3).get()
|
|
319
|
+
|
|
320
|
+
const writers = await User.has('posts.comments').get()
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`whereHas()` adds conditions on the related models:
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const posts = await Post.whereHas('comments', (query) => {
|
|
327
|
+
query.where('body', 'like', 'code%')
|
|
328
|
+
}).get()
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
`orHas()` and `orWhereHas()` join with `or`.
|
|
332
|
+
|
|
333
|
+
### Querying relationship absence
|
|
334
|
+
|
|
335
|
+
`doesntHave()` and `whereDoesntHave()` are their opposites:
|
|
336
|
+
|
|
337
|
+
```ts
|
|
338
|
+
const quiet = await Post.doesntHave('comments').get()
|
|
339
|
+
|
|
340
|
+
const clean = await Post.whereDoesntHave('comments', (query) => {
|
|
341
|
+
query.where('body', 'like', 'spam%')
|
|
342
|
+
}).get()
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
`has()` and its kin don't work on a `morphTo()` relation, whose related table isn't known in advance.
|
|
346
|
+
|
|
347
|
+
## Aggregating related models
|
|
348
|
+
|
|
349
|
+
### Counting related models
|
|
350
|
+
|
|
351
|
+
`withCount()` adds a `<relation>_count` attribute with the number of related models, without loading them:
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
const posts = await Post.withCount('comments').get()
|
|
355
|
+
|
|
356
|
+
for (const post of posts) {
|
|
357
|
+
console.log(post.getAttribute('comments_count'))
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
`as` names the attribute, and an object gives each count its conditions:
|
|
362
|
+
|
|
363
|
+
```ts
|
|
364
|
+
const posts = await Post.withCount('comments as comment_total')
|
|
365
|
+
.withCount({ 'comments as popular_count': (query) => query.where('votes', '>', 5) })
|
|
366
|
+
.get()
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
#### Deferred count loading
|
|
370
|
+
|
|
371
|
+
`loadCount()` counts on a model you already have:
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
const post = await Post.findOrFail(1)
|
|
375
|
+
|
|
376
|
+
await post.loadCount('comments')
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
### Other aggregate functions
|
|
380
|
+
|
|
381
|
+
`withSum()`, `withAvg()`, `withMin()` and `withMax()` add `<relation>_<function>_<column>`, and `withExists()` adds
|
|
382
|
+
`<relation>_exists`:
|
|
383
|
+
|
|
384
|
+
```ts
|
|
385
|
+
const posts = await Post.withSum('comments', 'votes').withExists('comments').get()
|
|
386
|
+
|
|
387
|
+
for (const post of posts) {
|
|
388
|
+
console.log(post.getAttribute('comments_sum_votes'), post.getAttribute('comments_exists'))
|
|
389
|
+
}
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`loadSum()`, `loadAvg()`, `loadMin()`, `loadMax()` and `loadExists()` add them to a model you have.
|
|
393
|
+
|
|
394
|
+
## Eager loading
|
|
395
|
+
|
|
396
|
+
Reading the author of each of 25 posts, one at a time, runs 26 queries: one for the posts, then one per author.
|
|
397
|
+
`with()` loads the authors of all of them in one more query:
|
|
398
|
+
|
|
399
|
+
```ts
|
|
400
|
+
const posts = await Post.with('author').get()
|
|
401
|
+
|
|
402
|
+
for (const post of posts) {
|
|
403
|
+
console.log(post.getRelation('author')?.name)
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
#### Eager loading multiple relationships
|
|
408
|
+
|
|
409
|
+
```ts
|
|
410
|
+
const posts = await Post.with('author', 'comments').get()
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
#### Nested eager loading
|
|
414
|
+
|
|
415
|
+
A dotted name loads a relation of a relation:
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
const posts = await Post.with('comments.post').get()
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
#### Eager loading specific columns
|
|
422
|
+
|
|
423
|
+
```ts
|
|
424
|
+
const posts = await Post.with('author:id,name').get()
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
List the key the relation matches on, here `id`, or the models can't be matched to their parents.
|
|
428
|
+
|
|
429
|
+
#### Eager loading by default
|
|
430
|
+
|
|
431
|
+
`with` on the model loads its relations for every query, and `withCount` counts them:
|
|
432
|
+
|
|
433
|
+
```ts
|
|
434
|
+
export class Post extends Model {
|
|
435
|
+
protected override with = ['author']
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
`without()` leaves a default relation out of one query.
|
|
440
|
+
|
|
441
|
+
### Constraining eager loads
|
|
442
|
+
|
|
443
|
+
Give `with()` an object to add conditions to a relation's query:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
const posts = await Post.with({ comments: (query) => query.where('body', 'like', '%code%') }).get()
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### Lazy eager loading
|
|
450
|
+
|
|
451
|
+
`load()` loads relations on a model you already have, and `loadMissing()` loads only those that aren't loaded yet:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
const post = await Post.findOrFail(1)
|
|
455
|
+
|
|
456
|
+
if (post.getAttribute('author_id') !== null) {
|
|
457
|
+
await post.load('author', 'comments')
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
await post.loadMissing('comments')
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
> [!NOTE]
|
|
464
|
+
> Laravel's `preventLazyLoading()` isn't in Oriel: since a relation is read only once loaded, or by running its query,
|
|
465
|
+
> nothing loads behind your back.
|
|
466
|
+
|
|
467
|
+
## Inserting and updating related models
|
|
468
|
+
|
|
469
|
+
### The `save` method
|
|
470
|
+
|
|
471
|
+
`save()` on a has-many relation sets the model's foreign key and saves it, and `saveMany()` does it for several:
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
const comment = Comment.make({ body: 'A new comment.' })
|
|
475
|
+
|
|
476
|
+
const post = await Post.findOrFail(1)
|
|
477
|
+
|
|
478
|
+
await post.comments().save(comment)
|
|
479
|
+
|
|
480
|
+
await post.comments().saveMany([Comment.make({ body: 'Another new comment.' }), Comment.make({ body: 'Another.' })])
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
### The `create` method
|
|
484
|
+
|
|
485
|
+
`create()` makes the related model with the foreign key set and saves it, and `createMany()` makes several:
|
|
486
|
+
|
|
487
|
+
```ts
|
|
488
|
+
const post = await Post.findOrFail(1)
|
|
489
|
+
|
|
490
|
+
const comment = await post.comments().create({ body: 'A new comment.' })
|
|
491
|
+
|
|
492
|
+
await post.comments().createMany([{ body: 'A new comment.' }, { body: 'Another new comment.' }])
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
### Belongs to relationships
|
|
496
|
+
|
|
497
|
+
`associate()` sets a model's foreign key to its new parent, and `dissociate()` clears it. Save the model after:
|
|
498
|
+
|
|
499
|
+
```ts
|
|
500
|
+
const user = await User.findOrFail(10)
|
|
501
|
+
const phone = await Phone.findOrFail(1)
|
|
502
|
+
|
|
503
|
+
phone.user().associate(user)
|
|
504
|
+
await phone.save()
|
|
505
|
+
|
|
506
|
+
phone.user().dissociate()
|
|
507
|
+
await phone.save()
|
|
508
|
+
```
|
|
509
|
+
|
|
510
|
+
### Many to many relationships
|
|
511
|
+
|
|
512
|
+
#### Attaching / detaching
|
|
513
|
+
|
|
514
|
+
`attach()` adds a row to the pivot table, with any extra pivot columns, and `detach()` removes one, several, or every
|
|
515
|
+
row of this model:
|
|
516
|
+
|
|
517
|
+
```ts
|
|
518
|
+
const user = await User.findOrFail(1)
|
|
519
|
+
|
|
520
|
+
await user.roles().attach(1, { expires: new Date('2026-12-31') })
|
|
521
|
+
|
|
522
|
+
await user.roles().detach(1)
|
|
523
|
+
|
|
524
|
+
await user.roles().detach()
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
#### Syncing associations
|
|
528
|
+
|
|
529
|
+
`sync()` leaves exactly the ids you give attached, adding and removing rows, and returns what it changed.
|
|
530
|
+
`syncWithoutDetaching()` only adds, and `toggle()` attaches the ids that aren't attached and detaches those that are:
|
|
531
|
+
|
|
532
|
+
```ts
|
|
533
|
+
declare const user: User
|
|
534
|
+
// ---cut---
|
|
535
|
+
const changes = await user.roles().sync([1, 2, 3]) // { attached: [...], detached: [...], updated: [...] }
|
|
536
|
+
|
|
537
|
+
await user.roles().syncWithoutDetaching([1, 2, 3])
|
|
538
|
+
|
|
539
|
+
await user.roles().toggle([1, 2, 3])
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
#### Updating a record on the intermediate table
|
|
543
|
+
|
|
544
|
+
```ts
|
|
545
|
+
declare const user: User
|
|
546
|
+
// ---cut---
|
|
547
|
+
await user.roles().updateExistingPivot(1, { active: false })
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
> [!NOTE]
|
|
551
|
+
> Laravel's `touches`, which updates a parent's `updated_at` when a child changes, isn't in Oriel: `touch()` the parent
|
|
552
|
+
> yourself.
|
|
553
|
+
|
|
554
|
+
## Serializing relations
|
|
555
|
+
|
|
556
|
+
A model's `toArray()` and its JSON include the relations it has loaded, under their names in snake case
|
|
557
|
+
(`blogPosts` as `blog_posts`), as their models' own arrays. A relation that isn't loaded isn't there.
|