In Eloquent, what do latestOfMany() and ofMany() add to a hasOne relation, and why prefer them over hasMany()->latest()->first()?
answer
- has-one-of-many
- latestOfMany defaults to the id column
- ofMany('score', 'max')
- grouped subquery join
- eager loadable for many parents
basics
~20 slatestOfMany(), oldestOfMany() and ofMany() turn a hasOne into the single latest, oldest or best row of a has-many, resolved by a grouped subquery join. Unlike latest()->first(), it is a real relation that can be eager loaded for many parents at once.
solid answer
~40 sThey define a **has-one-of-many** relation: `hasOne(Submission::class)->latestOfMany()` returns the related row with the highest primary key, or of a column you pass, `oldestOfMany()` the lowest, and `ofMany('score', 'max')` the row with the largest or smallest value of any sortable column. Eloquent resolves them by joining a subquery that groups by the foreign key and takes the aggregate, so the relation works for many parents at once. That is the difference from `$student->submissions()->latest()->first()`, which is a query per student and cannot be eager loaded for a list. `ofMany()` also takes an array of columns for tie-breaking and a closure for extra conditions, and `->one()` converts an existing has-many.
go deeper
Recall that latestOfMany() on a hasOne gives you the newest related row as a relation.
Explain the grouped subquery that lets one-of-many relations be eager loaded, and how ofMany() takes a column, an aggregate and tie-breakers.
Use one-of-many relations for list pages to avoid per-row queries, and plan tie-breakers and key types that the aggregate can handle.
Decide when to denormalise a latest pointer onto the parent instead of computing it with an aggregate join on every list.
## The problem A student has many submissions, and a dashboard wants "each student's latest submission". The tempting code is: ```php foreach ($students as $student) { $latest = $student->submissions()->latest()->first(); } ``` That works, but it is one query per student, and it cannot be eager loaded, because `latest()->first()` is a query you run, not a relation you declare. Eloquent's **has-one-of-many** relations turn "the single best row of a has-many" into a real relation. ## The three methods They are called on a `hasOne()` (or `morphOne()`, or `hasOneThrough()`) relation: ```php class Student extends Model { public function latestSubmission(): HasOne { return $this->hasOne(Submission::class)->latestOfMany(); } public function firstSubmission(): HasOne { return $this->hasOne(Submission::class)->oldestOfMany(); } public function bestSubmission(): HasOne { return $this->hasOne(Submission::class)->ofMany('score', 'max'); } } ``` - `latestOfMany()` picks the row with the **highest** value of a column, the primary key `id` by default; pass a column such as `submitted_at` to change it. - `oldestOfMany()` picks the **lowest**. - `ofMany($column, $aggregate)` picks by any sortable column with `min` or `max`. If a `submissions()` has-many already exists, `$this->submissions()->one()->latestOfMany()` converts it instead of restating the keys. ## How it is resolved Under the hood the relation joins the related table to a **subquery** that groups by the foreign key and selects the aggregate, for example the maximum `id` per `student_id`. Because the grouping works for any number of parents at once, the relation can be eager loaded for a whole page of students in a single extra query, which the per-student `latest()->first()` loop never can. ## Ties and extra constraints Aggregates can tie: two submissions with the same `score`. Eloquent breaks ties with the related model's **primary key** automatically: a single column is paired with the key under the same aggregate, and an array without the key gets `MAX` of the key appended. To choose the order yourself, `ofMany()` accepts an **array** of columns applied in order, plus a closure for extra conditions: ```php public function currentGrade(): HasOne { return $this->hasOne(Grade::class)->ofMany([ 'published_at' => 'max', 'id' => 'max', ], function ($query) { $query->where('published_at', '<', now()); }); } ``` That reads: the most recently published grade, excluding ones scheduled for the future, and on a tie the highest id. ## Pitfalls | Pitfall | Effect | |---|---| | Using `latestOfMany()` with a non-sortable key | wrong or failing aggregate; the docs note PostgreSQL cannot run `MAX` on UUID columns, so one-of-many does not work with PostgreSQL UUID keys | | Expecting the relation to update itself after inserting a new submission | a relation already loaded on the model keeps its old value until reloaded | | Passing an aggregate other than `min` or `max` | `ofMany()` throws `InvalidArgumentException` | | Declaring it on `hasMany()` instead of `hasOne()` | the one-of-many methods live on the single-result relations | ## When to use it Use a one-of-many relation whenever "the latest", "the first" or "the best" related row is shown for **lists** of parents, such as dashboards, tables and API collections, because that is where eager loading pays. For a single parent on a detail page, `latest()->first()` on the has-many is fine. ## Reading the declaration A one-of-many declaration has three parts, and naming them makes the intent reviewable: 1. **The base relation**: `hasOne(Submission::class)`, or `submissions()->one()`, which fixes the keys. 2. **The ranking**: `latestOfMany('submitted_at')`, `oldestOfMany()` or `ofMany([...])`, which decides which row wins. 3. **The filter**: the optional closure, which decides which rows may compete at all. Because the result is an ordinary `HasOne`, everything that works on a has-one works here: - eager loading with `Student::with('latestSubmission')`; - existence checks and counts through the usual relation-query methods; - `withDefault()` when a student with no submissions should still render. The `morphOne()` form covers polymorphic children, such as the latest comment on a course or a lesson, and `hasOneThrough()` accepts the same methods, so "the latest submission for a course" can be declared through its lessons.
- How are ties broken in an Eloquent one-of-many relation, and how do you change the rule?By default Eloquent adds the related primary key as a final tie-breaker, with the same aggregate for a single column or `MAX` when you pass an array that omits it. To set your own order, pass an array such as `['submitted_at' => 'max', 'score' => 'max']`; a closure as the second argument adds constraints such as excluding future-dated rows.
- You already have `submissions(): HasMany`. How do you declare the latest one without repeating the keys?Call `one()` on it: `return $this->submissions()->one()->latestOfMany();`. `one()` converts the has-many into a has-one with the same keys, and the one-of-many method then narrows it to a single row.
saying these in an interview costs you the question
- latestOfMany() orders by created_at by default
- hasMany()->latest()->first() can be eager loaded for a list of students
- ofMany() is called on a hasMany relation
- One-of-many relations run one query per parent
- ofMany() accepts any aggregate such as avg or count