Doctrine ManyToMany Unidirectional with shared attribute. Symfony 3.4 - php

I've 2 entities: Objetct and Product with a extra ID in each table: officeId
This id is in every table (I cant modify the database )
And 3 tables:
Object
Product
ObjectProduct
I want a manyTomany unidirectional relation.
Entity Object:
class Object
{
/**
* #var Products[]|ArrayCollection
*
* #ORM\ManyToMany(targetEntity="AppBundle\Entity\Products")
* #ORM\JoinTable(name="ObjectProduct",
* joinColumns={#ORM\JoinColumn(name="objectId", referencedColumnName="id"),
* #ORM\JoinColumn(name="officeId", referencedColumnName="officeId")},
* inverseJoinColumns={#ORM\JoinColumn(name="productId", referencedColumnName="id"),
* #ORM\JoinColumn(name="officeId", referencedColumnName="officeId")}
* )
*
*/
private $products;
}
My problem is when try to insert, insert the officeId attribute twice:
like:
INSERT INTO ObjectProduct (objectId, officeId, productId, officeId) VALUES (?, ?, ?, ?)

When you use a many to many relation ship (either uni or bi directionnal) it creates a join table. You seems to be aware of this part.
This join table contains tow foreign key that reprensent a composite primary key (to be sure of the unicity). This is automatique
If you wish to change anything (ad an extra ID , extra columns...) then you will need to map it as an entity inside you project and define one to many relation ship instead of many to many

Related

Doctrine: Check if relation exists for a Discriminator entity

I'm trying to check if a relationship exists or not, in other words, is the column NULL or filled out.
Here I have my QueryBuilder:
$qb->select('v')
->from('Entities\Voucher', 'v')
->leftJoin('Entities\VoucherCopy', 'vc', \Doctrine\ORM\Query\Expr\Join::WITH, 'v.voucherCopy = vc.id')
->where("v.code LIKE :voucherCode")
->andWhere('vc.id IS NULL');
This would be ok if VoucherCopy wasn't Extending "Entities\Codes" - a Single Table Inheritance
VoucherCopy:
class VoucherCopy extends Codes
Codes:
/**
* Entities\DiscountCode
*
* #Table(name="discount_code")
* #Entity()
*
* #InheritanceType("SINGLE_TABLE")
* #DiscriminatorColumn(name="type", type="string")
* #DiscriminatorMap({"D" = "DiscountCode", "VT" = "PortalVoucher", "V" = "VoucherCopy"})
*/
class Codes
The problem is that the executed SQL from this query builder is:
SELECT v0_.*
FROM voucher v0_
LEFT JOIN discount_code d1_ ON (
v0_.discount_code_id = d1_.id
) WHERE (v0_.code LIKE '%DA70511911%' AND d1_.id IS NULL) AND d1_.type IN ('V')
;
Note the AND d1_.type IN ('V'). This is because DQL is automatically adding this because it knows that's the discriminator column for this Entity, but in doing so AND d1_.id IS NULL becomes redundant as I can't check if the relationship fails with the AND d1_.type IN ('V') check.
Is checking if a relationship exists whilst using single table inheritance possible?
I've tried leftJoin to Entities\Code (the parent) directly, but I can't add the discriminator column (type) as a property because you get error: Duplicate definition of column 'type' on entity 'Entities\Codes' in a field or discriminator column mapping.

How to drop row from many to many relationship table in doctrine

Hi I have M:M relationship between two tables, Contacts and Tags and their M:M table is called Contacts_Tags:
Contacts
------------
ID
Name
Tags
-----------
ID
Name
Contacts_Tags
--------------
Contact_ID
Tag_ID
I have entities for Contacts called Contact and for Tags called Tag but not for Contacts_Tags table.
I want to drop row from Contacts_Tags only. This is my relationship in Contact Entity.
/**
* Many Contacts have Many Tags.
* #ORM\ManyToMany(targetEntity="Tag", inversedBy="contacts", cascade={"remove"})
* #ORM\JoinTable(name="Contacts_Tags",
* joinColumns={#ORM\JoinColumn(name="Tag_ID", referencedColumnName="ID")},
* inverseJoinColumns={#ORM\JoinColumn(name="Contact_ID", referencedColumnName="ID")}
* )
*/
private $tags;
I tried this query but it didn't help
$queryBuilder = $this->entityManager->getRepository(Contact::class)->createQueryBuilder("o")
->leftJoin("o.tags", "ct")
->leftJoin(Tag::class, "t", "WITH", "t.ID", "ct.Tag_ID")
->where("t.Name = :tagName")
->delete("o.tags", "ct")
->setParameter(":tagName", $diffBeat)
;
You can just remove it from the collection.
you can define a function in your Contact class like this:
function removeTag(Tag $tag)
{
if($this->tags->contains($tag)){
$this->tags->removeElement($tag);
}
}
Afterwards persist your entity

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

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

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 ;

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 ....

Categories