This is regarding a problem with Doctrine when I try to insert a record into a associative entity. Below is a simplified description of the problem.
I have two tables, let's call them One and Two. Table One has a foreign key to table Two, called twoId with a column two_id. Field two_id happens to be part of the primary key.
* #ORM\Id
* #ORM\Column(name="user_id", type="string", length=40)
*/
private $twoId;
/**
* #ManyToOne(targetEntity="[...]", inversedBy="[...]", fetch="EAGER")
* #JoinColumn(name="two_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $two;
I am trying to insert a new record into table A. This works:
$two = [.. read from DB ..];
$one = new One();
$one->setTwo($two);
$one->setTwoId($two->getId());
$em->persist($one);
$em->flush();
I don't like to call both setTwo and setTwoId. Furthermore, I don't like reading the $two record before referencing it.
If I skip setTwoId call, I get the error: Entity of type [..] is missing an assigned ID for field 'twoId'. The identifier generation strategy for this entity requires the ID field to be populated before EntityManager#persist() is called.
If I skip setTwo call, I get the error: Integrity constraint violation: 1048 Column 'two_id' cannot be null
My problems are:
How can I avoid calling both setTwo() and setTwoId()?
What if I want to reference a entity from Two without reading it? Should I use $em->getReference()? (PhpStorm doesn't even recognize it)
In case someone makes the same mistake:
As pointed out by #lordrhodos, declaring the field $twoId was wrong because Doctrine will create it automatically without having a definition.
Definition:
/**
* #ManyToOne(targetEntity="[...]", inversedBy="[...]", fetch="EAGER")
* #JoinColumn(name="two_id", referencedColumnName="id", onDelete="CASCADE")
*/
private $two;
Usage:
$two = [.. read from DB ..];
$one = new One();
$one->setTwo($two);
$em->persist($one);
$em->flush();
Related
I am trying to solve my issue on Doctrine ORM. I have 2 parent entities: CompanyDoctrineEntity and ServiceDoctrineEntity and 1 entity that are associated with these 2 (but the association is not required) OrderLinkRedirectLogDoctrineEntity. The association in OrderLinkRedirectLogDoctrineEntity is defined by:
class OrderLinkRedirectLogDoctrineEntity {
/**
* #Id
* #Column(type="integer")
* #ORM\GeneratedValue()
*
* #var int $id
*/
private $id;
/**
* Many logs have one company. This is the owning side.
*
* #ManyToOne(targetEntity="CompanyDoctrineEntity", cascade="detach")
* #JoinColumn(name="company_id", referencedColumnName="id")
*
* #var CompanyDoctrineEntity $company
*/
private $company;
/**
* Many logs have one service. This is the owning side.
*
* #ManyToOne(targetEntity="ServiceDoctrineEntity", cascade="detach")
* #JoinColumn(name="service_id", referencedColumnName="id")
*
* #var ServiceDoctrineEntity $service
*/
private $service;
}
My expected behaviour is, whenever either CompanyDoctrineEntity or ServiceDoctrineEntity is removed from the database, the association in the OrderLinkRedirectLogDoctrineEntity will be NULLed, which I believe what the cascade="detach" does, but for some reason, it's not working, as I am getting the following errors:
Fatal error: Uncaught PDOException: SQLSTATE[23000]: Integrity constraint violation: 1451 Cannot delete or update a parent row: a foreign key constraint fails (`test_app2`.`logs_order_link_redirects`, CONSTRAINT `FK_6C1CA74CED5CA9E6` FOREIGN KEY (`service_id`) REFERENCES `app_services` (`id`)) in /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php:117
Stack trace:
#0 /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/PDOStatement.php(117): PDOStatement->execute(NULL)
#1 /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php(1054): Doctrine\DBAL\Driver\PDOStatement->execute()
#2 /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Connection.php(656): Doctrine\DBAL\Connection->exe in /Users/arvil/Projects/app2.test/public_html/wp-content/themes/app-theme/vendor/doctrine/dbal/lib/Doctrine/DBAL/Driver/AbstractMySQLDriver.php on line 49
I'm far from an expert on Doctrine, so take this with a grain of salt and test thoroughly.
Your relations are not nullable (defaults to false), which is why your foreign key constraint is complaining: logs_order_link_redirects.service_id (and company_id) isn't allowed to be null. That likely wasn't a problem before because you're not inserting the OrderLinkRedirectLogDoctrineEntity entities without the relationships. If you were to say
$redirectLog = new OrderLinkRedirectLogDoctrineEntity();
$entityManager->persist($redirectLog);
$entityManager->flush();
you'd probably trigger the same error immediately.
Also, I don't believe you want cascade={"detach"} here. Detach would just remove the entity from this entity manager instance (in other words: for the running process), so anything you'd do to the entity after detaching it wouldn't be reflected in the database when $entityManager->flush() is called. On the next request, the entity would be back in the entity manager.
I believe that adding nullable=true to your ManyToOne's JoinColumn annotations, e.g.
#JoinColumn(name="company_id", referencedColumnName="id", nullable=true)
will get you the result you're looking for. You'll need to update your database schema afterwards for changes to be applied to the tables. Also, make sure you don't have (or add) orphanRemoval=true on the inverse side so Doctrine doesn't automatically remove your entities if they lose their parent.
I prefer adding the JoinColumn annotation to relationships as well, even though it's not required if you're fine with Doctrine's default field name choices. Adding nullable=false makes it more explicit that this relationship cannot be null. That's implied if you don't have nullable=true, but when I start looking at relationships and need to know whether they can be null or not, I'm usually confused by something and I don't have mental energy to spare to actively remember the default values for important attributes.
I'm trying to setup an FK between 2 entities in my DB. The parent table has a ManyToOne relationship to a child table. I can not join these tables using a normal parent_id => id FK due to how the child table is populated by external processes (the parent never knows the primary ID of the child).
Doctrine accepts the entities as shown below but Mysql fails when trying to add the FK to the table with the following error.
ALTER TABLE parent_tbl ADD CONSTRAINT FK_1172A832F85E0677 FOREIGN KEY (username) REFERENCES child_tbl (username);
SQLSTATE[23000]: Integrity constraint violation: 1452 Cannot add or update a child row: a foreign key constraint fails (`dmname`.`#sql-f16_d8acf`, CONSTRAINT `FK_1172A832F85E0677` FOREIGN KEY (`username`) REFERENCES `child_tbl` (`username`))
class Parent
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToOne(targetEntity="Child")
* #ORM\JoinColumn(name="username", referencedColumnName="username", nullable=false)
*/
protected $username;
// ...
}
class Child
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*
* #var integer
*/
protected $id;
/**
* #ORM\Column(type="string", unique=true)
*
* #var string
*/
protected $username;
// ....
}
From the Doctrine limitations and known issues :
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.
So, if you run the doctrine:schema:validate command, you should get something like :
[Mapping] FAIL - The entity-class Parent mapping is invalid:
* The referenced column name 'username' has to be a primary key column on the target entity class Child.
I hope you find a workaround to keep your logic intact using a primary key as join column.
I'm trying Doctrine Associations with Symfony2 for the first time and it's giving me headache.
I have an Admin interface that can, among other things, upload images. I want to know which administrator uploaded what image so i've putted a foreign key administrator to my images table. To gather data, a simple JOIN is necessary to collect the data but with Doctrine, I'm stuck, altough it seems simple.
So, I have an Administrator object that reflects the table. In that object, I have this statement...
#ORM\OneToMany(targetEntity="ImageBundleNamespace\ImageEntity", mappedBy="administrator")
It's simple. In my ImageEntity object (that reflects Images table) is a foreigh key column administrator.
In the ImageEntity object, I use this statement...
#ORM\ManyToOne(targetEntity="AdministratorNamespace\Administrator", inversedBy="imageEntity")
#ORM\JoinColumn(name="administrator", referencedColumnName="id")
There is a administrator field in ImageEntity and a imageEntity field in Administrator that the formentioned statements are mapping.
It doesn't work.
I've run the SchemaValidator on the EntityManger and it says the the administrator field on the ImageEntity object is not defined as an association. The second message says that the administrator field does not exist.
If it helps, this is my DQL for all of it...
'SELECT i.id,
i.imeSlike,
i.velicina,
i.ekstenzija,
i.paths,
a.username,
a.ime,
a.prezime FROM ImageBundle:ImageEntity i
JOIN a.administrator a'
Thank you in advance for all the help.
EDIT
I had a mistake in DQL. Corrected it.
EDIT
I forgot to add the source code.
Association part of the Administrator...
**
* #ORM\OneToMany(targetEntity="Icoo\Administracija\GalerijaBundle\Entity\ImageEntity", mappedBy="administrator")
*/
protected $imageEntity;
public function __construct() {
$this->imageEntity = new ArrayCollection();
}
Association part of the ImageEntity
/**
* #ORM\Column(type="smallint")
*
* #ORM\ManyToOne(targetEntity="Icoo\LoginBundle\Entity\Administrator", inversedBy="imageEntity")
* #ORM\JoinColumn(name="administrator", referencedColumnName="id")
*
*/
protected $administrator;
In the administrator class, you have:
/**
* #ORM\Column(type="smallint")
*
* #ORM\ManyToOne(targetEntity="Icoo\LoginBundle\Entity\Administrator", inversedBy="imageEntity")
* #ORM\JoinColumn(name="administrator", referencedColumnName="id")
*
*/
you need to remove the #ORM\Column(type="smallint")
This must be all. Let me know.
You can separate field mapping from association mapping:
/**
* #ORM\Column(name="administrator", type="smallint", nullable=false, options={"unsigned"=true})
*/
protected $administratorId;
/**
* #ORM\ManyToOne(targetEntity="Icoo\LoginBundle\Entity\Administrator", inversedBy="imageEntity")
* #ORM\JoinColumn(name="administrator", referencedColumnName="id")
**/
protected $administrator;
If you go further you can put exception into $administratorId getter/setter to avoid usage of it.
As i have tested, doctrine ignores value into $administratorId property when you persist/flush your entity (For confirmation you can look at prepareUpdateData() in Doctrine\ORM\Persisters\BasicEntityPersister)
EDIT
I think, my previous variant is possible but wrong. Because, doctrine gets field mapping from definitions in referencedColumn, you can add some more definitions using
* #ORM\JoinColumn(name="administrator", referencedColumnName="id", unique, nullable, onDelete, columnDefinition, fieldName)
This means that your image_entity.administrator field in db will be the same as your administrator.id field in db (except of additional definitions)
I have the following associations in my database (simplified version):
This is a Many-To-Many association but with an attribute on the joining table, so I have to use One-To-Many/Many-To-One associations.
I have a form where I can add as many relations as I want to one order item and create it at the same time (mainly inspired by the How to Embed a Collection of Forms tutorial from the documentation.
When I post the form, I get the following error:
Entity of type TEST\MyBundle\Entity\Relation has identity through
a foreign entity TEST\MyBundle\Entity\Order, 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 'TEST\MyBundle\Entity\Relation'. 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 understand this error because Doctrine tries to persist the Relation object(s) related to the order since I have the cascade={"persist"} option on the OneToMany relation. But how can I avoid this behavior?
I have tried to remove cascade={"persist"} and manually persist the entity, but I get the same error (because I need to flush() order to get the ID and when I do so, I have the same error message).
I also tried to detach() the Relation objects before the flush() but with no luck.
This problem seems unique if 1) you are using a join table with composite keys, 2) forms component, and 3) the join table is an entity that is being built by the form component's 'collection' field. I saw a lot of people having problems but not a lot of solutions, so I thought I'd share mine.
I wanted to keep my composite primary key, as I wanted to ensure that only one instance of the two foreign keys would persist in the database. Using
this entity setup as an example
/** #Entity */
class Order
{
/** #OneToMany(targetEntity="OrderItem", mappedBy="order") */
private $items;
public function __construct(Customer $customer)
{
$this->items = new Doctrine\Common\Collections\ArrayCollection();
}
}
/** #Entity */
class Product
{
/** #OneToMany(targetEntity="OrderItem", mappedBy="product") */
private $orders;
.....
public function __construct(Customer $customer)
{
$this->orders = new Doctrine\Common\Collections\ArrayCollection();
}
}
/** #Entity */
class OrderItem
{
/** #Id #ManyToOne(targetEntity="Order") */
private $order;
/** #Id #ManyToOne(targetEntity="Product") */
private $product;
/** #Column(type="integer") */
private $amount = 1;
}
The problem I was facing, if I were building an Order object in a form, that had a collection field of OrderItems, I wouldn't be able to save OrderItem entity without having saved the Order Entity first (as doctrine/SQL needs the order id for the composite key), but the Doctrine EntityManager wasn't allowing me to save the Order object that has OrderItem attributes (because it insists on saving them en mass together). You can't turn off cascade as it will complain that you haven't saved the associated entities first, and you cant save the associated entities before saving Order. What a conundrum. My solution was to remove the associated entities, save Order and then reintroduce the associated entities to the Order object and save it again. So first I created a mass assignment function of the ArrayCollection attribute $items
class Order
{
.....
public function setItemsArray(Doctrine\Common\Collections\ArrayCollection $itemsArray = null){
if(null){
$this->items->clear();
}else{
$this->items = $itemsArray;
}
....
}
And then in my Controller where I process the form for Order.
//get entity manager
$em = $this->getDoctrine()->getManager();
//get order information (with items)
$order = $form->getData();
//pull out items array from order
$items = $order->getItems();
//clear the items from the order
$order->setItemsArray(null);
//persist and flush the Order object
$em->persist($order);
$em->flush();
//reintroduce the order items to the order object
$order->setItemsArray($items);
//persist and flush the Order object again ):
$em->persist($order);
$em->flush();
It sucks that you have to persist and flush twice (see more here Persist object with two foreign identities in doctrine). But that is doctrine for you, with all of it's power, it sure can put you in a bind. But thankfully you will only have to do this when creating a new object, not editing, because the object is already in the database.
You need to persist and flush the original before you can persist and flush the relationship records. You are 100% correct in the reason for the error.
I assume from the diagram that you are trying to add and order and the relation to the contact at the same time? If so you need to persist and flush the order before you can persist and flush the relationship. Or you can add a primary key to the Relation table.
I ended up creating a separated primary key on my Relation table (instead of having the composite one).
It looks like it is a dirty fix, and I am sure there is a better way to handle this situation but it works for now.
Here is my Relations entity:
/**
* Relation
*
* #ORM\Entity
*/
class Relation
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Contact", inversedBy="relation")
*/
protected $contact;
/**
* #ORM\ManyToOne(targetEntity="Order", inversedBy="relation")
*/
protected $order;
/**
* #var integer
*
* #ORM\Column(name="invoice", type="integer", nullable=true)
*/
private $invoice;
//Rest of the entity...
I then added the cascade={"persist"} option on the OneToMany relation with Order:
/**
* Orders
*
* #ORM\Entity
*/
class Order
{
/**
* #ORM\OneToMany(targetEntity="Relation", mappedBy="order", cascade={"persist"})
*/
protected $relation;
//Rest of the entity...
Et voilĂ !
I'm using Symfony 2.1.2.
I have two entities and define a [many-to-one (bidirectional)] (1) association between them. I don't want to use the primary key for the foreign key (referencedColumnName). I want to use another integer unique column: customer_no
/**
* #ORM\Entity
* #ORM\Table(name="t_myuser")
*/
class MyUser extends BaseEntity // provides an id (pk)
{
/**
* #ORM\ManyToOne(targetEntity="Customer", inversedBy="user")
* #ORM\JoinColumn(name="customer_no", referencedColumnName="customer_no", nullable=false)
*/
public $customer;
}
/**
* #ORM\Entity
* #ORM\Table(name="t_customer")
*/
class Customer extends BaseEntity // provides an id (pk)
{
/**
* #ORM\Column(type="integer", unique=true, nullable=false)
*/
public $customer_no;
/**
* #ORM\OneToMany(targetEntity="MyUser", mappedBy="customer")
*/
public $user;
}
When I try to persist a MyUser entity with an Customer entity, I get this error:
Notice: Undefined index: customer_no in ...\vendor\doctrine\orm\lib\Doctrine\ORM\Persisters\BasicEntityPersister.php line 608
The schema on the db looks fine, these should be the important sql schema definitions:
CREATE UNIQUE INDEX UNIQ_B4905AC83CDDA96E ON t_customer (customer_no);
CREATE INDEX IDX_BB041B3B3CDDA96E ON t_myuser (customer_no);
ALTER TABLE t_myuser ADD CONSTRAINT FK_BB041B3B3CDDA96E FOREIGN KEY (customer_no)
REFERENCES t_customer (customer_no) NOT DEFERRABLE INITIALLY IMMEDIATE;
So there is definitely an index for customer_no
//update:
I fix the inversedBy and mappedBy stuff, but this is not the problem.
(1) : http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html#one-to-many-bidirectional
#m2mdas:
Yes you're right, I thought it's possible because JPA (which has influence to doctrine) has this feature. The attribute referencedColumnName only for the case when your property does not match the table column.
Whatever, I found a solution by patching the BasicEntityPersister.php, see here the gist on github: https://gist.github.com/3800132
the solution is to add the property/field name and value for the mapping column. This information is already there but not bound to the right place. It have to be added to the $newValId arrray this way:
$fieldName = $targetClass->getFieldName($targetColumn);
$newValId[$fieldName] = $targetClass->getFieldValue($newVal, $fieldName);
It only works for ManyToOne reference. ManyToMany doesn't work.
For ManyToOne I test it with already existing entities. You can test it, too:
change the doctrine annotation in tests/Doctrine/Tests/Models/Legacy/LegacyArticle.php
from
#JoinColumn(name="iUserId", referencedColumnName="iUserId")
to
#JoinColumn(name="username", referencedColumnName="sUsername")