djt
djt

Reputation: 7535

Laravel - Seeding Relationships

In Laravel, database seeding is generally accomplished through Model factories. So you define a blueprint for your Model using Faker data, and say how many instances you need:

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
        'password' => bcrypt(str_random(10)),
        'remember_token' => str_random(10),
    ];
});

$user = factory(App\User::class, 50)->create();

However, lets say your User model has a hasMany relationship with many other Models, like a Post model for example:

Post:
   id
   name
   body
   user_id

So in this situation, you want to seed your Posts table with actual users that were seeded in your Users table. This doesn't seem to be explicitly discussed, but I did find the following in the Laravel docs:

$users = factory(App\User::class, 3)
    ->create()
    ->each(function($u) {
         $u->posts()->save(factory(App\Post::class)->make());
    });

So in your User factory, you create X number of Posts for each User you create. However, in a large application where maybe 50 - 75 Models share relationships with the User Model, your User Seeder would essentially end up seeding the entire database with all it's relationships.

My question is: Is this the best way to handle this? The only other thing I can think of is to Seed the Users first (without seeding any relations), and then pull random Users from the DB as needed while you are seeding other Models. However, in cases where they need to be unique, you'd have to keep track of which Users had been used. Also, it seems this would add a lot of extra query-bulk to the seeding process.

Upvotes: 36

Views: 47617

Answers (7)

ignaciodev
ignaciodev

Reputation: 48

I use a custom made relateOrCreate function that finds a random entry of that model in the database. If none exist, it creates a new one:

function relateOrCreate($class) {
    $instances = $class::all();
    $instance;

    if (count($instances) > 0) {
        $randomIndex = rand(0, (count($instances) - 1));
        $instance = $instances[$randomIndex];
    }
    else {
        $instance = $class::factory()->create();
    }

    return $instance;
}

Then I use it like so:

$relatedUser = relateOrCreate(User::class);

return [
    'user_id' => $relatedUser->id,
    // ...
];

Upvotes: 0

Subramanya Rao
Subramanya Rao

Reputation: 170

this worked for me in laravel v8

for ($i=0; $i<=2; $i++) {
    $user = \App\Models\User::factory(1)->create()->first();
    $product = \App\Models\Product::factory(1)->create(['user_id' => $user->id])->first();
}

Upvotes: 0

Juan S&#225;nchez
Juan S&#225;nchez

Reputation: 1050

I want to share the approach i've taken for insert many posts to many users:`

factory(App\User::class, 50)->create() 
                ->each( 
                    function ($u) {
                        factory(App\Post::class, 10)->create()
                                ->each(
                                    function($p) use (&$u) { 
                                        $u->posts()->save($p)->make();
                                    }
                                );
                    }
                );

`

This workaround worked for me after being all day long looking for a way to seed the relationship

Upvotes: 4

TimmyG
TimmyG

Reputation: 751

You can do this using closures within the ModelFactory as discussed here.

This solution works cleanly and elegantly with seeders as well.

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
        'password' => bcrypt(str_random(10)),
        'remember_token' => str_random(10),
    ];
});

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'body' => $faker->paragraph(1),
        'user_id' => function() {
            return factory(App\User::class)->create()->id;
        },
    ];
});

For your seeder, use something simple like this:

//create 10 users
factory(User::class, 10)->create()->each(function ($user) {
    //create 5 posts for each user
    factory(Post::class, 5)->create(['user_id'=>$user->id]);
});

NOTE: This method does not create unneeded entries in the database, instead the passed attributes are assigned BEFORE the creation of associated records.

Upvotes: 31

user6392101
user6392101

Reputation: 638

You can use saveMany as well. For example:

factory(User::class, 10)->create()->each(function ($user) {
    $user->posts()->saveMany(factory(Posts::class, 5)->make());
});

Upvotes: 54

Roel
Roel

Reputation: 457

Personally I think one Seeder class to manage these relations is nicer then separated seeder classes, because you have all the logic in one place, so in one look you can see what is going on. (Anyone that knows a better approach: please share) :)

A solution might be: one DatabaseSeeder and private methods within the class to keep the 'run' method a bit cleaner. I have this example below, which has a User, Link, LinkUser (many-to-many) and a Note (many-to-one).

For the many-to-many relations I first create all the Links, and get the inserted ids. (since the ids are auto-inc I think the ids could be fetched easier (get max), but doesn't matter in this example). Then create the users, and attach some random links to each user (many-to-many). It also creates random notes for each user (many-to-one example). It uses the 'factory' methods.

If you replace the 'Link' for your 'Post' this should work. (You can remove the 'Note' section then...)

(There is also a method to make sure you have 1 valid user with your own login credentials.)

<?php

use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // Create random links
        factory(App\Link::class, 100)->create();

        // Fetch the link ids
        $link_ids = App\Link::all('id')->pluck('id')->toArray();

        // Create random users
        factory(App\User::class, 50)->create()->each(function ($user) use ($link_ids) {

            // Example: Many-to-many relations
            $this->attachRandomLinksToUser($user->id, $link_ids);

            // Example: Many-to-one relations
            $this->createNotesForUserId( $user->id );
        });

        // Make sure you have a user to login with (your own email, name and password)
        $this->updateCredentialsForTestLogin('[email protected]', 'John Doe', 'my-password');
    }

    /**
     * @param $user_id
     * @param $link_ids
     * @return void
     */
    private function attachRandomLinksToUser($user_id, $link_ids)
    {
        $amount = random_int( 0, count($link_ids) ); // The amount of links for this user
        echo "Attach " . $amount . " link(s) to user " . $user_id . "\n";

        if($amount > 0) {
            $keys = (array)array_rand($link_ids, $amount); // Random links

            foreach($keys as $key) {
                DB::table('link_user')->insert([
                    'link_id' => $link_ids[$key],
                    'user_id' => $user_id,
                ]);
            }
        }
    }

    /**
     * @param $user_id
     * @return void
     */
    private function createNotesForUserId($user_id)
    {
        $amount = random_int(10, 50);
        factory(App\Note::class, $amount)->create([
            'user_id' => $user_id
        ]);
    }

    /**
     * @param $email
     * @param $name
     * @param $password
     * @return void
     */
    private function updateCredentialsForTestLogin($email, $name, $password)
    {
        $user = App\User::where('email', $email)->first();
        if(!$user) {
            $user = App\User::find(1);
        }
        $user->name = $name;
        $user->email = $email;
        $user->password = bcrypt($password); // Or whatever you use for password encryption
        $user->save();
    }
}

Upvotes: 9

Helder Lucas
Helder Lucas

Reputation: 3343

$factory->define(App\User::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'email' => $faker->email,
        'password' => bcrypt(str_random(10)),
        'remember_token' => str_random(10),
    ];
});

$factory->define(App\Post::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->name,
        'body' => $faker->paragraph(1),
        'user_id' => factory(App\User::class)->create()->id,
    ];
});

So now if you do this factory(App\Post::class, 4)->create() it will create 4 different posts and in the process also create 4 different users.

If you want the same user for all the posts what I usually do is:

$user = factory(App\User::class)->create();
$posts = factory(App\Posts::class, 40)->create(['user_id' => $user->id]);

Upvotes: 6

Related Questions