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
{
// ...
}
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();
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 {}
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
hasManyto 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 in
config/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)