Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

doctrine is fetching bigints as string - how to prevent this

I have the following issue:

Doctrine is suppose to update entities that were changed. The problem is that for some reason (maybe legacy 32 bit systems?) the bigint data type is treated as string (as you can see below - this is the bigint type class in doctrine, there are also multiple other conversions to string in doctrine code).

<?php

namespace Doctrine\DBAL\Types;

use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;

/**
 * Type that maps a database BIGINT to a PHP string.
 */
class BigIntType extends Type implements PhpIntegerMappingType
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Type::BIGINT;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return $platform->getBigIntTypeDeclarationSQL($fieldDeclaration);
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return ParameterType::STRING;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return $value === null ? null : (string) $value;
    }
}

This results in updating data that should not be updated because the unit of work checker compares data as strict (as it should) resulting in differences (code below).

// skip if value haven't changed
if ($orgValue === $actualValue) { // $orgValue = "3829784117300", $actualValue = 3829784117300
    continue;
}

The end result for this is that the following code:

$product = $productRepo->find($id);
$product->setOwnerId($ownerId); // $ownerId = 3829784117300
$this->em->flush();

Is generating a query that does... nothing basically except stressing db (in my case I have a few tens of millions of those per day). The solution for the particular case above...

$product = $productRepo->find($id);
if ((int)$product->getOwnerId() !== $ownerId) {
    $product->setOwnerId($ownerId); // $ownerId = 3829784117300
}
$this->em->flush();

Simple right? But... what do you do when you have 2 bigints? Ok... 2 conditions... not a big deal. But what if they are... 90? Ok... we can use reflection go through entity properties and check all.

But... what if somewhere in the relation chain there is another entity that needs to be checked? And the complete solution is that we have to recursively check every attribute of the entity and its children and check for bigints.

But... isn't that what the doctrine unit of work is for? Why do I need to reparse the whole entity and check for something that is already checked for just because bigint is treated as string (resulting in basically duplicating a huge chunk of doctrine code)?

And now the question... how to go around this (bare in mind that I'm not asking for a particular solution for a simple case, I'm asking for a general solution that can be applied to a big code base that is supposed to be mentained for years to come - as you can see above I have solutions but I'm not ok with half jobs and fragile code unless there is really no other way)? I'm looking for maybe a setting that I missed that makes doctrine treat integers like integers and not as strings... stuff like that.

like image 438
zozo Avatar asked Oct 08 '19 12:10

zozo


1 Answers

One solution would be to override Doctrine's implementation of bigint type with your own. First, create a class identical to Doctrine's BigIntType, except replacing the cast to string with a cast to int:

class MyBigIntType extends Type
{
    /**
     * {@inheritdoc}
     */
    public function getName()
    {
        return Type::BIGINT;
    }

    /**
     * {@inheritdoc}
     */
    public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform)
    {
        return $platform->getBigIntTypeDeclarationSQL($fieldDeclaration);
    }

    /**
     * {@inheritdoc}
     */
    public function getBindingType()
    {
        return \PDO::PARAM_STR;
    }

    /**
     * {@inheritdoc}
     */
    public function convertToPHPValue($value, AbstractPlatform $platform)
    {
        return (null === $value) ? null : (int)$value;
    }
}

Then, register the type in config.yml or doctrine.yaml:

doctrine:
    dbal:
        types:
            bigint: MyBigIntType

I tested this with Symfony 2.8, and this caused to use MyBigIntType for all fields that were of type bigint. Should work with later Symfony versions as well.

like image 158
Bartosz Zasada Avatar answered Nov 09 '22 06:11

Bartosz Zasada