Abstract Doctrine association mapping without discriminator (legacy DB) - php

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
);
}

Related

How to complete an entity with relations which cannot be mapped by doctrine

I have an entity person, which is linked to an person_groups table.
As this link is not done through person.id primary key, but through an index person.matricule, I cannot map this attribute with a many to one relation
However, I'd really like to have a groups property in my entity. How can this be achieved ?
I was thinking of using the doctrine event lifecycle, to dynamically update my entity at load time. Is it a good idea ? Are there other solutions ?
What is the reason you can't map it by doctrine?
/**
* #ORM\OneToMany(
* targetEntity="YourNamespace\YourEntityGroups",
* mappedBy="person"
* )
*/
protected $groups;
and
/**
* #ORM\ManyToOne(
* targetEntity="YourNamespace\YourEntityPerson",
* inversedBy="groups"
* )
* #ORM\JoinColumn(name="person_matricule", referencedColumnName="matricule")
*/
protected $person;

Class Project\MyBundle\PhpbbTopics has no association named forumId

I'm trying to join the Entities PhpbbTopics and PhpbbForums on the field forumId. These are PhpBB tables and therefore do not have a foreign key on a database level. However, when joining the two Entities using DQL I get the following exception:
[Semantical Error] line 0, col 78 near 'f': Error: Class
Project\MyBundle\Entity\PhpbbTopics has no association named forumId
The code that is being executed in my Controller is:
$em = $this->getDoctrine()->getManager();
$query = $em->createQuery(
'SELECT f.forumName
FROM ProjectMyBundle:PhpbbTopics t JOIN t.forumId f'
);
The two entites are:
**
* PhpbbTopics
*
* #ORM\Table(name="phpbb_topics", indexes={#ORM\Index(name="forum_id", columns={"forum_id"}), #ORM\Index(name="forum_id_type", columns={"forum_id", "topic_type"}), #ORM\Index(name="last_post_time", columns={"topic_last_post_time"}), #ORM\Index(name="topic_approved", columns={"topic_approved"}), #ORM\Index(name="forum_appr_last", columns={"forum_id", "topic_approved", "topic_last_post_id"}), #ORM\Index(name="fid_time_moved", columns={"forum_id", "topic_last_post_time", "topic_moved_id"})})
* #ORM\Entity
*/
class PhpbbTopics
{
/**
* #var \Project\MyBundle\Entity\PhpbbForums
*
* #ORM\ManyToOne(targetEntity="Project\MyBundle\Entity\PhpbbForums")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="forum_id", referencedColumnName="forum_id")
* })
*/
private $forumId;
}
And:
/**
* PhpbbForums
*
* #ORM\Table(name="phpbb_forums", indexes={#ORM\Index(name="left_right_id", columns={"left_id", "right_id"}), #ORM\Index(name="forum_lastpost_id", columns={"forum_last_post_id"})})
* #ORM\Entity
*/
class PhpbbForums
{
/**
* #var integer
*
* #ORM\Column(name="forum_id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $forumId;
/**
* #var string
*
* #ORM\Column(name="forum_name", type="string", length=255, nullable=false)
*/
private $forumName;
}
Both of these entities have more fields than I have shown. These fields are not used in my query. Every field also has a getter and setter, which are not shown. If you feel the need to see more of the entities, please leave a comment.
I have tried the following solutions but they did not solve my issue:
Doctrine Class “....” has no association named “…”
Error: Class …\Entity.. has no association named
Error: Class … has no field or association named
EDIT:
I found that the file PhpbbTopics.om.xml at src\Project\MyBundle\Resources\config\doctrine did not contain the relationship to PhpbbForums. I have replaced the line:
<field name="forumId" type="integer" column="forum_id" nullable="false"/>
With:
<many-to-one field="forumId" target-entity="PhpbbForums">
<join-columns>
<join-column name="forum_id" referenced-column-name="forum_id"/>
</join-columns>
</many-to-one>
This did not solve or change the issue.
I have solved my issue by changing the syntax of the join. The syntax I am using right now explicitly states which fields of which entities should be joined together. The new query is:
$query = $em->createQuery(
'SELECT f.forumName
FROM ProjectMyBundle:PhpbbTopics t JOIN ProjectMyBundle:PhpbbForums f WITH f.forumId = t.forumId'
);
By using this query, I am able to remove the ManyToOne relationship that I have defined in PhpbbTopics.php and PhpbbTopics.om.xml. Without the declared relationship, my entity matches my database table closer as the table phpbb_topics does not have a foreign key to phpbb_forums.

Is it possible to reference a column other than 'id' for a JoinColumn?

I have an Item entity that has a ManyToOne relationship to a Category entity. I want them to be joined by a field other than Category's id (in this case, a field called id2). My schema is listed below.
class Item {
/**
* #ORM\Id
* #ORM\Column(name = "id", type = "integer")
* #ORM\GeneratedValue(strategy = "AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity = "Category")
* #ORM\JoinColumn(name = "category_id", referencedColumnName = "id2")
*/
protected $category;
}
class Category {
/**
* #ORM\Id
* #ORM\Column(name = "id", type = "integer")
* #ORM\GeneratedValue(strategy = "AUTO")
*/
protected $id;
/**
* #ORM\Column(name = "id2", type = "string", length = "255", unique = "true")
*/
protected $id2;
When I try saving an Item I get this error:
Notice: Undefined index: id2 in vendor/doctrine/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php line 511
Sure enough, if I change id2 to id in the JoinColumn annotation, everything works fine, but I need the entities to be connected through id2. Is this possible?
Edit
What I want to achieve is impossible according to the official Doctrine 2 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.
source: https://www.doctrine-project.org/projects/doctrine-orm/en/2.6/reference/limitations-and-known-issues.html#join-columns-with-non-primary-keys
I think Doctrine wants these to be primary keys, from the docs:
name: Column name that holds the foreign key identifier for this relation.
Another thing that jumps out at me from your code sample is category.id2 being type string, I would at least expect it to be an integer, but it may also need to be for #JoinColumn to work properly.
You may be able to get away with just #Index on category.id2 and leave it as a string though; worth a shot anyway.
Just to report. I was able to join non-PKs in Many2One (undirectional) relation, BUT my object can't be loaded the normal way. It must be loaded with DQL like:
SELECT d,u FROM DEntity d
JOIN d.userAccount u
this way I stopped getting error: Missing value for primary key id on ....

NULL inserted while Persisting new Entity, Mapping table column to 2 tables. doctrine 2

I need to map the same column to 2 differences tables (lets say normal and extended).
/**
* #var ItemValue
*
* #OneToOne(targetEntity="ItemValue")
* #JoinColumn(name="id_value", referencedColumnName="id_value")
*/
private $value;
/**
* #var ItemValueExtended
*
* #OneToOne(targetEntity="ItemValueExtended")
* #JoinColumn(name="id_value", referencedColumnName="id_value")
*/
private $valueExtended;
/**
* #var string $isExtended
*
* #Column(name="is_extended", type="string", nullable=false)
*/
private $isExtended = 'YES';
I have no problem with joining data based on the isExtended attribute using DQL:
"SELECT id,idv FROM ItemData id
JOIN id.value idv WHERE id.isExtended='NO'";
and
"SELECT id,idv FROM ItemData id
JOIN id.valueExtended idv WHERE id.isExtended='YES'";
but when ever I want to persist a new object, NULL is inserted in id_value column ?!!
$oValue = ItemValue();
.
.
$oData = new ItemData();
$oData->setValue($oValue);
.
.
.
$em->persist($oData);
$em->flush();
Any Idea ?
From Doctrine2 documentation:
In the case of bi-directional associations you have to update the
fields on both sides.
One possible solution would be:
$oData = new ItemData();
$oData->setValue($oValue);
$oValue->setData($oData);
but it's tedious. Another better one is set the cascade option on both sides of the one-to-one association:
#OneToOne(targetEntity="ItemValue"), cascade={"persist", "remove"})
This way your code will work. You can choose the appropriate cascade options looking at here.
In the case where both the parent and child entity are new entities (neither have been persisted) a PrePersist lifecycle event on the parent can help:
/**
* ....
*
* #ORM\HasLifecycleCallbacks
*/
class ParentEntity {...
/**
* #ORM\PrePersist()
*/
public function prePersist() {
foreach($this->getChildEntities() as $childEntity) {
$childEntity->setParent($this);
}
}
-
class ChildEntity {
....
This will automatically create the child -> parent relationship upon saving the parent.
In many instances Doctrine will then be able to work out the rest at the SQL level.

Doctrine 2 Inheritance Mapping Strategy Advice needed

I'll try to explain my problem. Shortly, I have address table, rooms table, buildings table and users tables. Address table should be able to keep address for rooms, buildings and users by their ID's of course. Since I can't use direct Association Mapping I need an inheritance mapping. Let me make this clear that I'm not good at inheritance mapping issue. My solution is to create an Alias table which keeps the ID of the rooms, buildings and users (also their class names) and address table can interact directly with this Alias table. So in SQL style this would be join Alias and users then join the address table find the user's address. However, I couldn't decide which mapping style would fit. Can you give me some advice about it and give me a short explanation why it is like that?
Thanks.
I suggest you to use relations like
I suppose this structure (user can own address, building have address, room have building)
users
address
buildings
rooms
And code for it
/**
* #orm:Table(name="rooms")
* #orm:Entity
*/
class Room {
....
/**
* #orm:InheritanceType("JOINED")
* #orm:OneToOne(targetEntity="Building")
* #orm:JoinColumn(name="bid", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $building;
....
}
/**
* #orm:Table(name="Buildings")
* #orm:Entity
*/
class Building {
....
/**
* #orm:InheritanceType("JOINED")
* #orm:OneToOne(targetEntity="Address")
* #orm:JoinColumn(name="aid", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $address;
....
}
/**
* #orm:Table(name="rooms")
* #orm:Entity
*/
class Address {
....
/**
* #orm:InheritanceType("JOINED")
* #orm:OneToOne(targetEntity="User")
* #orm:JoinColumn(name="uid", referencedColumnName="id", onDelete="CASCADE", nullable=false)
*/
private $user;
....
}
/**
* #orm:Table(name="users")
* #orm:Entity
*/
class User {
....
}
#orm: needs when using doctrine with symfony

Categories