skip to Main Content

Yesterday I tried to do what Symfony shouted out in some blog post (https://symfony.com/blog/new-in-symfony-5-2-doctrine-types-for-uuid-and-ulid) but failed hard. I want to store ULID (format "TTTTTTTTTTRRRRRRRRRRRRRRRR") in the database because they are not only sorteable but also contain a timestamp which is just perfect for my application. But when I tell a property to be "type=ulid" then it is stored as UUID (format: "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx") in the database.

I debugged half day long and got so annoyed by this that I started from scratch and still the problem exists.

Where did I go wrong?

(skip to the ULID heading if you like, the following may look long but 50% of it is just basic stuff)

Symfony

The thing I do over and over again taken from https://symfony.com/doc/5.4/setup.html :

  1. stat shyt # does not exist
  2. composer create-project symfony/skeleton:5.4.* shyt
  3. cd shyt; composer require webapp
  4. Do you want to include Docker configuration from recipes? YES (default)
  5. bin/console about shows Symfony Version 5.4.10 and PHP 7.4

ORM

Taken from https://symfony.com/doc/5.4/doctrine.html :

  1. composer require symfony/orm-pack
  2. composer require --dev symfony/maker-bundle
  3. docker-compose up -d

ERROR: could not find an available, non-overlapping IPv4 address pool among the defaults to assign to the network

So I add some lines to the docker-compose.override.yml file:

networks:
  default:
    ipam:
      config:
        - subnet: 10.1.2.1/24

services:
  database:
  # [...] doctrine/doctrine-bundle stuff [...]
  networks:
    default:
      ipv4_address: 10.1.2.3
  1. In ".env" set DATABASE_URL for the host "10.1.2.3"
  2. bin/console doctrine:database:create (silly but as documented)

Could not create database "app" for connection named default
An exception occurred while executing a query: SQLSTATE[42P04]: Duplicate database: 7 ERROR: database "app" already exists

Well, yes. Docker already did this.

  1. The make:entity is postponed until we have ULID capabilities.

ULID

We already lean into https://symfony.com/doc/5.4/components/uid.html (especially section ULIDs) :

  1. composer require symfony/uid
  2. bin/console make:entity Product
  • "someProperty" as "ulid" not nullable
  1. Inspect the Product entity

Looks almost like in the documentation except it has an additional field (the primary key, an integer) and some getter/setter.

  1. bin/console make:migration

Test the ULID entity

In between we use tests to programatically create a DB entry:

  1. composer require phpunit to programatically create a database entry
  2. bin/console --env=test doctrine:migrations:migrate
  3. bin/console --env=test doctrine:database:create
  4. The file "tests/FooTest.php" contains:
<?php

namespace AppTests;

use AppEntityProduct;
use AppRepositoryProductRepository;
use DoctrineORMEntityManager;
use SymfonyBundleFrameworkBundleTestKernelTestCase;
use SymfonyComponentUidUlid;

class FooTest extends KernelTestCase
{
    public function testFoo(): void
    {
        $product = new Product();
        $product->setSomeProperty(new Ulid());
        static::assertNotNull($product->getSomeProperty());

        self::getContainer()->get(ProductRepository::class)
            ->add($product);

        self::getContainer()->get('doctrine.orm.entity_manager')
            ->flush();
    }
}
  1. bin/console --env=test doctrine:query:sql 'TRUNCATE product' just to be sure
  2. bin/phpunit
  3. bin/console --env=test doctrine:query:sql 'SELECT * FROM product'

A UUID is shown instead of a ULID.

Shows ULID instead of UUID in the database

Using ULID as Primary Key

First clean up a bit then do the example shown in https://symfony.com/doc/5.4/components/uid.html#ulids :

  1. rm migrations/* to start again
  2. bin/console --env=test doctrine:database:drop --force
  3. bin/console --env=test doctrine:database:create
  4. bin/console doctrine:database:drop --force
  5. bin/console --env=test doctrine:database:create
  6. Edit "src/Entity/Product.php" to only contain the second ULID example from the documentation:
<?php

namespace AppEntity;

use DoctrineORMMapping as ORM;
use SymfonyComponentUidUlid;
use AppRepositoryProductRepository;

/**
 * @ORMEntity(repositoryClass=ProductRepository::class)
 */
class Product
{
    /**
     * @ORMId
     * @ORMColumn(type="ulid", unique=true)
     * @ORMGeneratedValue(strategy="CUSTOM")
     * @ORMCustomIdGenerator(class="doctrine.ulid_generator")
     */
    private $id;

    public function getId(): ?Ulid
    {
        return $this->id;
    }

    // ...

}

(example from documentation was missing the repository line)

  1. bin/console make:migration
  2. bin/console --env=test doctrine:migrations:migrate
  3. Test is now a bit simpler:
    public function testFoo(): void
    {
        self::getContainer()->get(ProductRepository::class)
            ->add(new Product());

        self::getContainer()->get('doctrine.orm.entity_manager')
            ->flush();
    }
  1. bin/phpunit (Risky is ok)
  2. bin/console --env=test doctrine:query:sql 'SELECT * FROM product'

Again those UUID instead of ULID

Database shows UUID instead of ULID

2

Answers


  1. I’m trying out the ULID as well and followed the same documentation (https://symfony.com/blog/new-in-symfony-5-2-doctrine-types-for-uuid-and-ulid). I noticed you did one thing different in your code compared to the documentation.

    In the entity change:

    * @ORMCustomIdGenerator(class="doctrine.ulid_generator")
    

    to

    * @ORMCustomIdGenerator(class=UlidGenerator::class)
    

    And add the following

    use SymfonyBridgeDoctrineIdGeneratorUlidGenerator;
    

    This should the make the ULID work.

    The dump of this would be something like this (sorry this is a dump in the console as I was testing the ulid, but I hope it gives you the information you need):

    ^ SymfonyComponentUidUlid^ {#842
      #uid: "01G7MCSCANWA2XXZ1NT1TZV6KQ"
      toBase58: "1BoCbKTaPcpbAuQVieEW54"
      toRfc4122: "0181e8cc-b155-e285-defc-35d075fd9a77"
      time: "2022-07-10 15:48:57.813 UTC"
    }
    

    In the database it looks as followed:
    Ulid in mariaDB

    I hope this helps.

    Login or Signup to reply.
  2. It’s bit late but for anyone whom facing this issue, I too have the database showing UUID instead of ULID and it seems to be the expected behavior from doctrine since you can use UUID/ULID interchangeably meaning even if you have UUID stored in the database but your entity is mapped on ULID you will have an ULID when retrieving the object from the database, also you can retrieve your same object by using ULID or UUID.

    so for instance I have a User entity having ULID for its identifier so the stored object will have uuid as below :

    018477e6-eebc-164c-12e3-22ca8f1a88f3    [email protected]    []  
    

    if I retrieve my user with that UUID I’ll get :

    AppEntityUser {#430 ▼
     -id: "01GHVYDVNW2S615RS2SA7HN27K"
     -email: "[email protected]"
     -roles: []
     #createdAt: DateTime @1668458933 {#423 ▶}
     #updatedAt: DateTime @1668458998 {#428 ▶}
     -password: "$2y$13$/2i9Ovc2lCQBRfSVgsnmoul1FhF.Kyki3irF6GQvrMrjacQX6ees6"
     -isVerified: true
    }
    

    now if you use that ULID to retrieve your user it will work too !

    going further if you inspect that UUID you’ll see that the returned object has that uuid converted to base 32 :

    bash-5.1$ bin/console uuid:inspect 018477e6-eebc-164c-12e3-22ca8f1a88f3
    ----------------------- --------------------------------------
    Label                   Value
    ----------------------- --------------------------------------
    Version                 1                                     
      toRfc4122 (canonical)   018477e6-eebc-164c-12e3-22ca8f1a88f3  
      toBase58                1BsMRvKcgozP4Kw2m4Fb1C                
      toBase32                01GHVYDVNW2S615RS2SA7HN27K
    ----------------------- -------------------------------------- 
    

    and finally you can get that stored uuid by converting it to refc4122 as you can see below:

    bin/console ulid:inspect 01GHVYDVNW2S615RS2SA7HN27K
    ---------------------- --------------------------------------
    Label                  Value
    ---------------------- --------------------------------------
    toBase32 (canonical)   01GHVYDVNW2S615RS2SA7HN27K
    toBase58               1BsMRvKcgozP4Kw2m4Fb1C
    toRfc4122              018477e6-eebc-164c-12e3-22ca8f1a88f3
    ---------------------- --------------------------------------
    Time                   2022-11-14 20:48:53.948 UTC
    ---------------------- --------------------------------------
    

    I’m not sure why doctrine doesn’t just store the ULID but its current behavior is not blocking you from using ULID in your project.
    Hope this help !

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search