How do I enable duplicate Discriminator Maps, allow a "default" mapping, or completely ignore Discriminator mappings when running a DQL query?
Setup:
// DQL Query:
$this->createQueryBuilder('s')
->select(['s.serverId', 'p.projectName'])
->leftJoin('s.serverServices', 'ss')
->leftJoin('ss.serverServiceProjects', 'ps')
->leftJoin('ps.project', 'p')
->getQuery()
->getArrayResult()
// Mapping
/**
* #ORM\InheritanceType("SINGLE_TABLE")
* #ORM\DiscriminatorColumn(name="service_type_id", type="integer")
* #ORM\DiscriminatorMap({
* "1" = "AppBundle\Entity\ServerService",
* "2" = "AppBundle\Entity\ServerService",
* [...]
* "12" = "AppBundle\Entity\Service\SubService",
* "" = "AppBundle\Entity\ServerService"
* })
*/
The join portion that's generated:
LEFT JOIN server_services s2_ ON s0_.server_id = s2_.server_id AND s2_.service_type_id IN ('1', '12')
This is entirely incorrect. I have an entire mapping of 1 - 12. And an "" empty string match. If a row does not have a known mapping (in my code) OR if it's empty, it should be set to the default base ServerService.
I did find this post: Leave out discriminator part of Doctrine' generated SQL
However, it's back in 2014, made mention of a difference in version, and does not compensate for duplicate, or default Mappings.
EDIT: For the record, I tried the ignore route -- it didn't work.
$q->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, MySqlWalker::class)
->setHint(MySqlWalker::IGNORE_DISCRIMINATION, array('ss'));
Still returns the above SQL Joint statement. It does call MySqlWalker; it does go into the setInheritanceType() call, but it does not ignore the Discriminator mapping. :/
Found an answer to one option: Ignoring
In Doctrine 2.2.x, my statements were in the JOIN, not WHERE clause. Using the example in Leave out discriminator part of Doctrine' generated SQL, I had to overload the ->walkJoin() method to get ignore to work properly:
/**
* {#inheritdoc}
*/
public function walkJoin($join)
{
$this->checkForHint();
return parent::walkJoin($join);
}
protected function checkForHint()
{
$ignoreDescription = $this->getQuery()->getHint(self::IGNORE_DISCRIMINATION);
if ($ignoreDescription) {
foreach ($this->getQueryComponents() as $k => $component) {
if (in_array($k, $ignoreDescription)) {
/** #var $meta ClassMetadata */
$meta = $component['metadata'];
$meta->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_NONE);
}
}
}
}
Still have not resolved Duplicate or Default discriminator values...
Related
In order to solve a problem I asked about earlier, I am trying to create a custom repository function that will determine whether an instance of Repair is unique, based on the device, name, and colors constraints.
Here's my Doctrine Annotation for class Repair. Mind that the device property is Many To One (many Repairs for one Device), and that colors is Many to Many.
/**
* #ORM\Table(name="repair")
* #ORM\Entity(repositoryClass="AppBundle\Repository\RepairRepository")
* #UniqueEntity(fields={"name", "device", "colors"}, repositoryMethod="getSimilarRepairs", message="Repair {{ value }} already exists for this name, device and colour combination.")
*/
This is my RepairRepository.php, in which $criteria['colors'] is an array.
public function getSimilarRepairs(array $criteria) {
$builder = $this->createQueryBuilder('r')
->where('r.device = :device')
->andWhere('r.colors = :colors')
->andWhere('r.name = :name')
->setParameters(['deviceid'=>$criteria['device'],'colors'=>$criteria['colors'],'name'=>$criteria['name']]);
return $builder->getQuery();
}
I have three problems that can probably be brought back to one:
editing: with every change, causing a duplicate or not, I get the message that a duplicate entity exists.
editing: despite the error message, name changes are performed anyway!
adding: I can create as many duplicates as I like, there never is an error message.
Your problem is that the colors relation is a ManyToMany.
In SQL you can not query '=' on this relation.
It is very complicated, that's why Doctrine (and we probably) can't make it alone .
A partial solution to build a query :
public function getSimilarRepairs(array $criteria) {
$builder = $this->createQueryBuilder('r')
->where('r.device = :device')
->andWhere('r.name = :name')->setParameter('name',$criteria['name'])
->andWhere('r.colors = :colors')->setParameter('deviceid',$criteria['device']);
// r matches only if each of your colors exists are related to r :
$i=0;
foreach($criteria['colors'] as $color){
$i++;
$builder->join('r.colors','c'.$i)->andWhere('c = :color'.$i)->setParameter('color'.$i,$color);
}
// Then you had also to check than there is no other color related to r :
// I don't know how
return $builder->getQuery();
}
But let me propose another solution :
In your repair entity, your can store a duplicate of your related colours :
/**
* #var string
*
* #ORM\Column(name="name_canonical", type="string")
*/
private $serializedColors;
set it with doctrine lifecycle events :
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function updateColors()
{
$serializedColors = '';
foreach($this->colors as $color){
$serializedColors .= $color->getId().'#';
}
$this->serializedColors = $serializedColors;
}
Don't forget to add #HasLifecycleCallbacks
Then change your UniqueEntityConstraint to fields={"name", "device", "serializedColors"}, forget the custom query, and it will work.
This is the craziest thing I ever seen.
My Symfony2 and Doctrine has gone mad
I have MySQL database with few different fields that are TINYINT.
Those are not nullable and all records have those values set to 0 or 1.
All doctrine ORM mapping ale set correctly (I doublechecked it hundred times).
All getters are set correctly (doublechecked as well).
But then - for some objects it doesn't work ... some of the TINYINT are not correctly translated to BOOLEAN as it should (and as it works with other objects and with other fields of that enity)... instead it gives NULL - even if in database this TINYINT is set to "1" (or "0" - it doesn't matter).
For some other objects (of the same entity) it works fine TINYINT = "1" is correctly recognized as true and "0" as false boolean.
Those my examples:
Events.orm.xml:
<entity name="Events" table="events">
<change-tracking-policy>DEFERRED_IMPLICIT</change-tracking-policy>
<field name="eventDeleted" type="boolean" column="_event_deleted"/>
Entity: "Events.php"
the mapping:
/**
* #var boolean $eventDeleted
*
* #ORM\Column(name="_event_deleted", type="boolean", nullable=false)
*/
private $eventDeleted;
and getter and setter:
/**
* Set eventDeleted
*
* #param boolean $eventDeleted
*/
public function setEventDeleted($eventDeleted)
{
$this->eventDeleted = $eventDeleted;
}
/**
* Get eventDeleted
*
* #return boolean
*/
public function getEventDeleted()
{
return $this->eventDeleted;
}
The database is:
And as I said - for some objects it works and gives "1" or "0" when accessed by for example:
{{ event[0].getEventDeleted }}
and sometimes it gives NULL.
I found some "similar" issue mentioned in doctrine jira, but no guess what solves that and what could be the reason: http://www.doctrine-project.org/jira/browse/DDC-1967
Any idea?
Try creating the query "by hand" with the query builder.
I also had the same problem on symfony 2.1 while fetching an entity from the session variable, a boolean field was returning null no matter what value it had. Using the query builder made it work for me.
EDIT
Example:
$qb = $em->createQueryBuilder();
$query = $qb->select('e.eventDeleted')
->from('BundleName:Events', 'e')
->where('e.id = :id')
->setParameter('id', $id)
->getQuery();
$eventDeleted = $query->getSingleResult();
I need to create a simple query that produces a result set of a database entry plus the username of the person that posted it.
I've tried to setup the associations properly but I'm not sure if that's right either. I'm finding the whole idea of using these small string identifiers quite confusing. Surely there must be a simpler way of doing a join?
My two entities:
class Users
{
// ...
/**
* #ORM\Column(type="string")
* #ORM\OneToMany(targetEntity="Titles", mappedBy="addedBy")
*/
protected $username;
// ..
}
and
class Titles
{
// ....
/**
* #ORM\Column(type="string")
* #ORM\ManyToOne(targetEntity="Users", inversedBy="username")
*/
protected $addedBy;
// ....
}
with the following in the controller:
$titles = $em->createQueryBuilder()
->select('t.*', 'u.*')
->from('dvdLoggerdvdBundle:Titles', 't')
->leftJoin('t.addedBy', 'u')
->addOrderBy('t.title', 'DESC')
->getQuery()
->getResult();
I'm getting the following error:
[Semantical Error] line 0, col 69 near 'u ORDER BY t.title': Error: Class
dvdLogger\dvdBundle\Entity\Titles has no association named addedBy `
Update 1
I made all the changes suggested by Tom and did lots of reading!
It appears that in order to overcome the lazy loading feature I need to carry out a leftJoin. I have rewritten my query as follows:
public function getAllTitles()
{
// view all records in db
$titles = $this->createQueryBuilder('t')
->select('t, u')
->leftJoin('t.addedBy', 'u')
->addOrderBy('t.title', 'DESC');
return $titles->getQuery()->getResult();
}
I am getting a result set, but the addedBy is returning NULL when I dump the result set. As far as I'm aware shouldn't this pull the associated field in from the other table?
Best practice is to reference the entity by its id, you are trying to reference it using the username. The inversed field should also be a specific field not an existing one that holds data. And keep it mind this field is optional and defines the associations as bidirectional, for the specified use case you don't actually need it as you are joining from the Titles entity. I would advice reading the doc here http://symfony.com/doc/current/book/doctrine.html#entity-relationships-associations as well as here http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/association-mapping.html
Bidirectional association (w/ inversed field)
First get rid of that line:
#ORM\Column(type="string")
In your $addedBy annotations and change inverseBy="username" to inversedBy="titles" (note the typo)
You optionaly could add
#ORM\JoinColumn(name="user_id", referencedColumnName="id")
Then in your Users Entity add
/**
*
* #ORM\OneToMany(targetEntity="Titles", mappedBy="addedBy")
*/
protected $titles;
And get rid of
* #ORM\OneToMany(targetEntity="Titles", mappedBy="addedBy")
In your $username annotations
Last make sure you update the database schema
Then your query should return the expected result.
Unidirectional association (w/out inversed field)
Get rid of
* #ORM\OneToMany(targetEntity="Titles", mappedBy="addedBy")
In your $username annotations
Then get rid of that line in your $addedBy annotations:
#ORM\Column(type="string")
As well as inverseBy="username"
You optionaly could add
#ORM\JoinColumn(name="user_id", referencedColumnName="id")
Last make sure you update the database schema
Then your query should return the expected result.
I'm doing a project in Symfony 2.3 with Doctrine 2.4 using MySQL database.
I have an Entity of FieldValue (simplified):
class FieldValue
{
/**
* The ID
*
* #var integer
*/
protected $fieldValueId;
/**
* Id of associated Field entity
*
* #var integer
*/
protected $fieldId;
/**
* Id of associated user
*
* #var integer
*/
protected $userId;
/**
* The value for the Field that user provided
*
* #var string
*/
protected $userValue;
/**
* #var \MyProjectBundle\Entity\Field
*/
protected $field;
/**
* #var \MyProjectBundle\Entity\User
*/
protected $user;
The problem I have is the fact that $userValue, while it's LONGTEXT in my database, can represent either actual text value , date or number, depending in the type of the Field.
The Field can be dynamically added. After adding a one to any of the users every other user can also fill it's own value for that Field.
While querying the database I use orderBy to sort on a certain column, which also can be one of those Fields. In that case I need to sort on $userValue. This is problematic when I need to have number fields sorted as numbers, and not as strings ('123' is less than '9' in that case...).
The solution for it (I thought) is to CAST the $sort, so I would get SQL like:
ORDER BY CAST(age AS SIGNED INTEGER) ASC
Since Doctrine does not have a built-in DQL function for that, I took the liberty of adding that to my project as INT DQL function (thanks to Jasper N. Brouwer):
class CastAsInteger extends FunctionNode
{
public $stringPrimary;
public function getSql(SqlWalker $sqlWalker)
{
return 'CAST(' . $this->stringPrimary->dispatch($sqlWalker) . ' AS SIGNED INTEGER)';
}
public function parse(Parser $parser)
{
$parser->match(Lexer::T_IDENTIFIER);
$parser->match(Lexer::T_OPEN_PARENTHESIS);
$this->stringPrimary = $parser->StringPrimary();
$parser->match(Lexer::T_CLOSE_PARENTHESIS);
}
}
So happy with myself finding an easy solution I did that:
$sort = "INT(".$sort.")";
$queryBuilder->orderBy($sort, $dir);
which produced expected DQL:
ORDER BY INT(age) ASC
But also produced an exception:
An exception has been thrown during the rendering of a template ("[Syntax Error] line 0, col 12272: Error: Expected end of string, got '('") in MyProject...
So I've tried to find out what is going on and got into this in Doctrine\ORM\Query\Parser.php:
/**
* OrderByItem ::= (
* SimpleArithmeticExpression | SingleValuedPathExpression |
* ScalarExpression | ResultVariable
* ) ["ASC" | "DESC"]
*
* #return \Doctrine\ORM\Query\AST\OrderByItem
*/
public function OrderByItem()
{
...
}
Does that mean that there is no possibility to use DQL functions inside ORDER BY?
And if this is the case - is there any other way to achieve this?
UPDATE
I actually already have INT used in my select query, inside CASE WHEN:
if ($field->getFieldType() == 'number') {
$valueThen = "INT(".$valueThen.")";
}
$newFieldAlias = array("
(CASE
WHEN ...
THEN ".$valueThen."
ELSE ...
END
) as ".$field->getFieldKey());
Later on the $newFieldAlias is being added to the query.
Doesn't change anything...
UPDATE 2
Even when I add an extra select to the query, which will result in this DQL:
SELECT age, INT(age) as int_age
and then sort like that:
ORDER BY int_age ASC
I still don't het the correct result.
I've checked var_dump from $query->getResult(), and this is what I got:
'age' => string '57' (length=2)
'int_age' => string '57' (length=2)
Like CAST does not matter. I'm clueless...
Doctrine DQL does not accept functions as sort criteria but it does accept a "result variable". It means that you can do the following:
$q = $this->createQueryBuilder('e')
->addSelect('INT(age) as HIDDEN int_age')
->orderBy('int_age');
Doctrine 2 does not support INT by default, but you can use age+0.
$q = $this->createQueryBuilder('e')
->addSelect('age+0 as HIDDEN int_age')
->orderBy('int_age');
It is problem in your parser.php file. I have similar kind of issue and I solve this issue to replace below code in my parser file.
/**
* OrderByClause ::= "ORDER" "BY" OrderByItem {"," OrderByItem}*
*
* #return \Doctrine\ORM\Query\AST\OrderByClause
*/
public function OrderByClause()
{
$this->match(Lexer::T_ORDER);
$this->match(Lexer::T_BY);
$orderByItems = array();
$orderByItems[] = $this->OrderByItem();
while ($this->lexer->isNextToken(Lexer::T_COMMA)) {
$this->match(Lexer::T_COMMA);
$orderByItems[] = $this->OrderByItem();
}
return new AST\OrderByClause($orderByItems);
}
Just use this:
->orderBy('u.age + 0', 'ASC');
Documentation states:
class Cart
{
// ...
/**
* #OneToOne(targetEntity="Customer", inversedBy="cart")
* #JoinColumn(name="customer_id", referencedColumnName="id")
*/
private $customer;
// ...
}
This annotation represents such sql:
JOIN Customer c ON c.id = cart.customer_id
And the issue is that I need to add additional comparison there, like:
JOIN Customer c ON c.id = cart.customer_id AND c.anotherField = <constant>
Any solutions for that?
UPD:
the real additional condition I need for now is <const> BETWEEN c.f1 AND c.f2
There doesn't seem to be any solution to your problem that Doctrine could do auto-magically.
Since #ficuscr already gave you a solution for queries, there's only one more thing to handle - check your additional criteria and return the Customer instance in your getter on success and NULL on failure to meet additional criteria.
class Cart
{
const VALUE = '<some_constant_value>';
/**
* #OneToOne(targetEntity="Customer", inversedBy="cart")
* #JoinColumn(name="customer_id", referencedColumnName="id")
*/
private $customer;
// ...
/**
* #return Customer|null
*/
public function getCustomer()
{
if ($this->customer->getField1() <= self::VALUE
&& $this->customer->getField2() >= self::VALUE
) {
return $this->customer;
}
return null;
}
}
If this was a One-To-Many relation, Collection Filtering API (a.k.a. Criteria) could be used to filter the collection created by the mappings:
use Doctrine\Common\Collections\Criteria;
class Cart
{
const VALUE = '<some_constant_value>';
/**
* #OneToMany(targetEntity="Customer", mappedBy="cart")
*/
private $customers;
// ...
/**
* #return Customer[]|ArrayCollection
*/
public function getCustomers()
{
$expr = Criteria::expr();
$criteria = Criteria::create()
->where($expr->andX(
$expr->lte('field1', self::VALUE),
$expr->gte('field2', self::VALUE)
));
return $this->patientProblems->matching($criteria);
}
}
you can use the WITH keyword to specify additional join conditions, as you can see in some of the examples.
i think this should get you going:
SELECT l, c FROM location
INNER JOIN Customer c
WITH CURRENT_TIMESTAMP() BETWEEN c.f1 AND c.f2
WHERE CURRENT_TIMESTAMP() BETWEEN l.f1 AND l.f2
i removed the ON clause because i think there's no need to explicitly specify the join's ON fields unless they are not the "standard" ones (id of each entity)
also notice the call to CURRENT_TIMESTAMP() which translates into MySQL's NOW(). check out a list of other pretty useful aggregate functions and expresions here