skip to content

Querying & Syncing Relations

Filtering by related rows with whereHas, counting them with withCount, and writing through relations with attach, sync and associate. Interviewers probe what sync deletes and when a query runs.

on this pageshow

explore

questions

6

In an Eloquent model, what is the difference between $photo->tags and $photo->tags(), and when does each run a query?

level: juniorimportance: must knowfreq 66%

answer

  1. one is a builder, one is results
  2. property result cached on the model
  3. method: new query every terminal call
  4. count() in SQL vs in PHP
  5. writes go through the method

basics

~20 s

$photo->tags() returns the relation object, a query builder that runs a fresh query on each terminal call. $photo->tags returns the loaded results, querying once on first access and then reusing the collection cached on the model.

solid answer

~40 s

Calling the method, `$photo->tags()`, returns a `BelongsToMany` relation: a query builder already constrained to this photo. Nothing runs until a terminal call such as `get()`, `count()` or `first()`, and every terminal call runs a new query — which lets you add constraints like `->where('slug', 'sunset')`. Reading the property, `$photo->tags`, goes through the model's attribute lookup: if the relation is already loaded it returns the cached value, otherwise it runs the relation query once, stores the result on the model and returns it — a `Collection` for to-many relations, a model or `null` for to-one. So `$photo->tags()->count()` runs a `COUNT` query, while `$photo->tags->count()` loads every tag and counts in PHP. Writes such as `attach()`, `sync()` or `associate()` always go through the method.

go deeper

for a junior

Recall that the method returns a query builder and the property returns results, and that writes always use the method.

for a middle

Explain how the property lookup checks the loaded relations first, queries once and caches, and why count() differs between the two forms.

for a senior

Point to real costs: loading a huge collection just to count it, stale cached relations after writes, and repeated method calls in views.

for a principal

Set team conventions on when relation results may be cached on a model and when code must query explicitly, and how reviews spot the difference.

## Two spellings, two different things An Eloquent relation is declared as a method on the model: ```php public function tags(): BelongsToMany { return $this->belongsToMany(Tag::class); } ``` That single method can be reached in two ways, and they return different things. | Expression | What you get | When SQL runs | |---|---|---| | `$photo->tags()` | A `BelongsToMany` **relation object**, a query builder scoped to this photo | Only on a terminal call (`get()`, `count()`, `first()`, `exists()`, `pluck()`), and **again on every** such call | | `$photo->tags` | The **results**: an Eloquent `Collection` of `Tag` models (for to-one relations, a model or `null`) | Once, on first access if not already loaded; later reads reuse the cached value | ## How the property form works A model has no real `tags` property. Reading `$photo->tags` falls through to the model's attribute lookup. In outline, the framework's `getRelationValue()` then does this: 1. If the relation is already in the model's loaded relations, return it without querying. 2. If `tags` is not a relation method, return `null`. 3. Otherwise call `tags()`, run the relation query, store the result on the model with `setRelation()`, and return it. Step 3 means the **first** read queries and every later read on the same model instance is free. It also means the value is a **snapshot**: after `$photo->tags()->attach(9)`, an already-loaded `$photo->tags` does not contain tag 9 until the relation is reloaded. If the relation method forgets its `return`, the property form throws a `LogicException` saying the method must return a relationship instance. ## How the method form works `$photo->tags()` builds a fresh relation object every time you call it. It behaves like a query builder with the join and the `photo_id` constraint already applied, so you can keep chaining: - `$photo->tags()->where('slug', 'like', 'sun%')->get()` filters in SQL and returns only the matching tags. - `$photo->tags()->orderBy('name')->pluck('name')` reads one column. - `$photo->tags()->exists()` asks the database a yes/no question. None of this touches the cached `tags` value on the model. ## Where the difference matters - **Counting.** `$photo->tags()->count()` sends a `COUNT` query. `$photo->tags->count()` loads every tag row and counts the collection in PHP. With five tags nobody notices; with fifty thousand likes on a popular photo, the second loads fifty thousand models. If the collection is **already loaded**, though, counting it in PHP costs nothing. - **Filtering.** `$photo->tags()->where(...)` filters in the database. `$photo->tags->where(...)` is a `Collection` method that filters rows already in memory — correct, but only after loading all of them. - **Writing.** Every write goes through the method: `$photo->tags()->attach()`, `sync()`, `detach()`, `$photo->album()->associate()`, `$album->photos()->create([...])`. The property is a plain collection or model; it has no `attach()`. - **Repeated reads.** In a Blade view that prints `$photo->tags` three times, the property form queries once. Writing `$photo->tags()->get()` three times queries three times. ## The rule to remember Use the **property** when you want the related records and are happy for them to be cached on this model. Use the **method** when you want to add constraints, ask an aggregate question in SQL, or write through the relation. Loading relations for many parents at once is a separate subject (eager loading), and so is making lazy property reads fail in development. ## Inspecting and resetting the cache The cached value lives in the model's relations array, and a few public methods work with it directly: - `$photo->relationLoaded('tags')` returns `true` once the property has been read (or the relation was loaded some other way), which is handy in a view that should not trigger a query. - `$photo->setRelation('tags', $collection)` stores a value you already have, so a later `$photo->tags` returns it without SQL. - `$photo->unsetRelation('tags')` forgets the cached value; the next property read queries again. This is the cheapest way to make a stale relation fresh after a write. ## A worked example On a photo detail page: 1. `$photo->tags` in the header runs one query and caches five tags. 2. `$photo->tags` in the sidebar costs nothing. 3. `$photo->tags()->where('featured', true)->get()` in the footer runs a new, filtered query and does **not** replace the cached five. 4. `$photo->tags()->attach($newTagId)` writes a pivot row; the cached collection still has five tags until you unset or reload it.

  • After $photo->tags()->sync([2, 4]), why might $photo->tags still show the old tags?
    The property returns whatever was cached on the model the first time it was read. `sync()` writes the pivot table through the relation object and never updates that cached collection. If `$photo->tags` had been read before the sync, you must reload the relation to see the new set; if it had not been read yet, the first read after the sync queries fresh data.
  • What does $photo->album return when the photo has no album?
    For a to-one relation such as `belongsTo`, the property returns the related model or `null` when no row matches, so `$photo->album->title` would fail with an access on null. To-many relations never return `null`: `$photo->tags` is an empty `Collection` when nothing is attached.

The method is a library request slip already filled in with this photo's ID: each time you hand it in, the library fetches again, and you may add conditions first. The property is the stack of books already on your desk: fetched the first time, then simply picked up again.

saying these in an interview costs you the question

  • Saying $photo->tags runs a new query on every access
  • Believing $photo->tags() returns the loaded Collection of tags
  • Calling attach() or sync() on the $photo->tags property
  • Thinking $photo->tags->count() and $photo->tags()->count() always cost the same
  • Expecting the loaded property to update itself after sync()
open as a page

In Eloquent many-to-many relations, how do attach(), detach(), sync(), syncWithoutDetaching() and toggle() differ, and what does sync() delete?

level: middleimportance: must knowfreq 63%

basics

~20 s

All five write pivot rows, never Tag rows. attach() inserts rows without checking for duplicates, detach() deletes them (every row when called without IDs), sync() makes the pivot match the given list exactly, syncWithoutDetaching() only adds, and toggle() flips each ID.

open as a page

With Eloquent, how do whereHas(), whereRelation(), doesntHave() and whereBelongsTo() filter photos by related rows, and what SQL do they produce?

level: middleimportance: must knowfreq 60%

basics

~20 s

They filter parents by related rows without loading them. whereHas() adds a WHERE EXISTS subquery (COUNT when given a count), doesntHave() adds NOT EXISTS, whereRelation() is whereHas() with one where, and whereBelongsTo() adds a foreign-key WHERE IN.

open as a page

In Eloquent, what does associate() do on a belongsTo relation such as $photo->album(), and why must you still call save()?

level: juniorimportance: should knowfreq 44%

basics

~20 s

associate() sets the child's foreign key (photos.album_id) to the parent's key and puts the parent in the child's loaded relations, all in memory. It runs no SQL, so the change reaches the database only when save() is called on the child.

open as a page

With Eloquent, what do withCount(), withSum() and withExists() add to a query, and how are the resulting attributes named?

level: middleimportance: should knowfreq 52%

basics

~20 s

They add a correlated subselect to the SELECT list, so each parent row comes back with an aggregate attribute and no related models are loaded: tags_count, purchases_sum_amount, or a boolean comments_exists. An 'as' alias renames it.

open as a page

A Laravel photo site's tag editor calls $photo->tags()->sync($request->input('tags', [])); why can tags vanish or half-update, and how do you harden it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

A missing tags field becomes [], and sync([]) detaches every tag. sync() also runs several statements without a transaction, so a bad ID can fail after links are deleted. Sync only when the field is present, validate IDs, use syncOrFail().

open as a page