30

When developing i'm having so many issues with migrations in laravel.

I create a migration. When i finish creating it, there's a small error by the middle of the migration (say, a foreign key constraint) that makes "php artisan migrate" fail. He tells me where the error is, indeed, but then migrate gets to an unconsistent state, where all the modifications to the database made before the error are made, and not the next ones.

This makes that when I fix the error and re-run migrate, the first statement fails, as the column/table is already created/modified. Then the only solution I know is to go to my database and "rollback" everything by hand, which is way longer to do.

migrate:rollback tries to rollback the previous migrations, as the current was not applied succesfully.

I also tried to wrap all my code into a DB::transaction(), but it still doesn't work.

Is there any solution for this? Or i just have to keep rolling things back by hand?



edit, adding an example (not writing Schema builder code, just some kind of pseudo-code):
Migration1:

Create Table users (id, name, last_name, email)

Migration1 executed OK. Some days later we make Migration 2:

Create Table items (id, user_id references users.id)
Alter Table users make_some_error_here

Now what will happen is that migrate will call the first statement and will create the table items with his foreign key to users. Then when he tries to apply the next statement it will fail.

If we fix the make_some_error_here, we can't run migrate because the table "items" it's created. We can't rollback (nor refresh, nor reset), because we can't delete the table users since there's a foreign key constraint from the table items.

Then the only way to continue is to go to the database and delete the table items by hand, to get migrate in a consistent state.

olivarra1
  • 3,269
  • 3
  • 23
  • 34
  • 3
    Indeed, this is very annoying. I haven't figured out a way to make run in a MySQL transaction either. It seems to ignore it entirely when I try. – timetofly Dec 23 '13 at 22:01
  • 1
    @Blossoming_Flower, DDL statements in MYSQL cannot be rolled back. Read my answer for more details and links. Thanks. – Yevgeniy Afanasyev Dec 20 '15 at 06:59

7 Answers7

22

It is not a Laravel limitation, I bet you use MYSQL, right?

As MYSQL documentation says here

Some statements cannot be rolled back. In general, these include data definition language (DDL) statements, such as those that create or drop databases, those that create, drop, or alter tables or stored routines.

And we have a recommendation of Taylor Otwell himself here saying:

My best advice is to do a single operation per migration so that your migrations stay very granular.

-- UPDATE --

Do not worry!

The best practices say:

You should never make a breaking change.

It means, in one deployment you create new tables and fields and deploy a new release that uses them. In a next deployment, you delete unused tables and fields.

Now, even if you'll get a problem in either of these deployments, don't worry if your migration failed, the working release uses the functional data structure anyway. And with the single operation per migration, you'll find a problem in no time.

Gus Costa
  • 609
  • 5
  • 13
Yevgeniy Afanasyev
  • 37,872
  • 26
  • 173
  • 191
  • how about if I need to change a primary key, this cant be sepated in multiples migrations, and will generate a big problem if an errors happens in the middle of the process, temporary tables need to be created to change a primary key – SpaceDogCS Sep 02 '20 at 19:03
  • @SpaceDogCS - forget the word "change". You don't change things in continuous integration approach. You create new elements, put them into use and then delete old ones. – Yevgeniy Afanasyev Sep 03 '20 at 04:22
  • So what would that be? In my scenario, I have an error where the exporting software don't send a field required as the primary key of the table (the table has a group of primary keys), so I realized that column shouln't be part of the primary key, the table it self is an orders table, should I create other table and join they on queries? That wouldn't be much harder to handle? – SpaceDogCS Sep 03 '20 at 12:38
  • First - the migration should not bring errors. If you have errors - you need to fix them and then follow the best practices to avoid errors in the future. Second - you are in wrong allowing "exporting software" to deal with your database. It should be one project per database. If you want to share the data with other projects, you need to supply it over API. You can try dealing with SQL Views to replace api layer with some sort of abstraction, it would be better, but it would not be the best practices. – Yevgeniy Afanasyev Sep 07 '20 at 01:05
  • I got this project with a poor architecture, the software is exporting throught an API, but it exporting with the primary keys, because the order is generated in the desktop software and exported to the web. I've made an error in my migration that just apear now in a specific scenerio. The only solution for this is to change my primary key – SpaceDogCS Sep 09 '20 at 14:06
  • A primary key as a combination of multiple fields - it is the most unusual. It would be fine to have a unique index for multiple fields... I'm not follwoing. How a primary index is different from any other index? Why do you need a temporary tables? – Yevgeniy Afanasyev Sep 10 '20 at 04:45
  • I don't need a temporary table anymore, I was doing it on a dump way, I couldn't change my primary key because it had records, but what I had to do is just add an unique index before dropping my primary key. Still can't do it without "changing" like you said forget the word "change" – SpaceDogCS Sep 10 '20 at 13:50
8

I'm using MySql and I'm having this problem.

My solution depends that your down() method does exactly what you do in the up() but backwards.

This is what i go:

try{
    Schema::create('table1', function (Blueprint $table) {
        //...
    });
    Schema::create('tabla2', function (Blueprint $table) {
        //...
    });
}catch(PDOException $ex){
    $this->down();
    throw $ex;
}

So here if something fails automatically calls the down() method and throws again the exception.

Instead of using the migration between transaction() do it between this try

Guiman04
  • 723
  • 1
  • 6
  • 12
  • 1
    That is a lot of extra work. I'll extend my answer for you. – Yevgeniy Afanasyev Nov 15 '19 at 02:28
  • though it needs few more lines of code but i lost my sweat! thanks! – MD Alauddin Al-Amin Jul 05 '20 at 16:24
  • This is not correct. If your migrations fails while creating table1 then you down will fail - I suppose your down is schema::dropTable('table1') AND schema::dropTable('table2') and this last one will fail! – Giuseppe Capoluongo Oct 27 '21 at 09:28
  • @GiuseppeCapoluongo it's right there in the answer. "My solution depends that your down() method does exactly what you do in the up() but backwards." – miken32 Jan 01 '22 at 16:32
  • @miken32 That's what he's addressing. If `up()` does `create(a)` and `create(b)`, but it fails on `create(b)`, then only `create(a)` is completed. So when `down()` is started, it will first try `drop(b)`, but as there is no `b`, it will fail down as well. – Sebastian May 10 '22 at 09:07
  • 1
    @Sebastian got it, that makes sense – miken32 May 10 '22 at 13:27
  • It will fail as in it will throw, but it will still put the database back in a correct state as the first drop table is what's needed – Tofandel Aug 29 '22 at 20:13
3

Like Yevgeniy Afanasyev highlighted Taylor Otwell as saying (but an approach I already took myself): have your migrations only work on specific tables or do a specific operation such as adding/removing a column or key. That way, when you get failed migrations that cause inconsistent states like this, you can just drop the table and attempt the migration again.

I’ve experienced exactly the issue you’ve described, but as of yet haven’t found a way around it.

Martin Bean
  • 38,379
  • 25
  • 128
  • 201
1

Just remove the failed code from the migration file and generate a new migration for the failed statement. Now when it fails again the creation of the database is still intact because it lives in another migration file.

Another advantage of using this approach is, that you have more control and smaller steps while reverting the DB.

Hope that helps :D

DAG
  • 6,710
  • 4
  • 39
  • 63
  • did it work that way? Solved the problem in another way? – DAG Dec 14 '13 at 21:20
  • 12
    That was a workarround. But having migrations that make multiple things is also useful, I don't want to have 10 files for a single database change. What I do now, inspired from your answer, is when i get an error in the middle of a migration, just comment out all the parts that were executed correctly. Once I fix the error, I uncomment it, and rollback and migrate again to re-check everything works properly. It's kind of the same effect you said, but without making more files. Still a workarround. – olivarra1 Dec 28 '13 at 12:08
  • olivarra1, I thought you were right on the first place, but later on it turns out that MYSQL doesn't support transactions in DDL. And it changed my mind. I wrote a separate answer to explain my vision. Thanks. – Yevgeniy Afanasyev Nov 19 '15 at 22:18
1

I think the best way to do it is like shown in the documentation:

DB::transaction(function () {
    DB::table('users')->update(['votes' => 1]);

    DB::table('posts')->delete();
});

See: https://laravel.com/docs/5.8/database#database-transactions

Thomas Venturini
  • 3,500
  • 4
  • 34
  • 43
  • 2
    Laravel migrations are already run within a transaction by default for databases that support it. See `withinTransaction` in https://github.com/laravel/framework/blob/5.5/src/Illuminate/Database/Migrations/Migration.php. I haven't looked into the original question enough to know why that didn't work for their particular situation. – orrd Aug 17 '20 at 21:35
1

I know it's an old topic, but there was activity a month ago, so here are my 2 cents.

This answer is for MySql 8 and Laravel 5.8 MySql, since MySql 8, introduced atomic DDL: https://dev.mysql.com/doc/refman/8.0/en/atomic-ddl.html Laravel at the start of migration checks if the schema grammar supports migrations in a transaction and if it does starts it as such. The problem is that the MySql schema grammar has it set to false. We can extend the Migrator, MySql schema grammar and MigrationServiceProvider, and register the service provider like so:

<?php

namespace App\Console;

use Illuminate\Database\Migrations\Migrator as BaseMigrator;

use App\Database\Schema\Grammars\MySqlGrammar;

class Migrator extends BaseMigrator {
    protected function getSchemaGrammar( $connection ) {
        if ( get_class( $connection ) === 'Illuminate\Database\MySqlConnection' ) {
            $connection->setSchemaGrammar( new MySqlGrammar );
        }
        if ( is_null( $grammar = $connection->getSchemaGrammar() ) ) {
            $connection->useDefaultSchemaGrammar();
            $grammar = $connection->getSchemaGrammar();
        }
        return $grammar;
    }
}


<?php

namespace App\Database\Schema\Grammars;

use Illuminate\Database\Schema\Grammars\MySqlGrammar as BaseMySqlGrammar;

class MySqlGrammar extends BaseMySqlGrammar {
    public function __construct() {
        $this->transactions = config( "database.transactions", false );
    }
}


<?php

namespace App\Providers;

use Illuminate\Database\MigrationServiceProvider as BaseMigrationServiceProvider;

use App\Console\Migrator;

class MigrationServiceProvider extends BaseMigrationServiceProvider {   
    /**
     * Register the migrator service.
     * @return void
     */
    protected function registerMigrator() {
        $this->app->singleton( 'migrator', function( $app ) {
            return new Migrator( $app[ 'migration.repository' ], $app[ 'db' ], $app[ 'files' ] );
        } );
        $this->app->singleton(\Illuminate\Database\Migrations\Migrator::class, function ( $app ) {
            return $app[ 'migrator' ];
        } );
    }


<?php

return [
    'providers' => [

        /*
         * Laravel Framework Service Providers...
         */
        App\Providers\MigrationServiceProvider::class,

    ],
];

Of course, we have to add transactions to our database config... DISCLAIMER - Haven't tested yet, but looking only at the code it should work as advertised :) Update to follow when I test...

user1649498
  • 121
  • 3
  • 15
0

Most of the answers overlook a very important fact about a very simple way to structure your development against this. If one were to make all migrations reversible and add as much of the dev testing data as possible through seeders, then when artisan migrate fails on the dev environment one can correct the error and then do

php artisan migrate:fresh --seed

Optionally coupled with a :rollback to test rolling back.

For me personally artisan migrate:fresh --seed is the second most used artisan command after artisan tinker.

Alexandru Eftimie
  • 149
  • 1
  • 1
  • 11
  • How does this help? By rerunning migrations, you also run that which failed and return to original issue – I Want Answers Apr 02 '22 at 17:36
  • It helps by strategically structuring migrations and testing them in ways that prevent this problem from appearing in the first place. By basically catching these errors in the dev environment where it's simply a matter of running migrate:fresh --seed if something goes wrong. – Alexandru Eftimie May 04 '22 at 16:31