Nested composite foreign keys as ID in Doctrine - php

We are developing an online store in Symfony 5 and Doctrine 2 where multiple customers (called participants in this case) can participate in the same order item and share the cost. The following simplified class diagram demonstrates the domain model:
The pure object model works fine in unit tests, but you obviously need to persist the data to a database, which is why we need to introduce IDs.
Order, Product and Participant are entities with their own ID. In an ideal world, OrderItem and OrderItemParticipation would not need their own ID but be identified by the related entities they belong to, meaning their ID would be a composite foreign key.
So, an OrderItem would by identified by the composite key of Order.id and Product.id, which is pretty much exactly the same as given in this example from the Doctrine 2 documentation: https://www.doctrine-project.org/projects/doctrine-orm/en/2.10/tutorials/composite-primary-keys.html#use-case-3-join-table-with-metadata.
Since OrderItemParticipation relates to OrderItem, which uses a composite key itself, it would need to use a nested composite key consisting of Order.id, Product.id and Participant.id.
Unfortunately, Doctrine 2 doesn't seem to be able to work with nested composite keys as ID. I get this error
Column name id referenced for relation from
App\Entity\OrderItemParticipation towards App\Entity\OrderItem does
not exist.
when I try to generate a migration with the following mapping:
/** #ORM\Entity */
class OrderItem {
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=Order::class, inversedBy="items")
*/
private Order $order;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=Product::class)
*/
private Product $product;
// ...
}
/** #ORM\Entity */
class OrderItemParticipation {
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=OrderItem::class, inversedBy="participations")
*/
private OrderItem $orderItem;
/**
* #ORM\Id
* #ORM\ManyToOne(targetEntity=Participant::class)
*/
private Participant $participant;
// ...
}
So it seems that Doctrine is fine with my ID mapping in OrderItem, but it struggles when it gets to OrderItemParticipation. Is there a way to make Doctrine work with the given domain model? Is it maybe just an issue with the auto-generation of the migration, so if I had already manually set up the database, Doctrine might work with the given mapping? Or is the nested composite key ID approach too complicated for Doctrine?

Related

How do I model my ER diagram when Doctrine won't let me associate composite primary key with another entity's foreign key?

I am trying to model the ManyToOne relations between
product_pricename and category_pricename
price_historyand product_pricename, as in the diagram below:
How?
Where I'm at
I note that this ER Diagram contains a composite foreign key,
I am talking about table product_pricename with key of (category_pricename_category_id, category_pricename_pricename_id)
which is part of another entity's primary key
I am talking about category_pricename with PRIMARY KEY of (category_id, pricename_id)
Which is totally okay by MySQL. But when I try to model this in Doctrine I get an error:
[Doctrine\ORM\Mapping\MappingException]
It is not possible to map entity 'CategoryPricename' with a composite
primary key as part of the primary key of another entity
'ProductPricename#categoryPricename'.
I so far have not visualized a proper way to do this in Doctrine.
I'm thinking I could introduce a surrogate PRIMARY KEY to entities product_pricename and category_pricename and re-model my tables. But would that be a way to do it?
Relevant Code
class CategoryPricename
{
/**
* #Id #ManyToOne(targetEntity="Category")
* #JoinColumn(name="category_id", referencedColumnName="id", nullable=false)
*
* #var Category
*/
private $category;
/**
* #Id #ManyToOne(targetEntity="Pricename")
* #JoinColumn(name="pricename_id", referencedColumnName="id", nullable=false)
*
* #var Pricename
*/
private $pricename;
}
class ProductPricename
{
/**
* #Id #ManyToOne(targetEntity="Product")
* #JoinColumn(name="product_id", referencedColumnName="id", nullable=false)
*
* #var Product
*/
private $product;
/**
* Note: I shortened the column names for my code
* by removing "category_pricename_" prefix for brevity
* as reflected in JoinColumn's name attributes
*
* #Id
* #ManyToOne(targetEntity="CategoryPricename")
* #JoinColumns({
* #JoinColumn(name="category_id", referencedColumnName="category_id", nullable=false),
* #JoinColumn(name="pricename_id", referencedColumnName="pricename_id", nullable=false)
* })
*
* #var CategoryPricename
*/
private $categoryPricename;
}
Natural vs Surrogate keys
It seems like with Doctrine I have to use surrogate keys in my tables to make my design work with Doctrine's limitations.
OR
I could go with mysqli and use natural keys (as in some tables of the ER Diagram now)
Part of me goes... which route do I go, surrogate (Doctrine) or natural (mysqli)
Modeling of more complex relations in Doctrine requires:
Model all relations as an entity. Have each such relation entity have a surrogate id primary key.
Instead of ManyToMany relation use ManyToOne for each side.
For the above to work, you may need change the structure of your tables to where you have no composite primary keys. That is due to Doctrine does allowing composite primary keys to be associated with foreign ids. It is a limitation. To work with this limitation means having more surrogate keys and less natural keys, which means having more table joins for i.e. querying price_history.
Alternative way is to use mysqli or PDO.
Alternative way is to not use Doctrine. Set up your tables however you need, and use mysqli or PDO. I chose to go this route because for my design I wanted to use full power of ER Design and DBMS and not have to deal with a lot of table joins.

How can I create a relation between entities twice

I am trying to create some sort of Inventory:
(The following Code is obviously not complete)
class User
{
/**
* #ORM\ManyToMany(targetEntity="Item", fetch="EAGER")
* #ORM\JoinTable(name="character_inventory_mm",
* joinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="item_id", referencedColumnName="id")}
* )
*/
private $inventory;
//etc
}
My Problem is that a User can have the same Item twice or even more often.
Is there a way to tell Doctrine not to create unique keys on those Relations or do I have to create some sort of mapping entity?
Something like:
* #ORM\ManyToMany(targetEntity="Item", fetch="EAGER", indexBy="NULL")
I have looked up doctrines joinTable and joinColumns and ManytoMany Documentation but I did not find a way to "fix" my Issue.
Thanks
That would need an unique key to differentiate multiple items connected to the same user.
Just create UserItems entity with a primary key and correspoding OneToMany and ManyToOne relations.

Doctrine - Composite IDs

I am using symfony2 with the Doctrine entities and I have a problem with the next:
I know I could solve the problem putting an ID to the "club_has_torneo" and turning it into an entity, but to me creating an entity for that looks like something that should not be done. So I wanted to know if there is a way to solve this issue or if I have to do what I think I have to.
Thank you in advance.
I guess I'll submit my own two cents worth.
ORM stands for Object Relational Mapper. The basic idea is to figure out how to map your objects without worrying too much about the database schema. So you have three domain model entities: torneo, club and nadador. Great. Figure out how your application will use these entities. Don't worry about how the relations will end up being stored.
Once you have a working domain model then worry about persistence. The three domain entities clearly map to three doctrine entities. As far as the relations go, I personally am not a big fan of composite primary keys. I think they just complicate things while adding little value. So I would make Doctrine entities for your two has tables and just given them their own primary database id.
Note that these are Doctrine entities not domain entities. Your application code should never need to deal with these relational doctrine entities at all. So in my opinion
creating an entity for that looks like something that should not be
done
does not apply here. It is just a persistence detail.
I think the best solution is indeed to make a entity for your club_has_torneo table. This ClubHasTorneo entity has club_id and torneo_id as composite keys and holds the owning side of a many-to-many relationship between your ClubHasTorneo entity and Nadador entity. This relationship can be done with a join table using the 3 keys. Check the code below on how to do that.
Your database scheme will look exactly like you drew it.
Your ClubHasTorneo entity would look something like this:
<?php
namespace Application\Entity;
use Application\Entity\Club;
use Application\Entity\Torneo;
use Application\Entity\Nadador;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\ArrayCollection;
/**
* #ORM\Entity
* #ORM\Table(name="club_has_torneo")
*/
class ClubHasTorneo
{
/** MANY-TO-ONE BIDIRECTIONAL, OWNING SIDE
* #var Club
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Application\Entity\Club", inversedBy="clubHasTorneos", cascade={"persist"})
* #ORM\JoinColumn(name="club_id", referencedColumnName="id")
*/
protected $club;
/** MANY-TO-ONE BIDIRECTIONAL, OWNING SIDE
* #var Torneo
* #ORM\Id
* #ORM\ManyToOne(targetEntity="Application\Entity\Torneo", inversedBy="clubHasTorneos")
* #ORM\JoinColumn(name="torneo_id", referencedColumnName="id")
*/
protected $torneo;
/** MANY-TO-MANY BIDIRECTIONAL, OWNING SIDE
* #var Collection
* #ORM\ManyToMany(targetEntity="Application\Entity\Nadador", inversedBy="clubHasTorneos")
* #ORM\JoinTable(name="club_has_torneo_has_nadador",
* joinColumns={
* #ORM\JoinColumn(name="club_id", referencedColumnName="club_id"),
* #ORM\JoinColumn(name="torneo_id", referencedColumnName="torneo_id")
* },
* inverseJoinColumns={
* #ORM\JoinColumn(name="nadador_id", referencedColumnName="id")
* }
* )
*/
protected $natadors;
public function __construct()
{
$this->natadors = new ArrayCollection();
}
// setters and getters
}
my 5 cents
If you want your implementation to match the drawn table structure, then (in my opinion) you need create an entity out of the 'club_has_torneo' table (for 'club_has_torneo_has_matador' you don't need to).
The rationale being that if you try to achieve this without creating the entity, you would need to create the entity associations so, that the 'natador' table references the 'club' and 'torneo' directly - in which case the actual database relations wouldn't match with your drawn table relationship anymore (i.e. the natador wouldn't have relationship to the 'club_has_torneo' table).

Many To One relationship in doctrine2

I'm having trouble understanding doctrine and trying to make an entity. Would the following sql statement correspond with the following doctrine entity
create table comments (
id SERIAL PRIMARY KEY,
message VARCHAR,
parent_id INTEGER REFERENCES comments(id)
);
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
*
* #ORM\Column(name="parent_id", type="integer")
* #ORM\OneToMany(targetEntity="Comment", mappedBy="id")
*/
private $parent_id;
You are mixing up the id's and the entity definitions a bit: with your example the parent_id variable would contain the parent comment entity instead of the id (that the variable name implies). Also your association is the wrong way around - it should be ManyToOne when defining the parent entity (many child comments for one parent comment).
Basically you can follow the example case in doctrine association mappins (section One-To-Many, Self-referencing) - there is also the entity relationship defined from parent comment to the children as an arrayCollection. In case you don't need the children connection you can just remove the related variable & inversedBy definition from the parent variable annotation.
If the referenced example is not clear enough I can post full code as well (as I happen to have precisely the same already done for comment entity)
The parent (which is another object than the initialized child) can have multiple 'childs', so the relationship should be a ManyToOne relationship.
Also take a look at the solution here: https://stackoverflow.com/a/24002956/1794894

Doctrine - OneToOne Unidirectional vs OneToOne Bidirectional

I joust started playing around with Doctrine ORM library, and Im learning about all associations between tables.
So Im stuck with differences in Unidirectional and Bidirectional relation.
As I get it, Unidirectional relation has primary key only on one side, and that side is owning side right?
And Bidirectional relation have primary key in both tables and therefore you can have relation from both sides, and set constrains on both sides.
Now, Im reading through Doctrine documentation about relations and there you have:
Unidirectional and Bidirectional associations.
But they produce the same SQL, and the same tables with the same primary key-s and constrains. So I dont really see any difference in those two. And both examples have primary key on one side.
As I get it the true Bidirectional relation should have primary keys in both tables pointing to the other table right? And with given example on Doctrine documentation that is not the case. Both examples give the same result and are the same.
So what I did, is this, lets say I have User and Card Entity, and want relation to be OneToOne Bidirectional.
/**
* #Entity
* #Table(name="users")
*/
class User
{
/**
* #Id
* #GeneratedValue
* #Column(type="bigint")
*/
protected $id;
/**
* #OneToOne(targetEntity="Card", mappedBy="User")
* #JoinColumn(name="card_id", referencedColumnName="id")
*/
protected $card;
/**
* #Column(name="user_name", type="string")
*/
protected $userName;
/**
* #Column(name="user_pass", type="string")
*/
protected $userPass;
}
/**
* #Entity
* #Table(name="cards")
*/
class Card
{
/**
* #Id
* #GeneratedValue
* #Column(type="bigint")
*/
protected $id;
/**
* #OneToOne(targetEntity="User", inversedBy="Card")
* #JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
/**
* #Column(name="post_title", type="string")
*/
protected $cardType;
}
The difference here is I wrote #JoinColumn in both objects/entities. And in Doctrine example there is only one.
Now I would get what I think is Bidirectional relation. If i look at EER diagram, I can see one line pointing from user to card, and the other from card to user.
So basicly did I get this right?
Is the Doctrine documentation wrong? :D
How would Bidirectional OneToOne relation look in EER diagram?
Thanks!
The only difference is in the PHP class interface, i.e. in the presence or absence of the property that points back to the owner (e.g. the $customer property in the mentioned Doctrine example). In other words Doctrine just needs to know whether it should take care about a single property ($shipping) or two properties ($cart and $customer). There is no other difference. Therefore, the SQL code is the same (because one foreign key is sufficient for representing any 1:N relationship) and there would no difference in EER diagram neither (because in EER you typically do not solve such PHP-related implementation details).
Unidirectional and bidirectional have nothing to do with the background algorithm how to create these connections in the database layer.
All they talk about is how the connections can be used. In an unidirectional relationship you can access the target only from one site. An bidirectional relationship allows the connection to be called from two (both) sides.
So in an unidir. rel. model_a can get to model_b, but model_b cant get to model_a (without extra work).
If you now use a bidir. rel both models can access each other without problems
In doctrine terms, a unidirectional relationship defines a
$modelA->getModelB() method, but not a $modelB->getModelA() method, whereas a bidirectional relationship defines both methods (or accessors, however you want to call them)
in an uml diagram it would look like this:
unidirectional
modelA --X------> modelB
bidirectional
modelA <--------> modelB

Categories