15

So I've done a bunch of Doctrine2 migrations (https://github.com/doctrine/migrations) but I have a question for a new migration I'm trying to do.

I've been digging into the library a little and I see that $this->addSql() is used to build a list of SQL to execute and then it gets executed later.

I wanted to do something where I select some data, iterate over the rows, insert new data based on that, and then delete the data I selected. This lends itself to the DBAL library pretty easily, but I'm wondering, can I use the protected $connection in a migration safely? Or is that bad because it would execute statements before any of my $this->addSql() SQL gets executed? Also it seems like this would break the dry-run setting from what I've seen in the code. Has anyone had any experience with this type of migration? Are there any best practices?

The following is the migration I want to do, but I'm not confident that this is supported by Doctrine Migrations:

public function up(Schema $schema)
{
    // this up() migration is autogenerated, please modify it to your needs
    $this->abortIf($this->connection->getDatabasePlatform()->getName() != "mysql");

    $this->addSql("ALTER TABLE article_enclosures ADD is_scrape TINYINT(1) NOT NULL");
    $this->addSql("ALTER TABLE images DROP FOREIGN KEY FK_E01FBE6AA536AAC7");

    // now lets take all images with a scrape and convert the scrape to an enclosure
    // 
    // Select all images where not scrape_id is null (join on article_image_scrape)
    // for each image:
    //     insert into article_enclosures
    //     update image set enclosure_id = new ID
    //     delete from article_image_scrape where id...
    //
    // insert into article_enclosures select article_image_scrapes...

    $sql = "SELECT i.id img_id, e.* FROM images i JOIN article_image_scrapes e ON i.scrape_id = e.id";
    $stmt = $this->connection->prepare($sql);
    $stmt->execute();
    $scrapesToDelete = array();
    while ($row = $stmt->fetch()) {
        $scrapeArticle = $row['article_id'];
        $scrapeOldId = $row['id'];
        $scrapeUrl = $row['url'];
        $scrapeExtension = $row['extension'];
        $scrapeUrlHash = $row['url_hash'];
        $imageId = $row['image_id'];

        $this->connection->insert('article_enclosures', array(
            'url' => $scrapeUrl,
            'extension' => $scrapeExtension,
            'url_hash' => $scrapeUrlHash
        ));

        $scrapeNewId = $this->connection->lastInsertId();

        $this->connection->update('images', array(
            'enclosure_id' => $scrapeNewId,
            'scrape_id' => null
        ), array(
            'id' => $imageId
        ));

        $scrapesToDelete[] = $scrapeOldId;
    }

    foreach ($scrapesToDelete as $id) {
        $this->connection->delete('article_image_scrapes', array('id' => $id));
    }

    $this->addSql("INSERT INTO article_scrapes (article_id, url, extension, url_hash) "
            ."SELECT s.id, s.url, s.extension, s.url_hash"
            ."FROM article_image_scrapes s");

    $this->addSql("DROP INDEX IDX_E01FBE6AA536AAC7 ON images");
    $this->addSql("ALTER TABLE images DROP scrape_id, CHANGE enclosure_id enclosure_id INT NOT NULL");
}
Matt
  • 5,478
  • 9
  • 56
  • 95
  • I decided to just do separate migrations before and after this one with the necessary `addSql` calls so that the ordering is correct. – Matt Apr 27 '12 at 19:22
  • Have you tried it? Looks ok to me – eddy147 Aug 28 '13 at 10:25
  • I think so, but this was a year ago. I believe the original issue still stands. That is, using `->addSql()` calls, those will be executed last. And the `dry-run` will still run your direct manipulation. So it still seems hacky (but I could be wrong, never did get an answer and I don't remember much about this any more). – Matt Aug 29 '13 at 00:50
  • I had a somewhat similar issue, ended up using a stored procedure so it could be added added as part of the `addSql()` statement. I used section 4.1 of https://www.sgalinski.de/typo3-agentur/technik/how-to-work-with-doctrine-migrations-in-symfony as part of the base for what I needed to do – Jon Feb 02 '21 at 12:31

2 Answers2

20

You can use the $connection like this

$result = $this->connection->fetchAssoc('SELECT id, name FROM table1 WHERE id = 1');
$this->abortIf(!$result, 'row with id not found');
$this->abortIf($result['name'] != 'jo', 'id 1 is not jo');
// etc..

You should only read the database and not use the connection to make update/delete so it won't break the dry-run option.

In your example, you should do two migrations. The first will do the two alter table. The second will do the "images with a scrape and convert the scrape to an enclosure" routine. Using multiple migration is easier to revert them if something goes wrong.

pmaruszczyk
  • 2,157
  • 2
  • 24
  • 49
Nico
  • 6,395
  • 4
  • 25
  • 34
13

FYI, latest docs show this example that is even better with a "postUp" method

http://symfony.com/doc/current/bundles/DoctrineMigrationsBundle/index.html

// ...
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

class Version20130326212938 extends AbstractMigration implements ContainerAwareInterface
{

    private $container;

    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }

    public function up(Schema $schema)
    {
        // ... migration content
    }

    public function postUp(Schema $schema)
    {
        $em = $this->container->get('doctrine.orm.entity_manager');
        // ... update the entities
    }
}
Matt
  • 5,478
  • 9
  • 56
  • 95
  • 12
    I'd like to add that you should be careful when dealing with `EntityManager` in Doctrine Migrations environment. EntityManager will use current entity definitions which might be different from those used in the original migration. I would refrain from using EntityManager in migrations at all. – SteveB Jul 25 '17 at 08:22
  • @SteveB maybe using the `DBAL` layer can be a solution. We have an access to the property `connection` which is a `\Doctrine\DBAL\Connection` – Adrien G Feb 14 '18 at 16:36
  • 1
    @AdrienG yeah, my remark is more about the issue of using `EntityManager` as a shortcut. While it looks perfectly fine, it isn't. It will cause issues down the line when one will update entity definition eventually. Using anything not directly related to the current state of application is perfectly fine imo. – SteveB Feb 15 '18 at 21:29