laravelmastery
SnippetsArticlesSeriesNews

Eloquent, properly · Lesson 1 of 3

Eloquent, properly · Lesson 1

Migrations you can still read next year

Shape the dataEloquentQueries

The model named number, issued date and status, and left the team id off the form. A migration stores those columns for the person who runs it in production, including you. Constrain the foreign key in the same migration that adds the column. The staff note is introduced here, as notes, and comes back in the indexes and API lessons.

Video

01 One invoice belongs to one team

foreignId()->constrained() is the readable form. foreignId() adds the unsigned bigint. constrained() adds the foreign key, and it assumes the related table is teams. MySQL creates the index that foreign key needs. Laravel does not add a second index() for the column. cascadeOnDelete() is a decision, not a default: deleting a team should delete its invoices only when the product says so. Leave it off and the database refuses to delete a team that still has invoices. That refusal is often the better letter.

Example.phpPHP
class Example
{
    public function up(): void
    {
        Schema::create('invoices', function (Blueprint $table): void {
            $table->id();
            $table->foreignId('team_id')->constrained()->cascadeOnDelete();
            $table->string('number', 32);
            $table->date('issued_on');
            $table->string('status', 16)->default('draft');
            $table->text('notes')->nullable();
            $table->timestamps();

            $table->unique(['team_id', 'number']);
        });
    }
}

The unique pair is the rule the domain already has: a team does not issue the same invoice number twice. Put that in the database, not only in a validator. Two requests can pass a unique rule in PHP and still insert two rows. The index is the constraint that still holds when the validator is skipped, queued, or wrong.

down() should undo the same change. If up() created the table, down() drops it. A migration that only added a column drops that column. Do not leave down() empty because rolling back feels unlikely. The method is how you read the change backwards.

Example.phpPHP
class Example
{
    public function down(): void
    {
        Schema::dropIfExists('invoices');
    }
}

Once this migration has run in production, stop editing it. A new column is a new file with a later timestamp. Editing an old up() does nothing on a database that already ran it, and it rewrites history for everyone who has not migrated yet.

02 Add the lines in the next migration

An invoice is not finished until it has lines. The lines table is the next letter, not a quiet edit to the invoices migration. invoice_id uses the same foreignId()->constrained() form, so a line cannot point at an invoice that does not exist. cascadeOnDelete() here matches the product choice from the one-to-many lesson: deleting the invoice deletes its lines. amount is an unsigned integer because the column stores pence, not a float.

PHP
Schema::create('invoice_lines', function (Blueprint $table): void {
    $table->id();
    $table->foreignId('invoice_id')->constrained()->cascadeOnDelete();
    $table->string('description');
    $table->unsignedInteger('amount');
    $table->timestamps();
});

down() for this file drops invoice_lines and leaves invoices alone. The two tables were created by two migrations, so they roll back separately. Name the class for the change, AddInvoiceLinesTable or whatever your prefix already uses, and resist a class called UpdateStuff. The filename is the index a tired person searches at ten at night.

If a team deletion must not remove invoices, take cascadeOnDelete() off team_id and let the database raise the foreign key error. Make that change in a new migration that drops the foreign key and adds it again without the cascade. The original file stays as the letter you sent.

Recap

You can add the team foreign key and the unique invoice number in one migration, then add lines in the next.

  • foreignId()->constrained() names the relationship in the schema.
  • cascadeOnDelete() is a product decision, not a default you copy.
  • unique(['team_id', 'number']) is the rule a validator can forget.
  • A table that production already has is changed by a new migration, not by editing the old one.

Check yourself · 3 questions

Question 1 of 3Two teams both issue INV-1. What should the database do?

The columns are stored. Next, what each one comes back as.

← PreviousModels that match the domain

On this page

  • One invoice belongs to one team
  • Add the lines in the next migration
  • 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.