uuid icon indicating copy to clipboard operation
uuid copied to clipboard

Most efficient type to use Uuids as identities in doctrine entities

Open flaushi opened this issue 6 years ago • 8 comments

Hi, I have switched to Uuids as primary keys for my doctrine project. All entities have such a field:

/**
     * @var UuidInterface
     *
     * @ORM\Id
     * @ORM\Column(type="uuid", unique=true)
     */
    protected $id;

    public function __construct() {
        $this->id = Uuid::uuid4();
    }

Attached you find an profiler screenshot. bildschirmfoto 2018-12-19 um 09 04 32

As one can see, almost 10% of the time are spent in the unserialize method of this lib. Would it maybe be more efficient to store as String? Like so:

/**
     * @var string
     *
     * @ORM\Id
     * @ORM\Column(type="string", length=64, unique=true)
     */
    protected $id;

    public function __construct() {
        $this->id = Uuid::uuid4()->toString();
    }

Please note, I want to keep my code and model compatible with Postgres and MySQL, which is the case currently. @ocramius

flaushi avatar Dec 19 '18 08:12 flaushi

I should add that I have enabled doctrine's second level cache (redis backend). This component caches database entities according to their identity in redis. I think the Uuid::de/serialisation is a big and unneccessary overhead here. The question is at which side one should address it: Should I change my model, or maybe one should stop recommending my type of usage of Uuid at the doctrine side?

flaushi avatar Dec 19 '18 08:12 flaushi

I would use a prePersist listener and only generate an UUID right before you persist. Because every initiation of an entity would result in generation of a new UUID while you already have one generated persisted.

martijnhartlief avatar Apr 15 '20 12:04 martijnhartlief

Because every initiation of an entity would result in generation of a new UUID

AFAIK, the constructor that I write into my entity class is not called during doctrine's hydration phase. https://www.doctrine-project.org/projects/doctrine-orm/en/2.7/reference/architecture.html#entities

flaushi avatar Apr 15 '20 13:04 flaushi

Most often I set the id property as being nullable, and lazy create the new UUID if not set in the getId() method, such as:

class Foo
{
    private ?UuidInterface $id = null;

    public function getId(): UuidInterface
    {
        return $this->id ?? ($this->id = Uuid::uuid4());
    }
}

Yet UUID creation is very, very slow I noticed (especially the fromString() method).

I'm not sure my method is the best, but it fits right in most our use cases, when hydrating our entities we have the entity from database, and when creating a new one, the new UUID is created application-side for sending to the database insert query transparently.

pounard avatar Apr 15 '20 13:04 pounard

If we only use the uuid's for getting a long random identity of database entities, what is the advantage of having a uuid class instance at all? Wouldn't it be sufficient to just have the string (see my second proposal)?

flaushi avatar Apr 15 '20 13:04 flaushi

I would always recommend to use types as much as you can, this way to are sure that an invalid UUID never will reach your business code. And this is enforced by both the UUID validation when created, but also by PHP static typing whenever it exists.

Each time we have codified identifiers (with business meaning for our clients) we create immutable value objects capable of formating, validating, fetching business meaning etc... from it, and thus even when it's a simple string. It makes code much more robust, especially when you drag those values all over your code, repository methods that takes an id, business layer, etc... It globally makes the code self-documented and less error-prone.

pounard avatar Apr 15 '20 13:04 pounard

Can these be of some help @flaushi ? https://github.com/ramsey/uuid/issues/251, https://github.com/ramsey/uuid/issues/63,

devrck avatar May 01 '20 14:05 devrck

Thanks @devrck, but AFAIU these issues are about binary representations at the database side.

I was wondering about the doctrine side. If I declare my ids as recommended

     /**
     * @var UuidInterface
     *
     * @ORM\Id
     * @ORM\Column(type="uuid", unique=true)
     */

then doctrine hydrates a Uuid instance (although postgres will store the uuid as a string). This is overhead in my opinion, and just makes all equality comparisons more costly.

flaushi avatar May 01 '20 16:05 flaushi