skip to content

Database Layer

Laravel's data layer under Eloquent: the fluent query builder, connections and transaction closures, schema migrations, factories and seeders, and paginators. Interviewers probe it constantly.

on this pageshow

explore

questions

28

In a fresh Laravel 13 app, which database connection is used by default, where is that decided, and how do you run a query on another named connection?

level: juniorimportance: must knowfreq 52%

answer

  1. config/database.php 'default' key
  2. DB_CONNECTION=sqlite in .env.example
  3. database/database.sqlite file
  4. DB::connection('pgsql')->table()
  5. connections array keyed by name

basics

~10 s

SQLite: config/database.php sets 'default' => env('DB_CONNECTION', 'sqlite') and the skeleton's .env.example sets DB_CONNECTION=sqlite, pointing at database/database.sqlite. DB::connection('pgsql') returns any other connection named in the 'connections' array.

solid answer

~30 s

In Laravel 13 the skeleton's `config/database.php` sets `'default' => env('DB_CONNECTION', 'sqlite')`, and `.env.example` ships `DB_CONNECTION=sqlite` with the MySQL lines commented out; the `sqlite` connection reads `DB_DATABASE`, falling back to `database/database.sqlite`. Every other connection lives in the `connections` array under a name (`sqlite`, `mysql`, `mariadb`, `pgsql`, `sqlsrv`, plus any you add). `DB::table()` and Eloquent use the default; `DB::connection('reporting')` returns the named connection, on which you call `table()`, `select()` or `transaction()`. An Eloquent model can pin its own connection with the `#[Connection('reporting')]` attribute or a `$connection` property. A single `DB_URL` can replace the separate host, port and credential variables.

code

php · 14 lines
php
<?php

use Illuminate\Support\Facades\DB;

// Default connection (SQLite in a fresh Laravel 13 app)
$members = DB::table('members')->count();

// A named connection from config/database.php 'connections'
$legacy = DB::connection('legacy')
    ->table('members')
    ->where('active', 1)
    ->count();

$name = DB::connection()->getName(); // 'sqlite' unless DB_CONNECTION says otherwise

go deeper

for a junior

Recall that DB_CONNECTION feeds config/database.php's default, that new apps use SQLite, and that DB::connection('name') picks another connection.

for a middle

Explain how named connections are created lazily and cached, how DB_URL replaces separate variables, and how a model pins its connection.

for a senior

Show that connections are independent, so transactions never span them, and diagnose environment changes hidden by cached configuration.

for a principal

Weigh whether a second database belongs behind its own connection in this app or behind a separate service boundary.

## Where the default comes from Laravel reads its database settings from **`config/database.php`**. Two keys matter most: - **`default`** names the connection used whenever code does not ask for a specific one. In the Laravel 13 skeleton it is `env('DB_CONNECTION', 'sqlite')`. - **`connections`** is an array of named connection definitions. The skeleton ships `sqlite`, `mysql`, `mariadb`, `pgsql` and `sqlsrv`, each reading its details from environment variables. The skeleton's `.env.example` sets `DB_CONNECTION=sqlite` and leaves `DB_HOST`, `DB_PORT`, `DB_DATABASE`, `DB_USERNAME` and `DB_PASSWORD` commented out. The `sqlite` connection uses `DB_DATABASE` when set and otherwise `database_path('database.sqlite')`, a file the installer creates. So a brand-new app runs migrations and serves requests with no database server at all. ## Switching the default To move an app to PostgreSQL, change the environment, not the code: ```ini DB_CONNECTION=pgsql DB_HOST=127.0.0.1 DB_PORT=5432 DB_DATABASE=coffee DB_USERNAME=app DB_PASSWORD=secret ``` Every connection also reads **`DB_URL`**. When a managed provider hands you one URL such as `pgsql://app:[email protected]:5432/coffee`, setting `DB_URL` supplies the driver, host, credentials and database in one variable. Because these values reach the app through `config/database.php`, a production deploy that caches configuration must re-cache after changing them. ## Using a second connection Many apps talk to more than one database: a reporting replica, a legacy system, a separate analytics store. Add a named entry to `connections`, then ask for it by name: ```php use Illuminate\Support\Facades\DB; $rows = DB::connection('legacy')->table('members')->where('active', 1)->get(); DB::connection('legacy')->transaction(function () { // statements here run on the legacy connection only }); ``` Useful facts about `DB::connection()`: 1. **Called without a name**, it returns the default connection. `DB::table()` is shorthand for `DB::connection()->table()`. 2. **Connections are created lazily** and cached by name for the rest of the process, so asking twice returns the same object and does not reconnect. 3. **Each connection is independent**: a transaction opened on the default connection does not cover statements sent through `DB::connection('legacy')`. 4. **An unknown name throws** an `InvalidArgumentException`, which surfaces a typo at the first query. ## Eloquent models A model uses the default connection unless told otherwise. In Laravel 13 the idiomatic way to pin one is the `Illuminate\Database\Eloquent\Attributes\Connection` attribute, `#[Connection('legacy')]`, placed on the class; the older `protected $connection = 'legacy';` property still works. ## Pitfalls around connections - **SQLite in development, MySQL in production.** The default makes a fresh app easy to start, but SQL that works on SQLite, such as date functions or loose type comparisons, can behave differently on the production driver. Run the test suite against the production driver before relying on it. - **Foreign keys on SQLite.** The skeleton's `sqlite` connection enables foreign key constraints through `DB_FOREIGN_KEYS`, which defaults to `true`; turning it off hides integrity errors that MySQL or PostgreSQL would raise. - **Hard-coded names.** Code that calls `DB::connection('mysql')` breaks when the default moves to `pgsql`. Use the default connection unless the second database is genuinely separate, and name that one after its role, such as `legacy` or `reporting`. ## Summary | Question | Answer in Laravel 13 | |---|---| | Default driver in a new app | SQLite | | Where it is chosen | `config/database.php` `default`, from `DB_CONNECTION` | | SQLite file location | `database/database.sqlite` unless `DB_DATABASE` is set | | Query another connection | `DB::connection('name')->table(...)` | | Pin a model to a connection | `#[Connection('name')]` or `$connection` | ## Why interviewers ask The question checks that a candidate knows configuration flows from environment variables through `config/database.php`, that the default changed to SQLite for new apps, and that a connection is a named, independent object. That last point matters later when transactions, read replicas and multi-database reports enter the conversation.

  • You set DB_CONNECTION=pgsql in production but queries still hit SQLite. What would you check first?
    Whether configuration is cached: after `php artisan config:cache`, the cached array is used and `.env` changes are ignored until the cache is rebuilt. Then check that `DB_CONNECTION` is set in the real environment of the PHP process, and that `config('database.default')` reports `pgsql` in `php artisan tinker` on that server.
  • Does DB::transaction() on the default connection protect writes made through DB::connection('legacy')?
    No. A transaction belongs to one connection object and its PDO handle. Writes sent through another named connection run in that connection's own autocommit mode or its own transaction. If both must succeed together, you need a design that tolerates partial failure, because Laravel does not coordinate transactions across connections.

saying these in an interview costs you the question

  • Says a new Laravel 13 app defaults to MySQL
  • Edits config/database.php values instead of environment variables
  • Thinks DB::connection('x') opens a new PDO on every call
  • Believes one DB::transaction() spans all configured connections
  • Expects a missing connection name to fall back to the default
open as a page

In a Laravel model factory, what is the difference between make() and create(), and what does each return?

level: juniorimportance: must knowfreq 62%

basics

~20 s

make() builds model instances from the factory's definition() without saving them; create() builds them the same way and then persists each one with save(). Both return a single model, or an Eloquent Collection once count() is set.

open as a page

In Laravel, how do you organise and run database seeders, and what do db:seed --class and migrate --seed do?

level: juniorimportance: must knowfreq 58%

basics

~20 s

A seeder is a class in database/seeders with a run() method; DatabaseSeeder is the root and runs others through $this->call(). php artisan db:seed runs DatabaseSeeder, --class runs one seeder, and migrate --seed seeds after migrating.

open as a page

In Laravel, what does php artisan make:migration create_listings_table generate, and how do its up() and down() methods relate to migrate and migrate:rollback?

level: juniorimportance: must knowfreq 72%

basics

~20 s

It writes a timestamped file in database/migrations that returns an anonymous class extending Migration, with up() creating the listings table and down() dropping it. migrate runs pending up() methods as one batch; migrate:rollback runs down() for the last batch.

open as a page

In Laravel, what is the difference between paginate(), simplePaginate() and cursorPaginate(), and which SQL queries does each run?

level: juniorimportance: must knowfreq 60%

basics

~20 s

paginate() runs a COUNT query plus a LIMIT/OFFSET query and returns a LengthAwarePaginator with totals and page numbers; simplePaginate() runs one LIMIT/OFFSET query for next/previous only; cursorPaginate() filters on the ordered columns instead of using an offset.

open as a page

In Laravel's query builder, what do DB::table('orders')->get(), ->first() and ->value('total') return, including when no row matches?

level: juniorimportance: must knowfreq 62%

basics

~10 s

get() returns an Illuminate\Support\Collection of stdClass rows, empty when nothing matches; first() returns one stdClass or null; value('total') returns that column from the first row or null. Only firstOrFail() and sole() throw.

open as a page

In Laravel, how does DB::transaction() with a closure differ from DB::beginTransaction(), commit() and rollBack() when transferring loyalty points between two members?

level: middleimportance: must knowfreq 60%

basics

~20 s

DB::transaction() begins, runs the closure, commits on success, and on any exception rolls back and rethrows, returning the closure's value. The manual trio leaves every path to you, so a missed rollBack() leaves the transaction open on the connection.

open as a page

In a Laravel migration, what does $table->foreignId('seller_id')->constrained()->cascadeOnDelete() create, and why must nullable() come before constrained()?

level: middleimportance: must knowfreq 60%

basics

~20 s

It adds an unsigned big-integer seller_id column and a foreign key to sellers.id that deletes listings when their seller is deleted. constrained() returns the foreign-key definition, so a nullable() chained after it never reaches the column.

open as a page

In Laravel's query builder, why does chaining where('shop_id', 3)->where('status', 'paid')->orWhere('status', 'refunded') return other shops' orders, and how do you fix it?

level: middleimportance: must knowfreq 58%

basics

~20 s

The chain compiles to shop_id = 3 and status = 'paid' or status = 'refunded'; SQL evaluates and before or, so every refunded order from any shop matches. Group the alternatives in a closure passed to where(), or use whereIn().

open as a page

When a Laravel route returns a paginator directly, what JSON does paginate() produce, and how does it differ for simplePaginate() and cursorPaginate()?

level: juniorimportance: should knowfreq 42%

basics

~20 s

Returning a paginator serialises it with the records under data plus metadata. paginate() adds total, last_page, last_page_url and a links array; simplePaginate() has page URLs but no total; cursorPaginate() has next_cursor, prev_cursor and their URLs instead of page numbers.

open as a page

In Laravel, how do you log every SQL query with DB::listen(), and how does DB::whenQueryingForLongerThan() differ for spotting slow requests?

level: middleimportance: should knowfreq 30%

basics

~10 s

DB::listen() registers a closure that receives a QueryExecuted event after every query, with sql, bindings, time in milliseconds and connectionName. whenQueryingForLongerThan() instead fires once when a connection's total query time exceeds a threshold.

open as a page

With Laravel model factories, how do for(), has() and hasAttached() build related records, and what do magic calls like hasAppointments(3) resolve to?

level: middleimportance: should knowfreq 40%

basics

~20 s

for() gives created models a belongsTo parent, from a factory or an existing model; has() creates hasMany or belongsToMany children after the parent is saved; hasAttached() attaches belongsToMany models with pivot values. hasAppointments(3) is has() driven by the appointments() relation.

open as a page

In a Laravel model factory, how do states and sequence() vary generated records, and how do you define a reusable named state?

level: middleimportance: should knowfreq 46%

basics

~20 s

A state is an attribute override layered on top of definition(); a named state is a factory method that returns $this->state(...). sequence() adds a state that cycles through its values one record at a time.

open as a page

In Laravel, how do migrate:rollback, migrate:reset, migrate:refresh and migrate:fresh differ, and which of them never calls down()?

level: middleimportance: should knowfreq 55%

basics

~10 s

rollback runs down() for the latest batch; reset runs down() for every migration; refresh does reset (or a --step rollback) and migrates again; fresh drops every table with db:wipe and migrates, never calling down().

open as a page

In Laravel's query builder, how would you build a coffee-shop chain's monthly revenue-per-shop report with a join, aggregates and grouping, keeping shops that sold nothing?

level: middleimportance: should knowfreq 42%

basics

~20 s

Start from shops, leftJoin orders with the month and status conditions inside the join closure, selectRaw a coalesced sum, and groupBy the shop columns. A date filter in where() would drop zero-sale shops, and calling sum() returns one number.

open as a page

In Laravel's query builder, how does when() apply an optional filter, and why can ->when($request->input('shop_id'), ...) skip the filter for shop 0?

level: middleimportance: should knowfreq 34%

basics

~20 s

when($value, $callback) runs the callback only if $value is truthy, passing the builder and the value. The string "0" is falsy in PHP, so a shop_id of 0 skips the filter and returns every shop's rows.

open as a page

In Laravel, when does a DB::afterCommit() callback registered during a loyalty-points transfer run, and what happens to it if the transaction rolls back?

level: seniorimportance: should knowfreq 32%

basics

~10 s

It runs right after the outermost transaction commits, or immediately if no transaction is open. If the transaction, or the savepoint it was registered in, rolls back, the callback is discarded and never runs.

open as a page

In Laravel's config/database.php, how do read and write hosts and the sticky option route queries, and why might a member not see points just transferred?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Selects use a read host and writes use the write host. Without sticky, a read after a write in the same request can hit a lagging replica and miss the new points; reads inside a transaction always use the writer.

open as a page

In Laravel, what does DB::transaction($callback, attempts: 5) actually retry, and what can go wrong when the loyalty-transfer closure runs a second time?

level: seniorimportance: should knowfreq 36%

basics

~20 s

Only concurrency errors are retried: SQLSTATE 40001, deadlock and lock-wait-timeout messages, or SQLite's 'database is locked'. Any other exception is rolled back and rethrown at once. A retry re-runs the whole closure, so non-database side effects inside it happen again.

open as a page

Why does a Laravel seeder built on model factories fail or stall during a production deploy, and what must change to seed there safely?

level: seniorimportance: should knowfreq 32%

basics

~20 s

The skeleton lists fakerphp/faker under require-dev, so a --no-dev production install lacks it and fake() fails; and db:seed asks for confirmation in production, cancelling non-interactive runs without --force. Seed production from a dedicated seeder with fixed values.

open as a page

A Laravel seeder creates 500 appointments whose factory sets 'doctor_id' => Doctor::factory() — why does it also create 500 doctors, and how do you fix it?

level: seniorimportance: should knowfreq 26%

basics

~10 s

definition() runs once per appointment, and a nested Doctor::factory() is created each time to supply the key. Create a pool first and chain recycle($doctors), so the factory picks a random existing doctor instead.

open as a page

In a Laravel migration, how would you add a required currency column to a marketplace's large orders table, and what does change() do to modifiers you omit?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Existing rows need a value: add it with a default, or add it nullable, backfill, then make it required with change(). change() redefines the whole column, so any modifier not restated, such as default or comment, is dropped.

open as a page

A Laravel activity feed uses cursorPaginate() ordered only by created_at — why can it skip or repeat activities, and what ordering rules does cursor pagination need?

level: seniorimportance: should knowfreq 32%

basics

~20 s

The cursor stores the last row's created_at and the next page asks for rows strictly older, so rows sharing that timestamp are skipped. Order by a unique combination, such as created_at then id, with non-null columns from the paginated table.

open as a page

In Laravel, a gift-card redemption uses DB::table('gift_cards')->where('id', $id)->lockForUpdate()->first() yet still double-spends a balance under load; what are the likely causes?

level: seniorimportance: should knowfreq 38%

basics

~10 s

lockForUpdate() only appends FOR UPDATE; without DB::transaction() around the read and the write the lock ends with the statement. On SQLite, the skeleton default, the grammar emits no lock clause at all.

open as a page

In Laravel's query builder, which parts of a query are bound as parameters and which are pasted in verbatim, and how do selectRaw, whereRaw and DB::raw change that?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Values passed to where(), whereIn(), insert() and update() are bound as PDO parameters. Column and table names are only quoted, and DB::raw() or selectRaw/whereRaw/orderByRaw strings are pasted verbatim, so user input there must go through their ? bindings or an allowlist.

open as a page

In Laravel, what do php artisan migrate --pretend and php artisan schema:dump --prune each do, and what are their limits?

level: middleimportance: nice to knowfreq 24%

basics

~10 s

migrate --pretend prints the SQL pending migrations would run without executing it; queries a migration reads return empty. schema:dump writes the schema, with migrations rows, to database/schema/<connection>-schema.sql, and --prune deletes database/migrations.

open as a page