belongsToMany relationship in Laravel across multiple databases

Very simply:

public function bs()
{
    $database = $this->getConnection()->getDatabaseName();
    return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}

I'm obtaining the database name dynamically because my connection is configured based off an environment variable. Laravel seems to assume the pivot table to exist in the same database as the target relation, so this will force it to look instead to the database corresponding to the model that this method is in, your 'A' realm.


If you're not worried about SQLite databases, i.e. in the scope of a unit-test, that's all you need. But if you are, keep reading.


Firstly, the previous example isn't sufficient on its own. The value of $database would end up being a file-path, so you need to alias it to something that won't break an SQL statement, and make it accessible to the current connection. "ATTACH DATABASE '$database' AS $name" is how you do that:

public function bs()
{
    $database = $this->getConnection()->getDatabaseName();
    if (is_file($database)) {
        $connection = app('B')->getConnection()->getName();
        $name = $this->getConnection()->getName();
        \Illuminate\Support\Facades\DB::connection($connection)->statement("ATTACH DATABASE '$database' AS $name");
        $database = $name;
    }
    return $this->belongsToMany('B', "$database.a_bs", 'a_id', 'b_id');
}

Warning: Transactions muck this up: If the current connection is using transactions, the ATTACH DATABASE statement will fail. You can use transactions on it after executing that statement though.

Whereas, if the related connection uses transactions, the resulting data will be silently rendered invisible to the current one. This drove me nuts for longer than I'd care to admit, because my queries ran without error, but kept coming up empty. It seems only data truly written to the attached database is actually accessible to the one it's attached to.

So, after being forced to write to your attached database, you may still want your test to clean up after itself. A simple solution there would be to just use $this->artisan('migrate:rollback', ['--database' => $attachedConnectionName]);. But if you have multiple tests that need the same tables, this is not very efficient, as it forces them to have to rebuild them each time.

A better option would be to truncate the tables, but leave their structure in tact:

//Get all tables within the attached database
collect(DB::connection($database)->select("SELECT name FROM sqlite_master WHERE type = 'table'"))->each(function ($table) use ($name) {
        //Clear all entries for the table
        DB::connection($database)->delete("DELETE FROM '$table->name'");
        //Reset any auto-incremented index value
        DB::connection($database)->delete("DELETE FROM sqlite_sequence WHERE name = '$table->name'");
    });
}

This will wipe all data from that connection, but there's no reason you couldn't apply some kind filter to that however you see fit. Alternatively, you could take advantage of the fact that SQLite DBs are easily-accessible files, and just copy the attached one to a temp file, and use it to overwrite the source after the test is done executing. The result would be functionally identical to a transaction.


You can set the database of the table in the model class:

protected $table = 'A.a_s';

And You have to use singular form when create a pivot table.

/app/model/A.php

class A extends Eloquent {

  // Set table name (plural) with database name
  protected $table = 'A.a_s';

  // Many to many relation
  public function b_s() {
    return $this->belongsToMany('B');
  } 
}

/app/model/B.php

class B extends Eloquent {

  // Set table name (plural) with database name
  protected $table = 'B.b_s';

}

Query

print_r(A::with('b_s')->where('id', 1)->get()->toArray());

MySQL

CREATE TABLE `A`.`a_s` (
   `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
   PRIMARY KEY (`id`)
);
CREATE TABLE `B`.`b_s` (
   `id` INT UNSIGNED NOT NULL AUTO_INCREMENT,
   PRIMARY KEY (`id`)
);
CREATE TABLE `A`.`a_b` (
  `a_id` int(10) unsigned NOT NULL,
  `b_id` int(10) unsigned NOT NULL,
  PRIMARY KEY (`a_id`,`b_id`)
) ENGINE=InnoDB;

INSERT INTO A.a_s VALUES (NULL);
INSERT INTO A.a_s VALUES (NULL);
INSERT INTO B.b_s VALUES (NULL);
INSERT INTO A.a_b VALUES (1,1);
INSERT INTO A.a_b VALUES (1,2);

If the databases/schemas are on the same host server like @NiRR said just do this to override the default schema of the second connection:

return $this->belongsToMany('B', 'real-schema-name.a_bs');

or alternatively

return $this->belongsToMany('A', 'real-schema-name.a_bs');

Depending on which model (A or B) is defined with the connection that isn't using the default schema.

Remember that it's impossible to do a join query that spans across two servers; which server is it going to get executed on? Each is missing some of the needed data needed to preform the request.


This is only possible if the two databases are on the same connection (server).

Its not possible to do this on two different connections (servers) since you'll need all three tables on a single server that will perform the join command.