How to do multi-level seeding in by using Eloquent Relationships in Laravel?

317 Views Asked by At

Here is the table stucture

  • Table A
  • Table B has foreign key a_id
  • Table C has foreign key a_id and b_id

I am trying to Seed these three tables with Eloquent Relationships

Here is my code structure

class A extends Model
{
    use HasFactory;

    protected $fillable = [...];

    public function b()
    {
        return $this->hasMany(B::class);
    }

    public function c()
    {
        return $this->hasMany(C::class);
    }
}
class B extends Model
{
    use HasFactory;

    protected $fillable = [...];

    public function c()
    {
        return $this->hasMany(C::class);
    }
}
class C extends Model
{
    use HasFactory;

    protected $fillable = [...];
}
class DatabaseSeeder extends Seeder
{
    public function run()
    {
        \App\Models\A::factory(5)
            ->has(\App\Models\B::factory(2))
            ->has(\App\Models\C::factory(1))
            ->create();
    }
}

Error:

SQLSTATE[HY000]: General error: 1364 Field 'b_id' doesn't have a default value (SQL: insert into c (... a_id, updated_at, created_at)

1

There are 1 best solutions below

6
IGP On

Your seeder will create 5 A models.

For each of these 5 A, it's going to try and do two things

  • create 2 B, passing in a_id.
  • create 1 C, passing in a_id, but not passing any b_id.

Here's how each of those A models would look like.

// A model
{
    id: 1
    b: [
        // B model
        {
            id: 1,
            a_id: 1
        },
        // B model
        {
            id: 2,
            a_id: 1
        },
    ],
    c: [
        // C model
        {
            id: 1,
            a_id: 1,
        ->  b_id: null <- This will throw the error because b_id is not a nullable column
        },
    ],
}

To fix it you will need to either remove has(\App\Models\C::class) or make a B model to associate with it.

You need to define the belongsTo relationship for this to work.

# B model
public function a()
{
    return $this->belongsTo(A::class);
}
# C model
public function a()
{
    return $this->belongsTo(A::class);
}

public function b()
{
    return $this->belongsTo(B::class);
}

For example

use App\Models\A;
use App\Models\B;
use App\Models\C;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $b = B::factory()->for(A::factory())->create();

        A::factory(5)
            ->has(B::factory(2))
            ->has(C::factory(1)->for($b))
            ->create();
    }
}

You could also not create everything in one statement.

use App\Models\A;
use App\Models\B;
use App\Models\C;

class DatabaseSeeder extends Seeder
{
    public function run()
    {
        $A_models = A::factory(5)
            ->has(B::factory(2))
            ->create();

        foreach ($A_models as $A) {
            $b_id = $A->b->pluck('id')->random();
            $c = C::factory()->make(['b_id' => $b_id]);

            $a->c()->save($c);
        }
    }
}