Image Principale

Les transactions SQL sur Laravel


Dans cet article, nous allons parler d'une fonctionnalité de MySQL: Les transactions. Celles-ci permettent de faire passer plusieurs requêtes en même temps au serveur SQL. On économise donc énormément de temps dans nos scripts.

Contact Person Mathieu Marteau
il y a 1 an

Imaginons un Seeder de table dans lequel on souhaite insérer un très grand nombre de données.

Dans ce système, nous avons deux modèles: User et University. J'ai défini pour ces deux modèles des migrations ainsi que des factories qui vont me permettre de créer des objets aléatoires destinés à être utilisés pour mes tests lors du développement:

// database/factories/ModelFactory.php

$factory->define(App\User::class, function (Faker\Generator $faker) {
    static $password;

    return [
        'name' => $faker->name,
        'email' => $faker->unique()->safeEmail,
        'password' => $password ?: $password = bcrypt('secret'),
        'remember_token' => str_random(10),
    ];
});

$factory->define(App\University::class, function (Faker\Generator $faker) {
    return [
        'name' => $faker->sentence(1)
    ];
});

J'ai ensuite créé un Seeder pour remplir ma base de données:

php artisan make:seeder UniversitiesTableSeeder

Ce seeder est pour l'instant très simple:

public function run()
{
    factory(\App\University::class, 5)->create()->each(function ($u) {
        $number_of_users = 5000;
        for($i = 0; $i < $number_of_users; $i++) {
            $u->users()->attach(factory(\App\User::class)->create()->id);
        }
    });
}

Comme vous le voyez, je souhaite assigner 5000 utilisateurs à chacune de mes universités. J'ai donc à peu près 25 000 nouvelles lignes qui vont être insérées dans ma base de données.

Pour les besoins de cet article, je vais aussi ajouter des petits indicateurs qui vont me dire en combien de temps mon code s'exécute:

public function run()
{
    $start = microtime(true);

    factory(\App\University::class, 5)->create()->each(function ($u) {
        $number_of_users = 5000;
        for($i = 0; $i < $number_of_users; $i++) {
            $u->users()->attach(factory(\App\User::class)->create()->id);
        }
    });

    $time_elapsed_secs = microtime(true) - $start;
    dump('effectué en ' . $time_elapsed_secs . 's');
}

Quand je lance ce code en raffraichissant entièrement mes bases de données, voici ce que j'obtiens:

php artisan migrate:refresh --seed

"effectué en 46.080822944641s"

Place aux transactions

La solution pour gagner un peu de temps se trouve dans les transactions SQL. Et leur utilisation est très simple. Il suffit d'envelopper le code que l'on souhaite exécuter dans une closure:

DB::transaction(function () {
    // Votre code ici
});

Appliqué à notre seeder, cela nous donne:

public function run()
{
    $start = microtime(true);

    DB::transaction(function () {
        factory(\App\University::class, 5)->create()->each(function ($u) {
            $number_of_users = 5000;
            for($i = 0; $i < $number_of_users; $i++) {
                $u->users()->attach(factory(\App\User::class)->create()->id);
            }
        });
    });

    $time_elapsed_secs = microtime(true) - $start;
    dump('effectué en ' . $time_elapsed_secs . 's');
}

Quand je relance ma commande j'obtiens:

php artisan migrate:refresh --seed

"effectué en 28.253461122513s"

On a gagné presque 20 secondes!

Autres bénéfices offerts par les transactions

Un autre bénéfice offert par les transactions repose dans le fait que si un problème arrive dans notre transaction, alors aucune information de cette transaction ne sera insérée dans la base de données et vous pourrez alors retourner un message d'erreur à vos utilisateurs:

try {
    DB::transaction(function () {
        // Le code à éxecuter
    });
} catch (\Exception $e) {
    // On catch l'erreur pour pouvoir faire quelque chose
}