BenMorel
BenMorel

Reputation: 36514

One to one relationship on two tables sharing primary key

I want two tables, Person and Address, to share their primary key:

CREATE TABLE Person (id INT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY);
CREATE TABLE Address (personId INT UNSIGNED NOT NULL PRIMARY KEY);

I'm trying to model the idea that when you create a Person, you're also creating an Address for it.

I came up with the following mapping:

class Person
{
    /**
     * @Id
     * @Column(type="integer")
     * @GeneratedValue
     */
    private $id;

    /**
     * @OneToOne(targetEntity="Address", cascade={"all"})
     * @JoinColumn(name="id", referencedColumnName="personId")
     */
    private $address;

    public function __construct() {
        $this->address = new Address($this);
    }
}

class Address
{
    /**
     * @Id
     * @OneToOne(targetEntity="Person")
     * @JoinColumn(name="personId", referencedColumnName="id")
     */
    private $person;

    public function __construct(Person $person) {
        $this->person = $person;
    }
}

When I try to persist Person, I would expect it to cascade to Address, but I get the following exception:

Doctrine\ORM\ORMException

Entity of type Address has identity through a foreign entity Person, however this entity has no identity itself. You have to call EntityManager#persist() on the related entity and make sure that an identifier was generated before trying to persist Address. In case of Post Insert ID Generation (such as MySQL Auto-Increment or PostgreSQL SERIAL) this means you have to call EntityManager#flush() between both persist operations.

I can't explicitly call flush(), because I'm not explicitly persist()ing Address, instead I'm expecting Person to cascade persist Address.

So as far as I understand it, it should be Doctrine's job to flush() at the right moment to get the id.

This one-to-one relationship cascades fine when using different auto_increment ids for both Person and Address (but then I have to add an addressId to Person).

My aim is to make them share the same id.

Is that possible?

Upvotes: 4

Views: 4412

Answers (3)

Hajri Aymen
Hajri Aymen

Reputation: 630

You can use the symfony listener and remove the auto increment strategy from adress entity :

First create a Listner and crete your listner class like AdressGenerator.php

<?php

namespace Zodiac\HelperBundle\Listener;


use Doctrine\ORM\Event\LifecycleEventArgs;
use AAA\XXXBundle\Entity\Person;
use AAA\XXXBundle\Entity\Adress;

class AdressGenerator
{
    public function prePersist(LifecycleEventArgs $args)
    {
        // This function is called just beforethe event Persist 
        // The AdressGeneratorlistner is defined in the config.yml file
        // Get the entity manager and the entity from le LifecycleEventArgs
        $em = $args->getEntityManager();
        $entity = $args->getEntity();


        if ($entity instanceof Person) {
            //Create the Adress entity
            $adress = new Adress();
            $adress->setId($entity->getId()); 
            $em->persist($adress);
            $entity->setAdress($adress);
            $em->persist($entity);
            $em->flush();

        }

    }
}

Then add your listener service in the config.yml:

services:
AdressGenerator.listener:
    class: AAA\xxxBundle\Listener\AdressGenerator
    tags:
        - { name: doctrine.event_listener, event: prePersist }

Upvotes: 2

Alain
Alain

Reputation: 36954

I found a solution but I don't know if that's the best (it looks heavy to me).

That's not a mapping problem, but the way we persist :

Instead of doing:

$em->persist($person);
$em->flush();

We should do:

$em->transactional(function($em) use ($person)
{
    $address = $person->getAddress();
    $person->setAddress(null);
    $em->persist($person);
    $em->flush();
    $address->setPerson($person);
    $em->persist($address);
    $em->flush();
});

If somebody has a cleaner solution, (s)he'll get the bounty :-).

Upvotes: 0

manix
manix

Reputation: 14747

How about use Identity through foreign Entities in doctrine 2.1?

In your scenario you can use @ManyToOne instead @OneToOne association.

Edit

The association is persisted when you persist the owning side. In your mapping, that is the Address. Change your mapping so the owner has the foreign key and everything should work fine.

/**
 * @OneToOne(targetEntity="Address", cascade={"all"}, inversedBy="person")
 * @JoinColumn(name="id", referencedColumnName="personId")
 */
private $address;

In other words, if Address should be owner by the Person then you must persist Address at first place to in order to persist the association. If you want the association to be persisted with Person, then you should mark the Person to be the owning side and Address to be the inverse side:

/**
 * @Id
 * @OneToOne(targetEntity="Person", inversedBy="address")
 * @JoinColumn(name="personId", referencedColumnName="id")
 */
private $person;

Upvotes: 3

Related Questions