I've implement Class Table Inheritance using Doctrine 2 in my Symfony 3 project, so as to have one base profile table, that houses both employee and company profiles.
When trying to persist a sub class (EmployeeProfile) of the mapped super class (AbstractProfile), I get the following error:
An exception occurred while executing 'INSERT INTO profile (id) VALUES
(?)' with params [27, 10, 85, \"employee\"]:\n\nSQLSTATE[HY093]:
Invalid parameter number: number of bound variables does not match
number of tokens
I'm not entirely sure what's going wrong, and why Doctrine is generating a query that's entirely ignoring the AbstractProfile's properties. Initially I thought it was due to said properties not being visible to the children, but even after setting the properties to protected, the error remains.
How exactly can I fix this, or am I trying to fit a square peg into a round hole by not using this functionality for what is was intended?
profile DB Table:
+------------+--------------+------+-----+-------------------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+--------------+------+-----+-------------------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| user_id | int(11) | NO | | NULL | |
| type | varchar(100) | NO | | NULL | |
| status | int(11) | NO | | NULL | |
| created_at | datetime | NO | | CURRENT_TIMESTAMP | |
| updated_at | datetime | NO | | CURRENT_TIMESTAMP | |
+------------+--------------+------+-----+-------------------+----------------+
AbstractProfile Super Class:
/**
* AbstractProfile
*
* #ORM\Table(name="profile")
* #ORM\Entity(repositoryClass="ProfileBundle\Repository\ProfileRepository")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="type", type="string")
* #ORM\DiscriminatorMap({
* "employee" = "EmployeeProfile",
* "company" = "CompanyProfile"
* })
*/
abstract class AbstractProfile
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #var int
*
* #ORM\Column(name="status", type="integer")
*/
protected $status;
/**
* #ORM\OneToOne(targetEntity="User", inversedBy="profile")
* #ORM\JoinColumn(name="user_id", referencedColumnName="id")
*/
protected $user;
//... Getters, setters and all the rest
}
EmployeeProfile Sub Entity:
<?php
/**
* EmployeeProfile
*
* #ORM\Table(name="profile")
* #ORM\Entity
*/
class EmployeeProfile extends AbstractProfile
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Skill", inversedBy="profiles")
* #ORM\JoinTable(name="profile_skills",
* joinColumns={#ORM\JoinColumn(name="profile_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="skill_id", referencedColumnName="id", unique=true)}
* )
*/
private $skills;
public function __construct()
{
$this->skills = new ArrayCollection();
}
//... Getters, setters and all the rest
}
CompanyProfile Sub Entity:
<?php
/**
* CompanyProfile
*
* #ORM\Table(name="profile")
* #ORM\Entity
*/
class CompanyProfile extends AbstractProfile
{
/**
* #var ArrayCollection
*
* #ORM\ManyToMany(targetEntity="Event", inversedBy="profiles")
* #ORM\JoinTable(name="profile_events",
* joinColumns={#ORM\JoinColumn(name="profile_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="event_id", referencedColumnName="id", unique=true)}
* )
*/
private $events;
public function __construct()
{
$this->events = new ArrayCollection();
}
//... Getters, setters and all the rest
}
It looks like you are trying to use #ORM\InheritanceType("JOINED") with a single table. You use #ORM\Table(name="profile") in your 3 entities.
The result is that Doctrine don't know what to do with your entities.
You could try replacing #ORM\InheritanceType("JOINED") by #ORM\InheritanceType("SINGLE_TABLE").
Related
Is this OneToOne relation possible with Doctrine (in Symfony)?
User Time
+----------+ +-------+
| ... | | ... |
| time_id |o-----+-----o| user |
| | | | ... |
| time2_id |o-----+ | type |
| ... | +-------+
+----------+
That is, a User can potentially have two different Time entities associated to it:
one of type=0 and the other one of type=1.
I could have splitted Time in two different entities, but I thought that this way I
could spare replicating some code, as Time entity has an eventListener and more code associated...
I have coded it like this:
class Time
{
...
/**
* #ORM\OneToOne(targetEntity=User::class, mappedBy="time, cascade={"persist"})
* #ORM\OneToOne(targetEntity=User::class, mappedBy="time2", cascade={"persist"})
*/
private $user;
...}
class User
{
...
/**
* #ORM\OneToOne(targetEntity=Time::class, inversedBy="user", cascade={"persist"})
* #ORM\JoinColumn(name="time_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
*/
private $time;
/**
* #ORM\OneToOne(targetEntity=Time::class, inversedBy="user", cascade={"persist"})
* #ORM\JoinColumn(name="time2_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
*/
private $time2;
...}
And in fact I am flawlessly using it, but Symfony silently complains (in Symfony toolbar/Doctrine/Entities Mapping > Mapping errors) with:
App\Entity\User
The mappings App\Entity\User#time2 and App\Entity\Time#user are inconsistent with each other.
Obviously, Doctrine doesn't like the second #ORM\OneToOne on $user, and is discarding it.
What hypotethical misbehaviours could this code lead to?
Note: doctrine/common v2.11
FWIW, it is possible to make this relation, as far as one renounces to make a Bidirectional Association:
User Time
+----------+ +-------+
| ... | | ... |
| time_id |o-----+-----o| |
| | | | |
| time2_id |o-----+ | type |
| ... | +-------+
+----------+
implemented by:
class Time
{
...
}
class User
{
...
/**
* #ORM\OneToOne(targetEntity=Time::class, cascade={"persist"})
* #ORM\JoinColumn(name="time_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
*/
private $time;
/**
* #ORM\OneToOne(targetEntity=Time::class, cascade={"persist"})
* #ORM\JoinColumn(name="time2_id", referencedColumnName="id", nullable=true, onDelete="SET NULL")
*/
private $time2;
...}
Also look at this link for Owning vs Inverse relations.
In my case, this totally fits my needs, as the inverse part is for methods like:
$Time->getUser();
which I can prescind of.
I've got two entities mapped like this:
namespace App\Entity\Email;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Contract\Entity\BlameableInterface;
use Knp\DoctrineBehaviors\Contract\Entity\SoftDeletableInterface;
use Knp\DoctrineBehaviors\Contract\Entity\TimestampableInterface;
use Knp\DoctrineBehaviors\Model\Blameable\BlameableTrait;
use Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletableTrait;
use Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait;
/**
* #ORM\Entity(repositoryClass="App\Repository\Email\EmailRepository")
*/
class Email implements SoftDeletableInterface, TimestampableInterface, BlameableInterface
{
use SoftDeletableTrait;
use TimestampableTrait;
use BlameableTrait;
/**
* #ORM\Column(type="integer")
* #ORM\Id()
* #ORM\GeneratedValue()
*/
private ?int $id = null;
...
/**
* #var Collection|EmailAddress[]
* #ORM\OneToMany(targetEntity="App\Entity\Email\EmailAddress", mappedBy="sentEmail", cascade={"all"}, orphanRemoval=true)
*/
private ?Collection $senders = null;
/***
* #var Collection|EmailAddress[]
* #ORM\OneToMany(targetEntity="App\Entity\Email\EmailAddress", mappedBy="receivedEmail", cascade={"all"}, orphanRemoval=true)
*/
private ?Collection $recipients = null;
...
}
and
namespace App\Entity\Email;
use Doctrine\ORM\Mapping as ORM;
use Knp\DoctrineBehaviors\Contract\Entity\BlameableInterface;
use Knp\DoctrineBehaviors\Contract\Entity\SoftDeletableInterface;
use Knp\DoctrineBehaviors\Contract\Entity\TimestampableInterface;
use Knp\DoctrineBehaviors\Model\Blameable\BlameableTrait;
use Knp\DoctrineBehaviors\Model\SoftDeletable\SoftDeletableTrait;
use Knp\DoctrineBehaviors\Model\Timestampable\TimestampableTrait;
/**
* #ORM\Entity()
*/
class EmailAddress implements SoftDeletableInterface, TimestampableInterface, BlameableInterface
{
use SoftDeletableTrait;
use TimestampableTrait;
use BlameableTrait;
/**
* #ORM\Column(type="integer")
* #ORM\Id()
* #ORM\GeneratedValue()
*/
private ?int $id = null;
...
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Email\Email", inversedBy="senders")
* #ORM\JoinColumn()
*/
private ?Email $sentEmail = null;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Email\Email", inversedBy="recipients")
* #ORM\JoinColumn()
*/
private ?Email $receivedEmail = null;
...
}
From unknown reason to me I'm getting message from Doctrine:
The association App\Entity\Email\EmailAddress#receivedEmail refers to the inverse side field App\Entity\Email\Email#recipients which does not exist.
What is wrong with my mapping? I really don't see any error. I've asked my colegues to check my code and they also don't see any problem. Strange thing is that relation sentEmail->senders is mapped according to doctrine right and it's working.
I've also tried to change OneToMany mapping to ManyToMany like this but I've still got same error.
EDIT 1:
Date in database:
Table email
id | created_by_id | updated_by_id | deleted_by_id | deleted_at | created_at | updated_at | subject | content
1 | NULL | NULL | NULL | NULL | 1616156920 | 1616156920 | Test | Test
Table email_address
id | created_by_id | updated_by_id | deleted_by_id | address | deleted_at | created_at | updated_at | sent_email_id | received_email_id
1 | NULL | NULL | NULL | test1#test.com | NULL | 1616156920 | 1616156920 | NULL | 1
2 | NULL | NULL | NULL | test2#test.com | NULL | 1616156920 | 1616156920 | 1 | NULL
This question/answer probably follows under the heading of a 'typo' but I thought it might be interesting to discuss.
The problem is here:
/***
* #var Collection|EmailAddress[]
* #ORM\OneToMany(targetEntity="App\Entity\Email\EmailAddress", mappedBy="receivedEmail", cascade={"all"}, orphanRemoval=true)
*/
private ?Collection $recipients = null;
The extra asterisk /*** in the annotation block opening caused the recipients property to be skipped by Doctrine. But everything still seemed to be okay. The database tables and indexes were all generated as expected.
I made a simple console command to insert an email entity and quickly noticed that the sender address was inserted but not the recipient address. Triple checked the various methods but still no go. Explicitly persisting the address worked but the cascade option should have taken care of that. And of course retrieving the recipient address was not working even after an entity was inserted.
At some point I noticed the /*** and changed it to /** and everything worked as expected. Using a console command to test helped quite a bit as opposed to refreshing a browser and hunting around in the debug bar.
On the plus side, if you ever need to temporarily remove an annotation then just adding an asterisk is basically the same as commenting it out.
In my work, I am trying to map the legacy database scheme with Doctrine. I can't change this scheme, because it is used by other company applications. Here is a brief scheme overview:
Table - global_register_item
ID | NAME | DTYPE
1 | "global register item with article #1" | law_regulation
2 | "global register item without article #1" | financial_reporter
3 | "global register item without article #2" | law_regulation
4 | "global register item without article #3" | law_regulation
5 | "global register item with article #2" | financial_reporter
Table - article
ID | SID | other fields which I actually do not need
1 | 89 | ...
5 | 45 | ...
Table - law_regulation
ID | other fields
1 | ...
3 | ...
4 | ...
Table - financial_reporter
ID | other fields
2 | ...
5 | ...
So global_register_item is a parent and law_regulation and financial_reporter inherit from this table. To solve this I used class table inheritance and it works fine.
The problem is a relation between global_register_item and article. It is one-to-one relation and joining is done via their ID columns (if there is a record in global_register_item with relation to article, there is a record in article table with the same ID). But some records in global_register_item doesn't have record in article. Is there any way how to map this relation with Doctrine?
EDIT 1
Here is my PHP code from the project. Btw. I only need to read records. And I need to get information about SID column to my GlobalRegisterItem entity.
Class GlobalRegisterItem
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="global_register_item")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="DTYPE", type="string")
* #ORM\DiscriminatorMap({
* "law_regulation" = "App\Entity\LawRegulation",
* "financial_reporter" = "App\Entity\FinancialReporter"})
*/
abstract class GlobalRegisterItem
{
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
*/
private $id;
/**
* #var string
* #ORM\Column(name="name", type="string")
*/
private $name;
/**
* Article|null
* HOW TO MAP THIS?
*/
private $article;
}
Class Article
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="article")
*/
class Article
{
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
*/
private $id;
/**
* #var int
* #ORM\Column(name="SID", type="int")
*/
private $sid;
}
Class LawRegulation
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="law_regulation")
*/
class LawRegulation extends GlobalRegisterItem
{
/** SOME MAPPED FIELDS */
}
Class FinancialReporter
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="law_regulation")
*/
class FinancialReporter extends GlobalRegisterItem
{
/** SOME MAPPED FIELDS */
}
You can use a OneToOne bidirectional relation, in the Article entity:
/**
* #ORM\OneToOne(targetEntity="GlobalRegisterItem", inversedBy="article")
* #ORM\JoinColumn(name="id", referencedColumnName="id", nullable=true)
*/
private $item;
And reference it in your GlobalRegisterItem class:
/**
* Article|null
* #ORM\OneToOne(targetEntity="Article", mappedBy="item")
*/
private $article;
/**
* Gets the sid of the article (if any).
* #returns int|null
*/
public function getArticleSid()
{
if (null !== $this->article) {
return $this->article->getSid();
}
return null;
}
One possible approach how to solve this (if you just need to read data) is:
Create view from global_register_item and article
SELECT global_register_item.*, article.SID AS `SID`
FROM global_register_item
LEFT JOIN article ON global_register_item.id = article.id
ID | NAME | DTYPE | SID
1 | "global register item with article #1" | law_regulation | 89
2 | "global register item without article #1" | financial_reporter | NULL
3 | "global register item without article #2" | law_regulation | NULL
4 | "global register item without article #3" | law_regulation | NULL
5 | "global register item with article #2" | financial_reporter | 45
Class GlobalRegisterItem
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="global_register_item_view")
* #ORM\InheritanceType("JOINED")
* #ORM\DiscriminatorColumn(name="DTYPE", type="string")
* #ORM\DiscriminatorMap({
* "law_regulation" = "App\Entity\LawRegulation",
* "financial_reporter" = "App\Entity\FinancialReporter"})
*/
abstract class GlobalRegisterItem
{
/**
* #var int
* #ORM\Column(name="id", type="integer")
* #ORM\Id
*/
private $id;
/**
* #var string
* #ORM\Column(name="name", type="string")
*/
private $name;
/**
* int|null
* #ORM\Column(name="SID", type="integer")
*/
private $sid;
}
I think this is far from being optimal, but this is the best solution I could come up with.
I have got two classes which are being associated using one to one uni-direction
{
id: 1,
name: "onetooneuniparent name",
onetooneunichild: {
id: 1,
name: "onetooneunichild name",
__initializer__: null,
__cloner__: null,
__isInitialized__: true
}
}
the above is the result when I do query like following
http://localhost:8000/onetooneRead?id=1
I want to know where and why the following come from
__initializer__: null,
__cloner__: null,
__isInitialized__: true
my expected result is just this
{
id: 1,
name: "onetooneuniparent name",
onetooneunichild: {
id: 1,
name: "onetooneunichild name"
}
}
OnetoOneUniParent.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="onetooneuniparent")
*/
class OnetoOneUniParent{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string",name="name")
*/
private $name;
/**
* #ORM\OneToOne(targetEntity="OnetoOneUniChild",cascade={"persist"})
* #ORM\JoinColumn(name="child_id", referencedColumnName="id")
*/
private $onetooneunichild;
<.... getter and setter here ...>
}
OnetoOneUniChild.php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* #ORM\Entity
* #ORM\Table(name="onetooneunichild")
*/
class OnetoOneUniChild{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\Column(type="string",name="name")
*/
private $name;
<.... getter and setter here ...>
This is the method in controller
/**
* #Route("/onetooneRead")
* #Method("GET")
*/
public function onetooneReadAction(Request $request){
$logger = $this->get('logger');
$encoders = array(new XmlEncoder(), new JsonEncoder());
$normalizers = array(new ObjectNormalizer());
$serializer = new Serializer($normalizers, $encoders);
$logger->info('onetoone Read');
$id = $request->query->get("id");
$em = $this->getDoctrine()->getManager();
$onetooneuniparent = $em->getRepository('AppBundle:OnetoOneUniParent')->find($id);
$onetooneuniparentJson = $serializer->serialize($onetooneuniparent, 'json');
$response = new JsonResponse();
$response->setContent($onetooneuniparentJson);
return $response;
}
This is what is inside in MySQL
mysql> select * from onetooneuniparent;
+----+----------+------------------------+
| id | child_id | name |
+----+----------+------------------------+
| 1 | 1 | onetooneuniparent name |
| 2 | 2 | onetooneuniparent name |
| 3 | 3 | onetooneuniparent name |
+----+----------+------------------------+
3 rows in set (0.00 sec)
mysql> select * from onetooneunichild;
+----+-----------------------+
| id | name |
+----+-----------------------+
| 1 | onetooneunichild name |
| 2 | onetooneunichild name |
| 3 | onetooneunichild name |
+----+-----------------------+
3 rows in set (0.00 sec)
Those functions are part of the Doctrine proxy coding, since you are using Lazy Loading Doctrine needs to keep track of the child entity if it needs to be loaded or not. Part of that keeping track is these functions (I believe it is in this portion of Doctrine)
There may be a way around this which would be to avoid using lazy loading. To do that you can utilize EAGER loading if you always want the child to load with the parent. Alternatively if you only want to use EAGER for this one query and not every time you would have to switch to DQL as documented here or you could use the JOIN comma (second example down) here
I have two tables (test and question) and the middle table (n-m). In this point all works fine.
But now, I need to put extra information in the (n-m) table, the order of this question in this test
I need this:
id | test_id | question_id | order
1 | 1 | 1 | 3
2 | 1 | 2 | 2
3 | 1 | 3 | 1
4 | 1 | 4 | 4
All these relationship have made with doctrine annotation...
Test Entity
/**
* #ORM\ManyToMany(targetEntity="Question", inversedBy="tests")
*/
private $questions;
Question Entity
/**
* #ORM\ManyToMany(targetEntity="Test", mappedBy="questions"))
*/
private $tests;
Any help will be appreciated
EDIT
Hi again!
Thanks a lot to #DonCallisto
my entities at the end:
Test
/**
* #ORM\OneToMany(targetEntity="RTestQuestion", mappedBy="question")
*/
private $questions;
Question
/**
* #ORM\OneToMany(targetEntity="RTestQuestion", mappedBy="test"))
*/
private $tests;
My new entity "RTestQuestion"
/**
* ET\BackendBundle\Entity\RTestQuestion
*
* #ORM\Table(name="rtest_question")
* #ORM\Entity
*/
class RTestQuestion {
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="Question", inversedBy="questions", cascade={"persist", "remove"})
*/
private $question;
/**
* #ORM\ManyToOne(targetEntity="Test", inversedBy="tests", cascade={"persist", "remove"})
*/
private $test;
/**
* #var integer $order
*
* #ORM\Column(name="question_order", type="integer", nullable=true)
*/
private $question_order;
I had to make two changes:
The properties need the cascade on persist and remove actions (doctrine console show errors without this)
And the word "order" are restricted for mysql, and now become a question_order.
And, again, thanks to #DonCallisto!
Split the relationship into a 1-n and m-1 as follows
Test Entity --- (1 - m) ---> RTestQuestion Entity <--- (m - 1) --- Question
So your code will be
Test Entity
/**
* #ORM\OneToMany(targetEntity="RTestQuestion", inversedBy="question")
*/
private $questions;
Question Entity
/**
* #ORM\OneToMany(targetEntity="RTestQuestion", mappedBy="test"))
*/
private $tests;
RTestQuestion Entity
/**
* #ORM\ManyToOne(targetEntity="Question", mappedBy="questions"))
*/
private $question;
/**
* #ORM\ManyToOne(targetEntity="Test", mappedBy="tests"))
*/
private $test;
/**
* EXTRA ATTRIBUTES HERE
*/
Remember that an association with extra fields isn't an association anymore but a new entity!