laravelmastery
SnippetsArticlesSeriesNews

Eloquent, properly · Lesson 2 of 3

Eloquent, properly · Lesson 2

One to many, without the surprises

Connect itEloquentQueries

The cast stores a line's amount as pence. hasMany is how those lines belong to this invoice. The foreign key lives on the lines table. Create lines through the relationship so invoice_id is set for you. A line created any other way is a row you hoped belonged to this invoice.

Video

01 The parent owns the collection

The method sits on Invoice, and the name is the word the team uses. lines() returns the query for this invoice's children. It does not load them until you ask.

Example.phpPHP
class Example
{
    public function lines(): HasMany
    {
        return $this->hasMany(InvoiceLine::class);
    }
}

Saving through the relationship is the difference between a line that belongs to this invoice and a line whose invoice_id you hoped you set. create() fills the foreign key and inserts the row. sum() on the relationship asks the database, so the total includes the line you just wrote.

PHP
$invoice->lines()->create([
    'description' => 'Hosting',
    'amount' => 2400,
]);

$total = $invoice->lines()->sum('amount');

orderBy on the relationship is fine when every caller wants the same order. A scope is better when only one screen wants lines sorted by amount. Do not add a default order that forces a filesort on a report that never displays that order. The report pays for the sort on every query, including the ones that only sum the amount.

Deleting the invoice should decide what happens to the lines. If the migration cascades, the database does it. If it does not, delete the lines in the same transaction as the invoice, or you will leave rows that point at nothing. Eloquent does not delete children because a hasMany method exists. The foreign key, or a transaction you wrote, does that work.

02 The loaded collection misses a line you just created

lines() and lines are not the same thing. The method returns a query. The property returns the collection already loaded on the model, or lazy-loads it once. create() writes a row and does not push that row onto a collection you loaded earlier. A total taken from the property can ignore the line you just added.

PHP
$invoice->load('lines');

$invoice->lines()->create([
    'description' => 'Support',
    'amount' => 800,
]);

$fromMemory = $invoice->lines->sum('amount');
$fromQuery = $invoice->lines()->sum('amount');

$fromQuery includes Support. $fromMemory does not, until you refresh the relation. unsetRelation('lines') drops the stale collection so the next read of the property loads again. refresh() reloads the invoice and its loaded relations, which is the heavier tool when you also need columns that changed in the database.

createMany() has the same gap. It inserts each line with the foreign key set, and the in-memory collection stays where it was. Use it when a form posts several lines and you want one foreign key for all of them. Then read the total from lines(), or reload, before you show the invoice back to the team.

A view that received the invoice after load('lines') is showing that snapshot. If the action creates a line and returns the same instance, the view is the stale one. Reload before you render, or build the response from the query. The relationship write is still the right insert. The property is only a cache of an earlier read.

Recap

You can create an invoice's lines through the relationship so invoice_id is set for you, and you can avoid totalling a collection that does not have the new row yet.

  • hasMany lives on the invoice. The foreign key lives on the lines.
  • A default order on the relationship costs every query, including reports that do not display it.
  • Deleting the invoice has to decide what happens to the lines.
  • A loaded lines collection does not gain rows that lines()->create() just inserted.

Check yourself · 3 questions

Question 1 of 3A line is created with InvoiceLine::create() and no invoice_id. What do you have?

A line now points at its invoice. Next, the invoice points back at its team.

← PreviousCasts, enums and custom casters

On this page

  • The parent owns the collection
  • The loaded collection misses a line you just created
  • Recap
  • Check yourself

Eloquent, properly

  • 1 Overview

Shape the data

  • 2 Models that match the domain
  • 3 Migrations you can still read next year
  • 4 Casts, enums and custom casters

Connect it

  • 5 One to many, without the surprises
  • 6 Belongs-to and whereBelongsTo
  • 7 Many to many and pivot data
  • 8 Has many through

Query it safely and fast

  • 9 Local and global scopes
  • 10 Eager loading on purpose
  • 11 Catching N+1 queries
  • 12 Indexes, select and chunking
  • 13 API resources without leaking attributes

Eloquent, properly

Lessons

  1. 01Overview

Shape the data

  1. 02Models that match the domain
  2. 03Migrations you can still read next year
  3. 04Casts, enums and custom casters

Connect it

  1. 05One to many, without the surprises
  2. 06Belongs-to and whereBelongsTo
  3. 07Many to many and pivot data
  4. 08Has many through

Query it safely and fast

  1. 09Local and global scopes
  2. 10Eager loading on purpose
  3. 11Catching N+1 queries
  4. 12Indexes, select and chunking
  5. 13API resources without leaking attributes

Get the Friday email

One snippet and the week’s Laravel news, in your inbox every Friday morning. Unsubscribe whenever you like.

laravelmastery

Practical Laravel learning for working developers, made in the UK.

Learn

  • Snippets
  • Articles
  • Series

Stay current

  • News
  • Friday email
  • RSS feed

Company

  • About
  • Privacy
  • Terms
© 2026 Laravel Mastery. Independent and not affiliated with Laravel Holdings Inc.Designed and built by Half Shell Studios

We remember the videos you watch, where you left off, and which series lessons you mark as read, against this connection’s address. Nothing is stored until you agree.