DEV Community

Cover image for Laravel 13 #[Connection] Attribute: Per-Model Database
devTalk
devTalk

Posted on Originally published at dev-talk.com

Laravel 13 #[Connection] Attribute: Per-Model Database

Most Laravel apps talk to one database and never think about connections. Then a reporting database shows up, or a legacy system, or a read-only analytics replica, and suddenly a few models need to talk somewhere else. Laravel 13's Eloquent documentation shows a clean way to do it: a #[Connection] attribute on the model class. I tested how it behaves with two real databases, including a few details the docs do not mention.

The basic example

First, define the extra connection in config/database.php as you normally would, for example one called reporting. Then point a model at it:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Attributes\Connection;
use Illuminate\Database\Eloquent\Model;

#[Connection('reporting')]
class SalesSummary extends Model
{
    // ...
}
Enter fullscreen mode Exit fullscreen mode

That is the whole change. Every query this model runs, reads and writes, goes to the reporting connection. Without the attribute, a model uses your application's default connection. The documentation example uses the same pattern with #[Connection('mysql')] on a Flight model.

What I tested

I set up two MariaDB databases, one as the default and one as reporting, each holding a table with the same name but different data, so any query that went to the wrong place was obvious. Results from the real Laravel 13.35.0 Eloquent classes:

Case Result
Model with #[Connection('reporting')] Reads came from the reporting database
Model with no attribute Used the default database
Saving a new record on the attributed model Row landed in the reporting database, default untouched
Attribute on a base class, child extends it Child used the reporting connection
Attribute plus protected $connection = 'default' The property won
Model::on('default') on an attributed model Overrode the attribute for that query

Three details worth knowing

1. You can use an enum instead of a string

The attribute's constructor in the framework source accepts either a string or a PHP enum. I confirmed that a string-backed enum case resolves to the right connection:

enum Db: string
{
    case Main = 'mysql';
    case Reporting = 'reporting';
}

#[Connection(Db::Reporting)]
class SalesSummary extends Model {}

SalesSummary::on(Db::Reporting)->first();
Enter fullscreen mode Exit fullscreen mode

This is not shown on the docs page, so it comes from the source and my test, not the documentation. It saves you from typos in connection names scattered across models. Using it needs an enum you maintain yourself.

2. Set it once on a base class

If ten models live in the reporting database, do not repeat the attribute ten times. Put it on an abstract base class and extend that. My child model picked up the connection from its parent.

#[Connection('reporting')]
abstract class ReportingModel extends Model {}

class SalesSummary extends ReportingModel {}
class DailyRevenue extends ReportingModel {}
Enter fullscreen mode Exit fullscreen mode

3. Relationships follow the parent unless the related model says otherwise

This is the one that bites. I tested three setups:

  • A parent on the default connection with a hasMany to a plain child: the children came from the default database.
  • The same parent loaded with Parent::on('reporting'): the plain child came from reporting. The relation inherits the parent's connection.
  • A related model with its own #[Connection('reporting')], loaded from a default-connection parent: it used its own connection.

So a child without an attribute follows whichever connection its parent was loaded from. If you want a related model to always use a specific database, give that model its own attribute. Also keep in mind that a single SQL query cannot span two separate database servers, so a relationship across connections is loaded with separate queries rather than a join. Avoid whereHas and similar across models on different connections unless the tables really live in the same database server.

When this is actually useful

  • Reporting or analytics database. Heavy queries stay away from your main database. A good production setup is a read-only database user on that connection, so a bug cannot write to it.
  • Legacy or third-party database. Wrap the old tables in models without moving data.
  • Multi-database setups. Different parts of the app, such as a billing database and a main database.

Common mistakes

  • The connection name is not inconfig/database.php. The attribute only names a connection, it does not create one.
  • Expecting transactions to span connections. A transaction belongs to one connection, so work on two connections is not covered by one DB::transaction().
  • Forgetting that the property wins. If an old model still has protected $connection, it overrides the attribute.

References


Originally published on DEV Talk.

Top comments (0)