Doctrine nullable one-to-one relationship still wants to create unique index - php

I have a Person entity which has two relations (hometown and current) to Location table. Both of these fields can be null, otherwise they must exist in the Location table:
class Person {
.....
/**
* #var Location
* #ORM\OneToOne(targetEntity="Location")
* #ORM\JoinColumn(name="hometown_id", referencedColumnName="id",nullable=true)
**/
protected $hometown;
/**
* #var Location
* #ORM\OneToOne(targetEntity="Location")
* #ORM\JoinColumn(name="current_id", referencedColumnName="id", nullable=true)
**/
protected $current;
....
}
Now, I want to update my db schema, based on doctrine:schema:update --dump-sql output, but it creates problems:
CREATE UNIQUE INDEX UNIQ_8D93D6494341EE7D ON person (hometown_id);
CREATE UNIQUE INDEX UNIQ_8D93D649B8998A57 ON person (current_id);
I cannot define these indexes as there are more than one null row in the table.
Would you please help me?

A OneToOne relationship is unique as it would mean that only one person could be assigned to one location and one location to one person.
In your scenario you would want one person to have multiple locations and one location could have multiple person(s). This would be a ManyToMany relationship.
In Doctrine when you use a ManyToMany you will specify a JoinTable that Doctrine will manage (You don't have to create an entity for a JoinTable). The JoinTable breaks down the ManyToMany to something like a OneToMany such as one person to many location(s) as shown in example below. The JoinTable will store the values you want when they apply.
/**
* #ORM\ManyToMany(targetEntity="Location")
* #ORM\JoinTable(name="hometown_location",
* joinColumns={#ORM\JoinColumn(name="person_id", referencedColumnName="id", unique=true)},
* inverseJoinColumns={#ORM\JoinColumn(name="location_id", referencedColumnName="id")}
* )
**/
protected $hometown;
/**
* #ORM\ManyToMany(targetEntity="Location")
* #ORM\JoinTable(name="current_location",
* joinColumns={#ORM\JoinColumn(name="person_id", referencedColumnName="id", unique=true)},
* inverseJoinColumns={#ORM\JoinColumn(name="location_id", referencedColumnName="id")}
* )
**/
protected $current;
public function __construct() {
$this->hometown = new \Doctrine\Common\Collections\ArrayCollection();
$this->hometown = new \Doctrine\Common\Collections\ArrayCollection();
}
If there is no location to assign to hometown or current that is fine, no space is taken up.
When you do have a location to assign to either hometown or current it will have to be a valid location from the location table.

It looks like you are looking for FOREIGN KEY
http://dev.mysql.com/doc/refman/5.6/en/create-table-foreign-keys.html
ALTER TABLE `Person` ADD INDEX ( `hometown_id` ) ;
ALTER TABLE `Person` ADD FOREIGN KEY ( `hometown_id` ) REFERENCES `Location` ( `id` )
ON DELETE RESTRICT ON UPDATE RESTRICT ;
ALTER TABLE `Person` ADD INDEX ( `current_id` ) ;
ALTER TABLE `Person` ADD FOREIGN KEY ( `current_id` ) REFERENCES `Location` ( `id` )
ON DELETE RESTRICT ON UPDATE RESTRICT ;

Related

Select a column (having foreign key) without joining two tables

I want select a column (which has a foreign key constraint) without creating joins on tables. I have two tables named eventupdate and eventcategory. The event column is common in both tables. whenever I try the following code it gives an error.
Please give some suggestion. I don't want to create a join.
$qb2 = $this->em->createQueryBuilder();
$from = 'Entities\EventCategory cat';
$qb2->add('from',$from)
->select('cat.event')
->Where('cat.id=3);
$query=$qb2->getQuery();
There are two options that I can see:
HINT_INCLUDE_META_COLUMNS together with ArrayHydrator
$query = $queryBuilder->getQuery();
$query->setHint(\Doctrine\ORM\Query::HINT_INCLUDE_META_COLUMNS, true);
var_dump($query->getArrayResult()); // Will return array with raw foreign key column name => value, e.g. user_id => 5
Create separate property in Entities\EventCategory which has the foreign key as primitive type
/**
* #var User
*
* #ManyToOne(targetEntity="User")
* #JoinColumn(name="user_id", referencedColumnName="user_id")
*/
private $user;
/**
* #var int
*
* #Column(name="user_id", type="integer", nullable=false)
*/
private $userId;

One-To-Many relationship deletes foreign key in Doctrine

This is driving me crazy.
A Client can have many Vehicles.
This is a one to many relationship. When trying to save the entities I get an error saying that the foreign key is null. When I remove the Doctrine relation and store the Vehicle separately everything is working fine.
This is how I created the relation:
class Vehicle {
...
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="vehicles")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
*/
public $client;
}
class Client {
...
public function __construct()
{
parent::__construct();
$this->vehicles = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="Vehicle", mappedBy="client", cascade={"persist"})
*/
private $vehicles;
}
I try to save the entities like this:
$client = new Client();
$vehicle = new Vehicle();
$client->getVehicles()->add($vehicle);
$em->persist($client);
$em->flush();
Next I get a PDO exception saying that client_id can't be null on the Vehicle table.
It seems like Doctrine is not copying the foreign key correctly.
What am I doing wrong?
According to their docs:
It is not possible to use join columns pointing to non-primary keys.
Doctrine will think these are the primary keys and create lazy-loading proxies with the data, which can lead to unexpected results. Doctrine can for performance reasons not validate the correctness of this settings at runtime but only through the Validate Schema command.
Obviously you need a different approach.
A solution is given in their example:
CREATE TABLE product (
id INTEGER,
name VARCHAR,
PRIMARY KEY(id)
);
CREATE TABLE product_attributes (
product_id INTEGER,
attribute_name VARCHAR,
attribute_value VARCHAR,
PRIMARY KEY (product_id, attribute_name)
);
This schema should be mapped to a Product Entity as follows:
class Product
{
private $id;
private $name;
private $attributes = array();
}
Where the attribute_name column contains the key and attribute_value contains the value of each array element in $attributes.

Remove rows from child table with Class Table Inheritance in Doctrine

I have made a really basic example with two models.
"Singer" extends from "Person"
I am using class table Inheritance with these two models:
<?php
namespace models;
/**
* #Table(name="persons")
* #entity
* #InheritanceType("JOINED")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap({"singer" = "\models\Singer"})
*/
abstract class Person {
/**
* #Id
* #Column(type="integer", nullable=false, name="id_person")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #column(type="string", nullable=false) */
protected $name;
The singer model looks like:
namespace models;
/**
* #Table(name="singers")
* #entity
*/
class Singer extends Person{
/** #column(type="string", nullable=false) */
protected $gender;
}
Workflow
Consider this scenario.
In the db I have these rows:
persons table:
id_person | name | type
-----------------------
1 john singer
singers table:
id_person | gender
------------------
1 pop
I proceed to remove this singer:
$singer = $em->find('models\Singer', 1);
$em->remove($singer);
$em->flush();
After execute the code above, I check again the database and I found this:
persons table:
id_person | name | type
-----------------------
(empty)
singers table:
id_person | gender
------------------
1 pop
As you note, the row from child table was not removed as expected.
So, after searching in doctrine's documentation, it states:
When you do not use the SchemaTool to generate the required SQL you should know that deleting a class table inheritance makes use of the foreign key property ON DELETE CASCADE in all database implementations. A failure to implement this yourself will lead to dead rows in the database.
So, I proceed to alter persons table as below:
ALTER TABLE persons
ADD CONSTRAINT fk_persons_1
FOREIGN KEY (id_person)
REFERENCES singers (id_person)
ON DELETE CASCADE
ON UPDATE NO ACTION;
Now, the problems get complicated:
when I remove a singer, the information still there, even the persons table was altered in order to delete from singers table too.
When I try to insert a new singer like
$singer = new \models\Singer('Zara', 'rock');
$em->persist($singer);
$em->flush();
it throws an exception:
Fatal error: Uncaught exception 'PDOException' with message 'SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`practica`.`persons`, CONSTRAINT `fk_persons_1` FOREIGN KEY (`id_person`) REFERENCES `singers` (`id_person`) ON DELETE CASCADE ON UPDATE NO ACTION)' in /var/www/html/doctrine/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php:138 Stack trace: #0 /var/www/html/doctrine/vendor/doctrine/dbal/lib/Doctrine/DBAL/Statement.php(138): PDOStatement->execute(NULL) #1 /var/www/html/doctrine/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php(165): Doctrine\DBAL\Statement->execute() #2 /var/www/html/doctrine/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(929): Doctrine\ORM\Persisters\JoinedSubclassPersister->executeInserts() #3 /var/www/html/doctrine/vendor/doctrine/orm/lib/Doctrine/ORM/UnitOfWork.php(318): Doctrine\ORM\UnitOfWork->executeInserts(Object(Doctrine\ORM\Mapping\ClassMetadata)) #4 /var/www/html/doctrine/vendor/doct in /var/www/html/doctrine/vendor/doctrine/dbal/lib/Doctrine/DBAL/DBALException.php on line 47
So, basically, all I need is to remove the information in child table too in MySQL db. But I do not get it.
Try reversing the FOREIGN KEY i.e. remove it from your persons table and add it to your singers table
ALTER TABLE singers
ADD CONSTRAINT fk_singers_1
FOREIGN KEY (id_person)
REFERENCES persons (id_person)
ON DELETE CASCADE
ON UPDATE NO ACTION;
I think you probably have to ask yourself if you really need a field relating to multiple tables.
In any case, maybe is better to do it through a relation instead of using inheritance and a discriminator map, that works well with objects using the same table, but since in your case they are different tables it is probably better to use a relation instead:
<?php
namespace models;
/**
* #Table(name="persons")
* #entity
*/
class Person {
/**
* #Id
* #Column(type="integer", nullable=false, name="id_person")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #column(type="string", nullable=false) */
protected $name;
/**
* #OneToOne(targetEntity="Singer")
* #JoinColumn(name="id_person", referencedColumnName="id_person")
*/
private $singer;
namespace models;
/**
* #Table(name="singers")
* #entity
*/
class Singer{
/**
* #Id
* #Column(type="integer", nullable=false, name="id_person")
* #GeneratedValue(strategy="AUTO")
*/
protected $id;
/** #column(type="string", nullable=false) */
protected $gender;
/**
* #OneToOne(targetEntity="Person", cascade={"persist", "remove", "merge"}, orphanRemoval=true)
* #JoinColumn(name="id_person", referencedColumnName="id_person")
*/
private $person;
}
By trying to attach the design to the real world you are over complicating the implementation which will make your application hard to mantain.
I did not try the code (probably needs adjustement).

Abstract Doctrine association mapping without discriminator (legacy DB)

I'm working with a legacy database (that means no schema changes!) and I need to create a associations between the the Doctrine entities involved. I'll describe the data structure first and then explain what I've tried.
The database has a user table with various other tables also storing user related info. Eg:
siteUser has:
contentId (PK)
firstName
lastName
username
password
...
and siteUser entities have metadata in this system which is along the lines of:
metadataId (PK)
title
description
keywords
createDate
publishDate
contentId
contentTable (discriminator)
...
Almost everything in the database can have Metadata by storing it's PK in the metadata.contentId field and the table name in the metadata.contentTable field. Note that metadata.contentId is not a foreign key, these must have been alien to the DBA as I'm yet to see a single one.
Users on the system can save information they find relevant to them so that they can come back to the system later and don't have to go hunting for the same information again.
This is done with content types called conLink, conVideo, conLeaflet stored as database entities (which have metadata).
For example a conVideo looks like this:
contentId (PK)
embedCode
The way users can store mark this information as being relevant to them is by the system storing it in a link table called userSavedContent:
userSavedContentId (PK)
userId
metadataId
Note that userSavedContent.userId and userSavedContent.metadataId are also not foreign key constraints.
THE APPROACH!
I need to get user's saved content. In SQL this is no problem!
SELECT
metadata.title,
conVideo.embedCode
FROM
userSavedContent
INNER JOIN
metadata ON userSavedContent.metadataId = metadata.metadataId
INNER JOIN
conVideo ON conVideo.contentId = metadata.contentId
WHERE userSavedContent.userId = 193745
AND metadata.contentTable = 'conVideo'
However doing this in Doctrine is more complicated because the value of metadata.contentTable could potentially be any of the conLink, conVideo, conLeaflet entities.
So my application is built using Symfony2 (and Doctrine) and I have models defined for all of the above entities.
In this Metadata is an abstract class with a discriminator on metadata.contentTable:
/**
*
* #ORM\Table(name="metadata")
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="contentTable", type="string")
* #ORM\DiscriminatorMap(
* {
* "conLink" = "MyApp\Bundle\DataApiBundle\Entity\Metadata\ConLinkMetadata",
* "conVideo" = "MyApp\Bundle\DataApiBundle\Entity\Metadata\ConVideoMetadata",
* "siteUser" = "MyApp\Bundle\DataApiBundle\Entity\Metadata\SiteUserMetadata"
* }
* )
*/
abstract class Metadata
The ConVideoMetadata class extends Metadata and adds a content property that associates the ConVideo entity to it:
/**
* #var ContentType $content
*
* #ORM\OneToOne(
* targetEntity="MyApp\Bundle\DataApiBundle\Entity\ContentType\ConVideo",
* inversedBy="metadata",
* cascade={"persist", "remove"}
* )
* #ORM\JoinColumn(name="contentId", referencedColumnName="contentId")
*/
protected $content;
Now the userSavedContent entity has metadata property to associated it to an item of metadata.
/**
* #var Metadata $metadata
*
* #ORM\ManyToOne(
* targetEntity="MyApp\Bundle\DataApiBundle\Entity\Metadata",
* inversedBy="userSavedContent"
* )
* #ORM\JoinColumn(name="id", referencedColumnName="metadataId")
*/
protected $metadata;
And finally the siteUser is related to userSavedContent by the following property on it's entity:
/**
* #ORM\OneToMany(
* targetEntity="MyApp\Bundle\DataApiBundle\Entity\UserSavedContent",
* mappedBy="siteUser",
* cascade={"persist", "remove"},
* orphanRemoval=true
* )
* #ORM\JoinColumn(name="contentId", referencedColumnName="userId")
*/
private $userSavedContentItems;
THE PROBLEM!
In my siteUserRepository class I now need to query for a siteUser and all it's saved content items:
$builder = $this->createQueryBuilder('s')
->select('s', 'm', 'usc', 'uscm', 'uscc')
->innerJoin('s.metadata', 'm')
->leftJoin('s.userSavedContentItems', 'usc')
->leftJoin('usc.metadata', 'uscm')
->leftJoin('uscm.content', 'uscc');
return $builder;
This doesn't work!
"[Semantical Error] Error: Class MyApp\Bundle\DataApiBundle\Entity\Metadata has no association named content"
This makes sense of course since MyApp\Bundle\DataApiBundle\Entity\Metadata doesn't have the content property, it's child MyApp\Bundle\DataApiBundle\Entity\Metadata\ConVideoMetadata is the one with that association. I thought Doctrine would have been able to work this out but apparently not.
So my question is:
Is this approach very wrong? And if not what can I do to make that association/query work?
The fix for this issue was to get Doctrine to eagerly fetch the concrete metadata->content entities. I could declare these explicitly but used Doctrine's MetadataFactory to get the Metadata entity's discriminator for the list of all possible content types.
$metadataFactory = $this->getEntityManager()->getMetadataFactory();
$metadataMetadata = $metadataFactory->getMetadataFor('MyApp\Bundle\DataApiBundle\Entity\Metadata');
foreach ($metadataMetadata->discriminatorMap as $contentEntity) {
$builder->getQuery()
->setFetchMode(
$contentEntity,
'content',
ClassMetadata::FETCH_EAGER
);
}

Need help understanding Doctrine many to many self referencing code

i am having trouble deciphering this block of code from doctrine documentation
/** #Entity */
class User
{
// ...
/**
* #ManyToMany(targetEntity="User", mappedBy="myFriends")
*/
private $friendsWithMe;
/**
* #ManyToMany(targetEntity="User", inversedBy="friendsWithMe")
* #JoinTable(name="friends",
* joinColumns={#JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="friend_user_id", referencedColumnName="id")}
* )
*/
private $myFriends;
// ...
}
below is how i decipher a one to many bidirectional relationship
(source: tumblr.com)
but if i use the same method, ... below is what i get
alt text http://img514.imageshack.us/img514/2918/snagprogram0000.png
UPDATE
i shld clarify my question. basically, i dont understand how is the opposite of myFriends, friendsWithMe. how i shld make sense of this code and more importantly know how to code such relationships myself.
i give a try at answering my question, i am still quite blur with this, hope someone can really give a better answer,
so 1st to answer the question abt how do i derive with $friendsWithMe
basically, i started off with "decoding" a simpler, more common, many to many bidirectional relationship.
1 user can be in many groups
$user->groups
1 group can have many users
$group->users
very straight forward. but how does this make sense in SQL?
code to implement
# select groups user is in
select group_id from users_groups
where user_id = 1
#select users of group
select user_id from users_groups
where group_id = 1
now to the actual model ... in SQL
in code
# select friends of given user
# $user->myFriends
select friend_id from friends
where user_id = 1;
# select users that are friends of given user
# $user->friendsWithMe
select user_id from friends
where friend_id = 1;
ah ha! select users that are friends of given user. so this is how i get $friendsWithMe. then to fill up the inversedBy & mappedBy & the rest of the class?
1st look at the bottom note.
not clear without so much and deep thinking, abt 2 days. i guess
then as practice how do i create a many to many self referencing relationship from scratch?
the example i am going to work on is... hmm, quite crappy i think but, i'll try :) ... 1 user/student can have many teachers. 1 teacher can have many users/students. 1 user can be a teacher and student here. u know like in forums such as these, when u answer someones questions, you are a teacher. when u ask, u are a student
the ERD will look like
some code to select, students of teachers, teachers of students
# select students of teacher
# $teacher->students
select student from teacher_student
where teacher = 1;
# select teachers of student
# $student->teachers
select teacher from teacher_student
where student = 2;
ok, the doctrine part?
/** #Entity #Table(name="users")) */
class User {
/**
* #Id #Column(type="integer")
* #GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #Column(type="string", length="30")
*/
private $name;
/**
* #ManyToMany(targetEntity="User", inversedBy="teachers")
* #JoinTable(name="Teachers_Students",
* joinColumns={#JoinColumn(name="teacher", referencedColumnName="id")},
* inverseJoinColumns={#JoinColumn(name="student", referencedColumnName="id")}
* )
*/
private $students;
/**
* #ManyToMany(targetEntity="User", mappedBy="students")
*/
private $teachers;
}
which generated this tables for me
# users
CREATE TABLE `users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(30) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
#teachers_students
CREATE TABLE `teachers_students` (
`teacher` int(11) NOT NULL,
`student` int(11) NOT NULL,
PRIMARY KEY (`teacher`,`student`),
KEY `student` (`student`),
CONSTRAINT `teachers_students_ibfk_2` FOREIGN KEY (`student`) REFERENCES `users` (`id`),
CONSTRAINT `teachers_students_ibfk_1` FOREIGN KEY (`teacher`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
at last i done it! lets test it ... erm i am getting
Fatal error: Class 'Entities\User' not
found in
D:\ResourceLibrary\Frameworks\Doctrine\tools\sandbox\index.php
on line 61
when i try to do a
$user = new User;
zzz ...
i have also blogged abt this question and my explaination on my tumblr
The question is, having the M:N table:
friend_user_id
user_id
with two users id 1 and 2. Do you have only:
friend_user_id = 1 and user_id = 2
or both
friend_user_id = 1 and user_id = 2
user_id = 2 and friend_user_id = 1
You can implement both ways, depending on how you code the management of the collection of the owning side.
Case A:
public function addFriend(User $friend)
{
$this->myFriends[] = $friend;
}
Case B:
public function addFriend(User $friend)
{
$this->myFriends[] = $friend;
$friend->myFriends[] = $this; // php allows to access private members of objects of the same type
}

Categories