doctrine / orm

Doctrine Object Relational Mapper (ORM)
https://www.doctrine-project.org/projects/orm.html
MIT License
9.93k stars 2.51k forks source link

Persisting Entity with OneToOne relation #10663

Open jakublabno opened 1 year ago

jakublabno commented 1 year ago

Bug Report

Q A
BC Break no
Version 2.14.3

Summary

Invalid parameters binding while persisting new entity with OneToOneRelation

Current behavior

I have two entities: Invoice and InvoiceNumeration

<?php

declare(strict_types=1);

namespace Payments\Invoicing\Domain;

use Core\Database\DateTimeTrait;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
use Doctrine\ORM\Mapping\Id;
use JMS\Serializer\Annotation as Serializer;
use OpenApi\Attributes as OA;
use Payments\Domain\PaymentId;

#[OA\Schema]
#[Entity]
#[ORM\Table(name: 'invoices')]
#[HasLifecycleCallbacks]
class Invoice
{
    use DateTimeTrait;

    #[Id]
    #[ORM\Column(type: 'InvoiceIdType')]
    #[Serializer\Exclude]
    private InvoiceId $id;

    #[ORM\Column(name: 'payment_id', type: 'PaymentIdDoctrineType')]
    #[Serializer\Exclude]
    private PaymentId $paymentId;

    #[OA\Property]
    #[ORM\Column(name: 'type')]
    private string $invoiceType;

    #[OA\Property]
    #[ORM\Embedded]
    #[Serializer\Exclude]
    private InvoiceDetails $invoiceDetails;

    #[OA\Property]
    #[ORM\Column]
    private ?string $file = null;

    #[ORM\OneToOne(targetEntity: InvoiceNumeration::class, cascade: ['DETACH'], fetch: 'EAGER')]
    #[ORM\JoinColumn(name: 'id', referencedColumnName: 'invoice_id', nullable: true)]
    private InvoiceNumeration $numeration;

    public function __construct(PaymentId $paymentId, InvoiceDetails $invoiceDetails, InvoiceType $invoiceType)
    {
        $this->id = InvoiceId::generate();
        $this->paymentId = $paymentId;
        $this->invoiceDetails = $invoiceDetails;
        $this->invoiceType = $invoiceType->getName();
    }

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

    public function setFile(string $file): void
    {
        $this->file = $file;
    }

    public function getDetails(): InvoiceDetails
    {
        return $this->invoiceDetails;
    }

    public function getType(): InvoiceType
    {
        return InvoiceTypeFactory::fromName($this->invoiceType);
    }
}
<?php

declare(strict_types=1);

namespace Payments\Invoicing\Domain;

use Core\Database\DateTimeTrait;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\Id;
use JMS\Serializer\Annotation as Serializer;

#[Entity]
#[ORM\Table(name: 'invoice_numeration')]
#[ORM\HasLifecycleCallbacks]
class InvoiceNumeration
{
    use DateTimeTrait;

    #[Id]
    #[ORM\Column(name: 'invoice_id', type: 'InvoiceIdType')]
    #[Serializer\Exclude]
    private InvoiceId $invoiceId;

    #[ORM\Column(length: 4)]
    private int $year;

    #[ORM\Column(length: 2)]
    private string $month;

    #[ORM\Column]
    private string $type;

    #[ORM\Column(length: 6)]
    private string $number;

    #[Serializer\Accessor(getter: 'getNumber')]
    private string $fullNumber;

    public function __construct(InvoiceId $invoiceId, InvoiceNumber $invoiceNumber, InvoiceType $invoiceType)
    {
        $this->invoiceId = $invoiceId;
        $this->type = $invoiceType->shortName();
        $this->year = $invoiceNumber->getYear();
        $this->month = $invoiceNumber->getMonth();
        $this->number = $invoiceNumber->getOrdinalNumber();
    }

    public function getNumber(): InvoiceNumber
    {
        return new InvoiceNumber("$this->type/$this->year/$this->month/$this->number");
    }
}

While I'm creating Invoice entity, doctrine throws a pdo exception

START TRANSACTION
INSERT INTO invoices (id, payment_id, type, file, date_created, date_updated, invoiceDetails_seller_name, invoiceDetails_seller_country, invoiceDetails_seller_city, invoiceDetails_seller_zipcode, invoiceDetails_description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ROLLBACK
An exception occurred while executing a query: SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'id' cannot be null: #0 /app/vendor/doctrine/dbal/src/Connection.php(1907): Doctrine\\DBAL…

When checking deeper, it seems id is not being passed to PDO, I see null there. obraz

How to reproduce

Here is DDL:

create table invoices
(
    id                            binary(16)               not null
        primary key,
    payment_id                    binary(16)               not null,
    file                          varchar(255)             null,
    date_created                  datetime                 not null,
    date_updated                  datetime                 null,
    invoiceDetails_seller_name    varchar(255)             not null,
    invoiceDetails_seller_country varchar(255)             not null,
    invoiceDetails_seller_city    varchar(255)             not null,
    invoiceDetails_seller_zipcode varchar(255)             not null,
    invoiceDetails_description    varchar(255)             not null,
    type                          enum ('proforma', 'vat') not null
);

create table invoice_numeration
(
    invoice_id   binary(16)              not null
        primary key,
    year         int                     not null,
    month        varchar(2)              not null,
    type         enum ('FV', 'PROFORMA') not null,
    number       int                     not null,
    date_created datetime                not null,
    date_updated datetime                null,
    constraint invoice_numeration_pk
        unique (year, number, month, type)
);

Of course, everything worked before adding OneToOne and JoinColumn definition. Maybe I'm doing something wrong?

I would not use JoinColumn, because it seems not be necessary, but when I don't it throws another exception

Could not resolve type of column \"id\" of class \"Payments\\Invoicing\\Domain\\InvoiceNumeration\"

However, it's the same type as in Invoice Entity Id

mpdude commented 1 year ago

Thank you for providing a code example! That makes it easier to understand the situation.

As a first step, could you please try to simplify the code a bit more?