I have a family tree like that:
class Family
{
/**
* #var integer
*
* #ORM\Column(type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var Family
*
* #ORM\ManyToOne(targetEntity="Family", inversedBy="children")
*/
private $parent;
/**
* #var string
*
* #ORM\Column(name="name", type="string")
*/
private $name;
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Family", mappedBy="parent")
*/
private $children;
// [...]
}
I'm trying to findAll() and get the parent and children attached
$familyRepo = $this->em->getRepository(Family::class);
$families = $familyRepo->findAll();
foreach ($families as $family) {
dump($family->getParent()->getName());
}
I can see the parents name dumped and only one query executed, so they are well attached.
However if I try to show the children:
dump($family->getChildren()->count());
I'm seeing as much queries as there are families.
How can I get the children attached as the parents are ? (without more queries)
What am I forgetting ?
On the one-to-many relation for $children you can specify to fetch objects eagerly as follows:
/**
* #var ArrayCollection
*
* #ORM\OneToMany(targetEntity="Family", mappedBy="parent", fetch="EAGER")
*/
private $children;
See also the docs for other params.
Following #dlondero's suggestion, I forced the deep fetch into the repository.
Here is how I did:
public function getRootNodes($eagerLevels = 5)
{
$qb = $this->createQueryBuilder('entity0')
->select('partial entity0.{id, name, parent}')
->where('entity0.parent IS NULL')
;
for ($i = 0; $i < $eagerLevels; $i++) {
$qb
->leftJoin('entity'.$i.'.children', 'entity'.($i+1))
->addSelect('partial entity'.($i+1).'.{id, name, parent}')
;
}
return $qb->getQuery()->getResult();
}
This partially fetches just what I need so no lazy loading happens.
I also made the level of deepness configurable.
Related
I'm building a query that would be for creating a list of Posts that have a Project that is associated to the user, and within that structure hit the right criteria for "tierAccess."
My query builder:
$qb = $this->em->createQueryBuilder();
foreach($subs as $sub)
{
if($sub->getDisabled() == true)
{
continue;
}
$qb->select('p')
->from('App\Entity\ProjectPost', 'p')
->where('project = '.$sub->getProject()->getId())
->andwhere('p.Published = true')
->andwhere('p.TierAccess = '.$sub->getProjectTier()->getId())
->orderBy('p.PostTime', 'DESC');
$query = $qb->getQuery();
$object[] = $query->execute();
}
What I am aiming to do is add posts that the user subscription will allow for, and within that subscription making sure their access to this post is allowed (ie: tierAccess).
I then return the object variable to pass along to my Twig template file.
The error I'm receiving is:
[Semantical Error] line 0, col 45 near 'project = 3 AND': Error: 'project' is not defined.
My ProjectPost entity:
class ProjectPost
{
/**
* #ORM\Id
* #ORM\GeneratedValue
* #ORM\Column(type="integer")
*/
private $id;
/**
* #ORM\Column(type="string", length=255)
*/
private $PostTitle;
/**
* #ORM\Column(type="text", nullable=true)
*/
private $PostHero;
/**
* #ORM\Column(type="string", length=255, nullable=false)
*/
private $PostType;
/**
* #ORM\Column(type="text")
*/
private $PostBody;
/**
* #ORM\ManyToOne(targetEntity=Project::class, inversedBy="projectPosts")
* #ORM\JoinColumn(nullable=false)
*/
private $Project;
/**
* #ORM\Column(type="array", nullable=true)
*/
private $TierAccess = [];
/**
* #ORM\Column(type="datetimetz", nullable=true)
*/
private $PostTime;
/**
* #ORM\ManyToOne(targetEntity=User::class, inversedBy="projectPosts")
* #ORM\JoinColumn(nullable=true)
*/
private $PostBy;
/**
* #ORM\Column(type="array", nullable=true)
*/
private $PostCategories = [];
/**
* #ORM\Column(type="boolean")
*/
private $Published;
/**
* #ORM\Column(type="string", length=255, nullable=true)
*/
private $PostCover;
/**
* #ORM\Column(type="boolean")
*/
private $PostSupporter = 0;
}
The basic mistake is this one:
->where('p.Project = '.$sub->getProject()->getId())
Notice that you declare p to be the alias of Post, and then you don't use it. And even if you define the property as Project, you were trying to use it as project.
Nevertheless, the whole thing is rather suspect. Executing a query within a loop usually points to something wrong with the design.
A simpler approach, using WHERE IN instead of a loop and multiple selects:
// get the "subs" ids in an array:
$subsIds = array_map(fn($s) => $s->getProject()->getId(), $subs);
qb->select('p')
->from('App\Entity\ProjectPost', 'p')
->where('p.Project IN :subsIds')
->andwhere('p.Published = true')
->andwhere('p.TierAccess = '.$sub->getProjectTier()->getId())
->orderBy('p.PostTime', 'DESC')
->setParameter('subsIds', $subsIds)
;
$result = $qb->getQuery()->getResult;
I have to internationalize an app and particularly an entity called Program. To do so, I created an other entity ProgramIntl which contains a "locale" attribute (en_GB, fr_FR, etc) and strings which must be internationalized. I want the programIntl attribute in Program to be an associative array (with locale as key).
We have an API to read/write programs. GET and POST works fine but when I want to update data (PUT), the programIntl is not updated: an insert query is launched (and fails because of the unique constraint, but that's not the question).
Here is the code:
In Program.php:
/**
* #var
*
* #ORM\OneToMany(targetEntity="ProgramIntl", mappedBy="program", cascade={"persist", "remove", "merge"}, indexBy="locale", fetch="EAGER")
* #ORM\JoinColumn(nullable=false, onDelete="cascade")
* #Groups({"program_read", "program_write"})
*/
private $programIntl;
public function addProgramIntl($programIntl)
{
$this->programIntl[$programIntl->getLocale()] = $programIntl;
$programIntl->setProgram($this);
return $this;
}
public function setProgramIntl($programIntls)
{
$this->programIntl->clear();
foreach ($programIntls as $locale => $programIntl) {
$programIntl->setLocale($locale);
$this->addProgramIntl($programIntl);
}
}
public function getProgramIntl()
{
return $this->programIntl;
}
In ProgramIntl.php:
/**
* #ORM\Entity(repositoryClass="App\Repository\ProgramIntlRepository")
* #ORM\Table(name="program_intl",uniqueConstraints={#ORM\UniqueConstraint(name="program_intl_unique", columns={"program_id", "locale"})})
*/
class ProgramIntl
{
/**
* #ORM\Id()
* #ORM\GeneratedValue()
* #ORM\Column(type="integer")
* #Groups({"program_read", "program_write"})
*/
private $id;
/**
* #ORM\ManyToOne(targetEntity="App\Entity\Program", inversedBy="programIntl")
* #ORM\JoinColumn(nullable=false)
*/
private $program;
/**
* #ORM\Column(type="string", length=5, options={"fixed" = true})
*/
private $locale;
/**
* #ORM\Column(type="string", length=64)
* #Assert\NotBlank()
* #Groups({"program_read", "program_write"})
*/
private $some_attr;
/* ... */
}
Any idea of what could be the reason of the "insert" instead of "update" ?
Thanks
I forgot to mention that we use api-platform.
But I found the solution myself. In case anyone is interested, adding the following annotation to classes Program and ProgramIntl solved the problem:
/* #ApiResource(attributes={
* "normalization_context"={"groups"={"program_read", "program_write"}},
* "denormalization_context"={"groups"={"program_read", "program_write"}}
* }) */
why Symfony2 performs 40 DB queries if I use following code:
$em = $this->getDoctrine()->getManager();
$records = $em->getRepository('MyWebBundle:Highlight')->findAll();
I thought that findAll() method returns only all items from Highlight entity and associations to other entities replaces Proxy objects. But now findAll() method gettings all associations entities.
Do you know where is the problem ?
indexAction
public function indexAction() {
$em = $this->getDoctrine()->getManager();
$records = $em->getRepository('MyWebBundle:Highlight')->findAll();
$csrf = $this->get('security.csrf.token_manager');
$token = $csrf->refreshToken(self::FORM_TOKEN_ID);
$params = array(
"data" => array(
"all" => $records,
),
"token" => $token->getValue(),
"static" => array(
"add" => $this->generateUrl("admin_highlight_add"),
"edit" => $this->generateUrl("admin_highlight_edit"),
"del" => $this->generateUrl("admin_highlight_del"),
),
);
$ser = $this->get('jms_serializer');
$jsonContent = $ser->serialize($params, 'json');
return array('jsonContent' => $jsonContent);
}
Highlight entity
namespace My\WebBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use JMS\Serializer\Annotation as JMS;
/**
* Highlight
*
* #JMS\ExclusionPolicy("none")
* #ORM\Table()
* #ORM\Entity(repositoryClass="My\WebBundle\Entity\HighlightRepository")
*/
class Highlight {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="abbreviation", type="string", length=8, unique=true)
*/
private $abbreviation;
/**
* #var string
*
* #ORM\Column(name="description", type="string", length=80, nullable=true)
*/
private $description;
/**
* #var string
*
* #ORM\Column(name="color", type="string", length=7)
*/
private $color;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Goods", mappedBy="highlight")
*/
private $goods;
/**
* #var ArrayCollection
* #ORM\OneToMany(targetEntity="Calibration", mappedBy="highlight")
*/
private $calibrations;
/**
* Constructor
*/
public function __construct() {
$this->goods = new \Doctrine\Common\Collections\ArrayCollection();
$this->calibrations = new \Doctrine\Common\Collections\ArrayCollection();
}
/**
* Get id
*
* #return integer
*/
public function getId() {
return $this->id;
}
/**
* Set abbreviation
*
* #param string $abbreviation
* #return Highlight
*/
public function setAbbreviation($abbreviation) {
$this->abbreviation = $abbreviation;
return $this;
}
/**
* Get abbreviation
*
* #return string
*/
public function getAbbreviation() {
return $this->abbreviation;
}
/**
* Set description
*
* #param string $description
* #return Highlight
*/
public function setDescription($description) {
$this->description = $description;
return $this;
}
/**
* Get description
*
* #return string
*/
public function getDescription() {
return $this->description;
}
/**
* Set color
*
* #param string $color
* #return Highlight
*/
public function setColor($color) {
$this->color = $color;
return $this;
}
/**
* Get color
*
* #return string
*/
public function getColor() {
return $this->color;
}
/**
* Add goods
*
* #param \My\WebBundle\Entity\Goods $goods
* #return Highlight
*/
public function addGood(\My\WebBundle\Entity\Goods $goods) {
$this->goods[] = $goods;
return $this;
}
/**
* Remove goods
*
* #param \My\WebBundle\Entity\Goods $goods
*/
public function removeGood(\My\WebBundle\Entity\Goods $goods) {
$this->goods->removeElement($goods);
}
/**
* Get goods
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getGoods() {
return $this->goods;
}
/**
* Add calibrations
*
* #param \My\WebBundle\Entity\Calibration $calibrations
* #return Highlight
*/
public function addCalibration(\My\WebBundle\Entity\Calibration $calibrations) {
$this->calibrations[] = $calibrations;
return $this;
}
/**
* Remove calibrations
*
* #param \My\WebBundle\Entity\Calibration $calibrations
*/
public function removeCalibration(\My\WebBundle\Entity\Calibration $calibrations) {
$this->calibrations->removeElement($calibrations);
}
/**
* Get calibrations
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getCalibrations() {
return $this->calibrations;
}
}
Highlight repository is empty
I think the problem comes from the serializer. Since you serializer highliths, each of them has their properties serialized as well which means that lazy query will be performed to retieved Goods which will be also serialized.
You should then prevent this behaviour by adding annotations to highlight's goods property as this
use ...
use JMS\SerializerBundle\Annotation\ExclusionPolicy;
use JMS\SerializerBundle\Annotation\Exclude;
/**
* ...
* #ExclusionPolicy("none")
*/
class Highlight
{
/**
* ...
* #Exclude
*/
private $goods;
}
You can have further details about exclusion stratigies from JMSSerializer doc
findAll itself does not perform many queries. Queries are executed when you access related entity via getters. As relation are not fetched eagerly, they first time are fetched when you are acessing them.
I think serializer access all children to send your object.
See Doctrine documentation
Whenever you have a managed entity instance at hand, you can traverse
and use any associations of that entity that are configured LAZY as if
they were in-memory already. Doctrine will automatically load the
associated objects on demand through the concept of lazy-loading.
To prevent this either disable children serialization or use fetch EAGER or build a DQL query, which prefetch all the children alongside with parents, like (just sample, not valid DQL)
SELECT Highlight, Good, Calibration
FROM Highlights Highlight
LEFT JOIN Highlight.googs Good
LEFT JOIN Goog.calibrations Calibration
WHERE ...
Here's what I'm having trouble with.
I've a Table which contains a column called shown_on_homepage and only one row should be set to 1, the rest should all be set to 0. I'm trying to add a new row to the database and this one should be set to 1, setting the one that previously had a 1 to 0.
In MySQL I know this can be achieved by issuing an update before the insert:
UPDATE table_name SET shown_on_homepage = 0
Here's my Entity:
class FeaturedPerson {
/**
* #var integer
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="content", type="string", length=2500, nullable=false)
*/
private $content;
/**
* #var \DateTime
*
* #ORM\Column(name="date_updated", type="datetime")
*/
private $dateUpdated;
/**
* #var bool
*
* #ORM\Column(name="shown_on_homepage", type="boolean", nullable=false)
*/
private $isShownOnHomepage;
//...
public function getIsShownOnHomepage() {
return $this->isShownOnHomepage;
}
public function setIsShownOnHomepage($isShownOnHomepage) {
$this->isShownOnHomepage = $isShownOnHomepage;
return $this;
}
}
And for the Controller I've:
$featured = new FeaturedPerson();
$featured->setContent('Test content.');
$featured->setDateUpdated('01/02/2013.');
$featured->setIsShownOnHomepage(TRUE);
$em = $this->getDoctrine()->getManager();
$em->persist($featured);
$em->flush();
It does add the new row, but the one that had a shown_on_homepage set to 1 still has it. I've researched but I couldn't find a way to achieve this, I hope you can help me.
You could execute a query prior to your existing code in your controller:
$queryBuilder = $this->getDoctrine()->getRepository('YourBundleName:FeaturedPerson')->createQueryBuilder('qb');
$result = $queryBuilder->update('YourBundleName:FeaturedPerson', 'd')
->set('d.isShownOnHomepage', $queryBuilder->expr()->literal(0))
->where('d.isShownOnHomepage = :shown')
->setParameter('shown', 1)
->getQuery()
->execute();
Change 'YourBundleName' to your bundle name.
I have a Product class that has many fields on it for ManyToMany, such as ingredients, sizes, species, etc.. A total of about 14 different fields
Not all of the fields are are relevant to each product.
I have mapping set up like this
Class product {
/**
* #var Species[]
* #ORM\ManyToMany(targetEntity="Species")
* #ORM\JoinTable(name="product_species",
* joinColumns={#ORM\JoinColumn(name="productId", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="speciesId", referencedColumnName="id")}
* )
* #ORM\OrderBy({"name" = "asc"})
*/
private $species;
This works great for a manytomany/manyto one.
The problem is in my product_ingredients table I needed to add an additional field, meaning need to switch from ManyToMany to a OneToMany/ManyToOne
So like this
/**
* #var ProductIngredient[]
*
* #ORM\OneToMany(targetEntity="ProductIngredient", mappedBy="product")
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
*/
private $ingredients;
Now my ProductIngredient Entity Looks like this
/**
* #var IngredientType
* #ORM\ManyToOne(targetEntity="IngredientType", fetch="EAGER")
* #ORM\JoinColumn(name="ingredientTypeId", referencedColumnName="id")
*/
private $ingredientType;
/**
* #var Ingredient
*
* #ORM\ManyToOne(targetEntity="Ingredient", inversedBy="products", fetch="EAGER")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="ingredientId", referencedColumnName="id")
* })
*/
private $ingredient;
/**
* #var Product
*
* #ORM\ManyToOne(targetEntity="Product", inversedBy="ingredients")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="productId", referencedColumnName="id")
* })
*/
private $product;
So in my product class for species I use the #ORM\OrderBy so that species is already ordered.. Is there a way I can somehow also do this for my ingredients field?
Or am I doing my logic wrong and these shouldn't even be fields on the product class and should just be looking up by the repository instead?
I was wanting it to be easy so I could loop through my objects like $product->getIngredients()
instead of doing
$ingredients = $this->getDoctrine()->getRepository('ProductIngredient')->findByProduct($product->getId());
in the Product entity just also aadd the orderBy to the ingredients relation
/**
* ...
* #ORM\OrderBy({"some_attribute" = "ASC", "another_attribute" = "DESC"})
*/
private $ingredients;
Well I came up with a hackish way.. Since I really only care about the sort on output, I have made a basic twig extension
use Doctrine\Common\Collections\Collection;
public function sort(Collection $objects, $name, $property = null)
{
$values = $objects->getValues();
usort($values, function ($a, $b) use ($name, $property) {
$name = 'get' . $name;
if ($property) {
$property = 'get' . $property;
return strcasecmp($a->$name()->$property(), $b->$name()->$property());
} else {
return strcasecmp($a->$name(), $b->$name());
}
});
return $values;
}
I would like to avoid this hack though and still would like to know a real solution
You should use 'query_builder' option in your Form: http://symfony.com/doc/master/reference/forms/types/entity.html#query-builder
The value of the option can be something like this:
function(EntityRepository $er) {
return $er->createQueryBuilder('i')->orderBy('i.name');
}
Don't forget to add the "use" statement for EntityRepository
If you use xml mapping, you could use
<order-by>
<order-by-field name="some_field" direction="ASC" />
</order-by>
inside your <one-to-many> tag.