Database Performance issues with Sonata Admin entity listing - symfony

I am using Symfony2 and Sonata Admin Bundle but I am encountering performance issues with entity listing.
I Have this Entity with Relations to other Entities:
class Collection
{
/**
* Primary Key - autoincrement value
*
* #var integer $id
*
* #ORM\Column(name="id", type="integer", nullable=false)
* #ORM\Id
* #ORM\GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* OWNING SIDE
*
* #ORM\ManyToOne(targetEntity="\App\Entity\Order")
* #ORM\JoinColumn(name="orderId", referencedColumnName="id")
* #var \App\Entity\Order
*/
protected $order;
/* ... */
}
On my AdminClass i'd like to show for each collection some order details and other related entity information.
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('id')
->add(
'order.number',
null,
array(
'template' => 'App:Admin:Field/order-data.html.twig',
'label' => 'Order Number'
)
);
}
But these creates fore each referenced entity one extra query. How can I resolve this?
My first Idea was to extend the createQuery Method:
public function createQuery($context = 'list')
{
/** #var \Doctrine\ORM\QueryBuilder|QueryBuilder $query */
$query = parent::createQuery($context);
$query
->addSelect($query->getRootAliases()[0])
->addSelect('collectionOrder')
;
$query
->leftJoin($query->getRootAliases()[0] . '.order', 'collectionOrder')
;
return $query;
}
But this takes no effect.
So how can i manage to get all needed data with one query to reduce database load time?

Add the select to the table you joined with left join.
Example:
$query
->leftJoin($query->getRootAliases()[0] . '.order', 'collectionOrder')
->addSelect('collectionOrder')
;

Related

Symfony-Doctrine : join a table with a view after persisting entity

In my database, I have a table T and a view V.
The view has some columns of my table and other data (from other tables).
In Symfony, I declared my view as a read-only Entity.
/**
* #ORM\Table(name="V")
* #ORM\Entity(readOnly=true, repositoryClass="AppBundle\Entity\Repository\VRepository")
*/
class V
{
In my T entity, I did a Join :
/**
* #ORM\OneToOne(targetEntity="V")
* #ORM\JoinColumn(name="T_id", referencedColumnName="V_id")
*/
private $view;
And I added just the getter :
/**
* Get view
*
* #return \AppBundle\Entity\V
*/
public function getView()
{
return $this->view;
}
Everything is working well when I want to read and show data.
But I have a problem after persisting a new T entity.
Symfony seems to lost posted data of my form when I create a new T entity (editAction() works perfectly).
An exception occurred while executing 'INSERT INTO T (T_id, T_name, updated_at) VALUES (?, ?, ?)' with params [null, null, "2017-09-01 15:30:41"]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Field 'T_id' cannot be empty (null)
When I remove ORM annotations of the $view property, it creates correctly my new record T in the database.
I think the problem is due to the fact that the V entity (the record in my SQL view) will exist just after the creation of T. And when I persist/flush data in Symfony, V doesn't exist yet. They are "created" at the same time.
I tried to add Doctrine #HasLifecycleCallbacks on my T entity and the #PostPersist event on the getView() method but it doesn't change anything...
Any idea to differ the Join after the creation of the entity ?
I know it's not conventional to use views as entities with Symfony but I haven't other choice.
I've just checked, it works fine with Bidirectional One-To-One relation
In my case tables are defined like:
create table T (`id` int(11) NOT NULL AUTO_INCREMENT, name varchar(100), primary key (id));
create view V as select id as entity, name, '123' as number from T;
Annotations in T:
/**
* #ORM\Table(name="T")
* #ORM\Entity()
*/
class T
{
/**
* #var int
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, nullable=true)
*/
private $name;
/**
* #var V
*
* #ORM\OneToOne(targetEntity="V", mappedBy="entity")
*/
private $view;
Annotations in V:
/**
* #ORM\Table(name="V")
* #ORM\Entity(readOnly=true)
*/
class V
{
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=255, nullable=true)
*/
private $name;
/**
* #var string
*
* #ORM\Column(name="number", type="string", length=255, nullable=true)
*/
private $number;
/**
* #var T
*
* #ORM\Id
* #ORM\OneToOne(targetEntity="T", inversedBy="view")
* #ORM\JoinColumn(name="entity", referencedColumnName="id")
*/
private $entity;
And a test snippet to prove that it saves, updates and reads fine:
public function testCRUD()
{
/** #var EntityManager $manager */
$manager = $this->client->getContainer()->get('doctrine.orm.default_entity_manager');
$t = new T();
$t->setName('Me');
$manager->persist($t);
$manager->flush();
$t->setName('He');
$manager->flush();
$manager->clear();
/** #var T $t */
$t = $manager->find(T::class, $t->getId());
$this->assertEquals('He', $t->getView()->getName());
}
Based on the #Maksym Moskvychev answer : Prefer a bidirectional One-to-One relation.
T Entity :
/**
* #ORM\OneToOne(targetEntity="V", mappedBy="entity")
*/
private $view;
V Entity :
/**
* #ORM\OneToOne(targetEntity="T", inversedBy="view")
* #ORM\JoinColumn(name="V_id", referencedColumnName="T_id")
*/
private $entity;
Fix the loss of data after posting the addAction() form (new T instance).
In the form where I list all T records :
$builder->add('entity', EntityType::class, array(
'class' => 'AppBundle:T',
'choice_label' => 'id',
'query_builder' => function (EntityRepository $er) {
return $er->createQueryBuilder('t')
->orderBy('t.name', 'ASC')
->setMaxResults(25); // limit the number of results to prevent crash
}
))
Fix the too consuming resources problem (show 25 entities instead of 870+).
Ajax request :
$(".select2").select2({
ajax: {
type : "GET",
url : "{{ path('search_select') }}",
dataType : 'json',
delay : 250,
cache : true,
data : function (params) {
return {
q : params.term, // search term
page : params.page || 1
};
}
}
});
Response for Select2 :
$kwd = $request->query->get('q'); // GET parameters
$page = $request->query->get('page');
$limit = 25;
$offset = ($page - 1) * $limit;
$em = $this->getDoctrine()->getManager();
$repository = $em->getRepository('AppBundle:V');
$qb = $repository->createQueryBuilder('v');
$where = $qb->expr()->orX(
$qb->expr()->like('v.name', ':kwd'),
$qb->expr()->like('v.code', ':kwd')
);
$qb->where($where);
// get the DQL for counting total number of results
$dql = $qb->getDQL();
$results = $qb->orderBy('m.code', 'ASC')
->setFirstResult($offset)
->setMaxResults($limit)
->setParameter('kwd', '%'.$kwd.'%')
->getQuery()->getResult();
// count total number of results
$qc = $em->createQuery($dql)->setParameter('kwd', '%'.$kwd.'%');
$count = count($qc->getResult());
// determine if they are more results or not
$endCount = $offset + $limit;
$morePages = $count > $endCount;
$items = array();
foreach ($results as $r) {
$items[] = array(
'id' => $r->getCode(),
'text' => $r->getName()
);
}
$response = (object) array(
"results" => $items,
"pagination" => array(
"more" => $morePages
)
);
if (!empty($results))
return new Response(json_encode($response));

List entities belonging to an association in Doctrine

Let's say I have a "Person" entity. A person can belong to a "Group". They are associated through a ManyToMany, Join Table strategy.
The general code looks like this:
/**
* Vendor\AcmeBundle\Entity\Person
*
* #ORM\Entity(repositoryClass="Vendor\AcmeBundle\Entity\PersonRepository")
*/
class Person extends BaseUser
{
/**
* #ORM\Column(type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\ManyToMany(targetEntity="Vendor\AcmeBundle\Entity\Group")
*/
protected $groups;
}
and the group entity
/**
* #ORM\Entity
*/
class Group extends BaseGroup
{
/**
* #ORM\Id
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* #ORM\Column(type="string", nullable=true)
*/
protected $publicName;
}
What do I want to achieve?
Given a group, list users belonging to that group in a consistent way, including pagination options (aka limit and offset)
Something like this:
function getUserFromGroup(Group $group, $criteria, $limit, $offset){};
Considerations:
The entities are mutable, they can be adapted to achieve this requisite (e.g. the association could be changed from unidirectional to bidirectional)
The amount of person entities is in the thousands (2000~8000)
The amount of groups is less than 10
This is explained in the Symfony2 Book, chapter on Doctrine
For your case, I would suggest using the findBy() method.
From the official doctrine documentation:
function getUserFromGroup($group, $criteria, $limit, $offset){
// You should probably build the criteria into a paramaters array,
// but I'll just asume it's "fieldName" => "valueToFilterBy"
$criteria['groups'] = $group;
$users = $em->getRepository('AppBundle\Entity\User')
->findBy(
$criteria, // Filter by columns
array('name' => 'ASC'), // Sorting
$limit, // How many entries to select
$offset // Offset
);
return $users;
};
I would not use the associations to list group's members, but a custom repository call. This should be close enough:
class PersonRepository extends EntityRepository
{
public function findPeopleInGroup(Group $group, $criteria, $limit, $offset){
$qb = $this->createQueryBuilder('p');
$qb->join('p.groups', 'g')
->where(':group MEMBER OF p.groups')
->setParameter('group', $group)
->orderBy('p.'.$criteria);
$qb->setFirstResult($offset);
$qb->setMaxResults($limit);
return $qb->getQuery()->getResult();
}
}

Symfony, Sonata Admin datagrid OR filtering

I'm using Symfony 2 with Sonata Admin Bundle.
As far as i can see, Sonata Admin has only AND filtering on list action.
Use this as example:
My entity : Prodotto
/**
* Prodotto
*
* #ORM\Table()
* #ORM\Entity
* #UniqueEntity("prodotto")
*/
class Prodotto extends DefaultEntity
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
//getters - setters Zone
/**
* #param int $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return int
*/
public function getId()
{
return $this->id;
}
/**
* #var string
*
* #ORM\Column(name="prodotto", type="string", length=255)
*/
private $prodotto;
/**
* #var \mybundle\Entity\Tipologia
* #ORM\ManyToOne(targetEntity="Tipologia")
*/
private $tipologia;
/**
* #var \mybundle\Entity\Brand
* #ORM\JoinColumn(name="brand_id", referencedColumnName="id")
* #ORM\ManyToOne(targetEntity="Brand")
*
*/
private $brand;
/**
* #var \mybundle\Entity\Layout
* #ORM\ManyToOne(targetEntity="Layout")
* #ORM\JoinColumn(name="layout_id", referencedColumnName="id")
*/
private $layout;
/**
* #var \mybundle\Entity\Carta
* #ORM\ManyToOne(targetEntity="Carta")
* #ORM\JoinColumn(name="carta_id", referencedColumnName="id")
*/
private $carta;
//[... many other fields omitted...]
//[... Getters and setters omitted (default get/set, no custom code) ...]
My admin class: ProdottoAdmin (please note that i copied only the configuration useful for this question, the class contains all the required configurations for all the actions)
/**
* #param DatagridMapper $datagridMapper
*/
protected function configureDatagridFilters(DatagridMapper $datagridMapper)
{
$datagridMapper
->add('id')
->add('prodotto')
->add('tipologia')
->add('brand')
->add('layout')
->add('misura')
;
}
/**
* #param ListMapper $listMapper
*/
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->add('dataModifica','datetime',array('pattern' => 'dd/MM/yyyy','label'=>'Data'))
->add('tipologia')
->add('brand')
->add('layout')
->add('misura')
->add('Nome Prodotto', 'string', array('template' => 'mybundle:Admin:List/link_column_list_prodotto.html.twig'))
->add('_action', 'actions', array(
'actions' => array(
'show' => array(),
'edit' => array(),
'delete' => array(),
)
))
;
}
My Service Configuration (in services.yml):
services:
mybundle.admin.prodotto:
class: mybundle\Admin\ProdottoAdmin
arguments: [~, mybundle\Entity\Prodotto, mybundle:ProdottoAdmin]
tags:
- {name: sonata.admin, manager_type: orm, group: Prodotti, label: Prodotti}
With this configuration, i actually got a fully functional data grid filter, as you can see from the picture(image added for better understanding of the problem):
But, the default Sonata Admin filtering expose only an AND filter, i can add ONE time all the property of the entity and make an AND filtering with them.
But, now i need to extend that functionality.
I need to add an OR filter, app-wide. I want that the user can filter like "give me all the product that are related with Brand1 OR Brand2 AND are of Tipologia1."
I know i can make precise custom query, like the example above, to obtain a single result, but:
That's will not be app-wide, i have many entities and i can't write all the custom query needed
That's verbose, i will have to write much of the same code in all the data grid filters
That's not wise, because if tomorrow i change an entity, the data grid filter is coupled with the entity and i need to remember to add/modify the query.
So, finally, my question is:
There is a "correct" (or at least, a raccomended) way or pattern / maybe a configurable bundle to implement that OR filtering?

symfony2 twig template load checkboxes from many to many entity relationship

New Symfony2 User here. I have 2 entities that are related, one to many that is unidirectional. I'm doing it as ManyToMany as the doctrine documentation suggests, Article(one) and Tags(many). I'd like to have checkboxes show up that show the tag names on the article.new page and the article.edit page. On form submission the id of the tag entity is stored in the article_tags side table that the entity generator created for me.
Posting only relevant code.
Tag Entity AppBundle/Entity/Tag.php
/**
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=20)
*/
public $name;
Article Entity AppBundle/Entity/Article.php
/**
* #ORM\ManyToMany(targetEntity="Tag")
* #ORM\JoinTable(
* name="article_tags",
* joinColumns={#ORM\JoinColumn(name="article_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="tag_id", referencedColumnName="id", unique=true)}
* )
*/
protected $tags;
/**
* Add tag
*
* #param \AppBundle\Entity\Tag $tag
*
* #return Article
*/
public function addTag(\AppBundle\Entity\Tag $tag)
{
$this->tags[] = $tag;
return $this;
}
/**
* Remove tag
*
* #param \AppBundle\Entity\Tag $tag
*/
public function removeTag(\AppBundle\Entity\Tag $tag)
{
$this->tags->removeElement($tag);
}
/**
* Get tags
*
* #return \Doctrine\Common\Collections\Collection
*/
public function getTags()
{
return $this->tags;
}
Article Form Type AppBundle/Form/ArticleType
$builder->add('title')
->add('body')
->add('author')
->add('tags', 'entity', array(
'class' => 'AppBundle\Entity\Tag',
'property' => 'name',
'expanded' => 'true', ));
ArticleController AppBundle/Controller/ArticleController.php
* #Template()
*/
public function newAction()
{
$entity = new Article();
$tags = new Tag();
$entity->addTag($tags);
$form = $this->createCreateForm($entity);
return array('entity' => $entity,'form' => $form->createView(), );
}
As of now the error I receive is...
Entities passed to the choice field must be managed. Maybe persist
them in the entity manager?
I'm not entirely sure I'm on the right track. I just want to attach tags to articles!
Thanks
In the controller, you create a blank Tag and add it to the new Article before creating the form. That doesn't make sense to me, and I suspect that's where the error is coming from.
If there are any tags in the database, Symfony will automatically get them and display them with a checkbox in the form. If the user checks a checkbox, this Tag will be added to the Article.
Just delete these two lines and you should be fine:
$tags = new Tag();
$entity->addTag($tags);

Symfony2 entity field type query_builder innerJoin

I have 3 Entities (Group, GroupCategory and GroupLanguage)
Group Table
id (pk)
id_group_category (fk)
GroupCategory Table
id (pk)
GroupCategoryLanguage Table
id (pk)
id_language (fk)
id_group_category (fk)
I have created a GroupType which takes in GroupCategory as a subform.
$builder->add('id_group_category', 'entity', array(
'class' => 'BazaarBundle:GroupCategory',
'property' => 'id',
'query_builder' => function(EntityRepository $a) {
return $a->createQueryBuilder('a')
->innerJoin('BazaarBundle:GroupCategoryLanguage', 'b')
->where('b.id_group_category = a.id')
->orderBy('a.id', 'ASC');
}
)
)
->add('Add', 'submit');
I'm trying to innerJoin the language table so that the dropdownlist would be populated with text and not the ids of the category.
I'm quite new to Symfony2 and have already looked up to their documentation and sorry to say it was quite puzzling for me. Am I doing it right because i'm having some errors with the code.
The error message:
[Semantical Error] line 0, col 111 near 'id_group_category': Error: Class Karl\BazaarBundle\Entity\GroupCategoryLanguage has no field or association named id_group_category
GroupCategory.php
class GroupCategory
{
public function __construct()
{
$this->groupCategoryLanguage = new ArrayCollection();
}
public function __toString(){
return $this->groupCategoryLanguage->getName();
}
/**
* #ORM\OneToMany(targetEntity="GroupCategoryLanguage", mappedBy="idGroupCategory")
* #ORM\JoinColumn(nullable=false,referencedColumnName="id_group_category")
*/
protected $groupCategoryLanguage;
}
GroupCategoryLanguage.php
class GroupCategoryLanguage
{
/**
* #var integer
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var integer
*
* #ORM\Column(name="id_language", type="integer")
*/
private $idLanguage;
/**
* #var integer
*
* #ORM\JoinColumn(name="id_group_category", nullable=false)
* #ORM\ManyToOne(targetEntity="Karl\BazaarBundle\Entity\GroupCategory", inversedBy="groupCategoryLanguage")
*/
private $idGroupCategory;
/**
* #var string
*
* #ORM\Column(name="name", type="string", length=32)
*/
private $name;
}
i think you need to add GroupCategoryLanguage data into your query by adding:
->addSelect('b')
to your query builer object.
Exemple below.
Please note i have deleted the where condition because it seems to be a join condition, adn this is not needed bacause Doctrine is suposed to know all about relations. If i'm wrong, don't delete it...
$builder->add('id_group_category', 'entity', array(
'class' => 'BazaarBundle:GroupCategory',
'property' => 'id',
'query_builder' => function(EntityRepository $a) {
return $a->createQueryBuilder('a')
->innerJoin('a.languages', 'b')
->addSelect('b')
->orderBy('a.id', 'ASC');
}
))
->add('Add', 'submit');
EDIT:
Regarding our discussions, i update my answer:.
Let's start from the beginning:
You have a relation between GroupCategory and GroupCategoryLanguage and the GroupCategoryLanguage is the owner of this relation (it have to FK).
Here you want to get languages from the GroupCategory so it's $owner->getSlave() and you need a bidirectionnal relation.
For that you need to add a field into the slave entity:
So in GroupCategory entity:
/**
* #ORM\OneToMany(targetEntity="Karl\BazaarBundle\Entity\GroupCategoryLanguage",referencedColumnName="id_group_category", mappedBy="category")
* #ORM\JoinColumn(nullable=false)
*/
private $languages;
And i assume that in GroupCategoryLanguages you have:
/**
* #ORM\ManyToOne(targetEntity="Karl\BazaarBundle\Entity\GroupCategory", inversedBy="languages")
* #ORM\JoinColumn(name="id_group_category", nullable=false)
*/
private $category;
I think one of your problems is that you think in terms of tables, am i wrong ?
You really need to think in term of objects (entities) and let Doctrine manage the boring things :)
Display language in place of id
You can totally delete the 'property' option and add a __toString method into your GroupCategory entity, which one will be called and the returned value will appear in your form.
I think we are good :)
Cheers

Resources