Doctrine 2 lazy loading won't work - doctrine-orm

I'm starting with Doctrine 2 and I'm trying to make this simple case working: a Series has a title, which is a MultilingualText (encapsulates the English and French titles).
I'm loading a Series, but when I try to get its French title, the lazy loading doesn't work.
Here is the Series code:
class Series {
/**
* #Id #Column(type="smallint", name="seriesId")
* #GeneratedValue
*/
private $seriesId;
/**
* #OneToOne(targetEntity="MultilingualText")
* #JoinColumn(name="title", referencedColumnName="multilingualTextId")
**/
private $title;
public function getTitle() {
return $this->title;
}
public function setTitle(MultilingualText $title) {
$this->title = $title;
}
}
Now the MultilingualText code:
class MultilingualText {
/**
* #Id #Column(type="smallint", name="multilingualTextId")
* #GeneratedValue
*/
private $multilingualTextId;
/** #Column(type="text") */
private $frenchText;
/** #Column(type="text") */
private $englishText;
public function getFrenchText() {
return $this->frenchText;
}
public function setFrenchText($frenchText) {
$this->frenchText = $frenchText;
}
}
Now the loading, which doesn't work:
$series = $entityManager->find('Series', 1);
echo $series->getTitle()->getFrenchText();
Surprisingly however, this does work:
$multilingualText = $entityManager->find('MultilingualText', 1);
echo $multilingualText->getFrenchText() . "<br/>";
$series = $entityManager->find('Series', 1);
echo $series->getTitle()->getFrenchText();
Result:
My French title
My French title
From this test, I conclude that lazy loading doesn't work in this context, but no idea why...
Any help would be much appreciated
Thanks
Mat

I found the thing: dev mode was set to false...
This simple line is damn important!
$isDevMode = true;

Related

Doctrine : 2 leftJoin on same table, but different Join::WITH

i got a request with 2 leftJoin on the same table, with different alias :
$qb2->leftJoin('d2.groupeDossiers', 'gd',Join::WITH, 'gd.type = \'assure\'');
$qb2->leftJoin('d2.groupeDossiers', 'gdt',Join::WITH, 'gdt.type = \'tiers\'');
But it seems there's conflicts. How could i do that leftJoin in a single request ?
Thank you !
Without using Doctrine Relationships
If your relationship is like this :
/**
* #ORM\OneToMany(targetEntity="GroupeDossier", mappedBy="test")
*/
private $groupeDossiers;
And
/**
* #ORM\ManyToOne(targetEntity="Test", inversedBy="groupeDossier")
* #ORM\JoinColumn(name="id_test", referencedColumnName="id")
*/
private $test;
The best way to return what you want is :
$qb2->select('MyBundle:Test','t');
$qb2->addSelect('gd');
$qb2->addSelect('gdt');
$qb2->leftJoin('MyBundle:GroupeDossiers', 'gd',Join::WITH, 'gd.type = \'assure\' AND gd.test = t');
$qb2->leftJoin('MyBundle:GroupeDossiers', 'gdt',Join::WITH, 'gdt.type = \'tiers\' AND gdt.test = t');
But then, you should use getArrayResult() to get gd dans gdt
With Doctrine Relationships -- Quick and dirty
In your entity, do this :
/**
* #ORM\OneToMany(targetEntity="GroupeDossier", mappedBy="test")
*/
private $groupeDossiers;
public function getGroupesDossierSeparated() {
$result = ['assure'=>[], 'tiers'=>[]];
foreach ($this->groupeDossiers as $d) {
$result[$d->getType()][] = $d;
}
return $result;
}
Then, just with $qb->leftJoin('d2.groupeDossiers') you can use getGroupesDossierSeparated() to get the data you want.

Add Method not working in a many to many relationship

I have this many to many relationship between bus and driver .
This is the bus entity :
/**
* #var ArrayCollection<Driver> The driver of this bus.
* #ORM\ManyToMany(targetEntity="Driver", inversedBy="bus" , cascade={"persist"})
* #ORM\JoinTable(name="bus_driver")
* #ORM\JoinColumn(name="driver_id", referencedColumnName="id")
* */
private $driver;
public function __construct() {
$this->driver = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addDriver($driver) {
$this->driver[] = $driver;
return $this;
}
And this is the driver entity :
/**
* #var ArrayCollection<Bus> The buses of this driver
* #ORM\ManyToMany(targetEntity="Bus", mappedBy="driver")
* #ORM\JoinTable(name="bus_driver")
* #ORM\JoinColumn(name="bus_id", referencedColumnName="id")
*/
private $bus;
public function __construct() {
$this->bus = new \Doctrine\Common\Collections\ArrayCollection();
}
public function addBus($bus) {
$this->bus[] = $bus;
$bus->addDriver($this);
return $this;
}
My problem is that when I add a bus with a driver the relation is persisted but not when I add a driver whih a bus . It works only from the bus side.
please, consider renaming $driver into $drivers, as there is multiple drivers (same for bus -> buses)
and then you should try that:
#ORM\ManyToMany(targetEntity="xxx", cascade={"persist"})
more details: http://doctrine-orm.readthedocs.io/projects/doctrine-orm/en/latest/reference/working-with-associations.html#transitive-persistence-cascade-operations
Change these (add the null and call it 'drivers'):
use Doctrine\Common\Collections\ArrayCollection;
...
private $drivers = null;
public function __construct() {
$this->drivers = new ArrayCollection();
}
public function addDriver($driver) {
$this->drivers[] = $driver;
return $this;
}
Also, to resolve the problem from the Bus Entity side, you might (but I'm not sure) need this change:
public function addDriver($driver) {
$driver->addBus($this);
$this->drivers[] = $driver;
return $this;
}
Try it, since I have a similar scenario in a ManyToOne relation, and I'm wondering if the above change might work.
The only working scenario was when I had :
A setter for the drivers collection .
An addDriver Method .
A removeDriver Method.
If I remove one of the pervious , addDriver won't even trigger at all .

Doctrine 2 Many To Many follower relationship not working

I have followed the example here doctrine 2 documentation and made the entity
<?php
namespace Account\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Zend\Filter\Null;
/**
* #ORM\Entity
* #ORM\Table(name="accounts")
*/
class Account
{
/**
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
* #ORM\Column(type="integer")
* #ORM\Column(length=11)
*/
private $id;
// ......
/**
* #ORM\ManyToMany(targetEntity="Account\Entity\Account", mappedBy="following")
*/
private $followers;
/**
* #ORM\ManyToMany(targetEntity="Account\Entity\Account", inversedBy="followers")
* #ORM\JoinTable(name="followers",
* joinColumns={#ORM\JoinColumn(name="account_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="follower_id", referencedColumnName="id")}
* )
*/
private $following;
public function __construct(){
$this->followers = new ArrayCollection();
$this->following = new ArrayCollection();
}
/**
* #param mixed $followers
*/
public function setFollowers($followers)
{
$this->followers[] = $followers;
}
/**
* #return mixed
*/
public function getFollowers()
{
return $this->followers;
}
public function addFollowers($followers){
foreach($followers as $follower)
$this->followers->add($follower);
}
public function removeFollowers($followers){
$this->followers->removeElement($followers);
}
/**
* #param mixed $following
*/
public function setFollowing($following)
{
$this->following[] = $following;
}
/**
* #return mixed
*/
public function getFollowing()
{
return $this->following;
}
public function addFollowing($followers){
foreach($followers as $follower)
$this->following->add($follower);
}
public function removeFollowing($followers){
$this->following->removeElement($followers);
}
/**
* #param mixed $id
*/
public function setId($id)
{
$this->id = $id;
}
/**
* #return mixed
*/
public function getId()
{
return $this->id;
}
}
So I have 2 accounts (ids 1 and 2) and made it so that 1 follows (is friend to) 2.
The column is something like
user_id follower_id
2 1
By using the following code, I'm not getting any results as I should
$user = $this->entityManager()->getRepository('Account/Entity/Account')->find(1);
$followers = $user->getFollowers();
var_dump($followers);
It returns something like:
object(Doctrine\ORM\PersistentCollection)#357 (9) { ["snapshot":"Doctrine\ORM\PersistentCollection":private]=> array(0) { } ["owner":"Doctrine\ORM\PersistentCollection":private]=> NULL ["association":"Doctrine\ORM\PersistentCollection":private]=> NULL ["em":"Doctrine\ORM\PersistentCollection":private]=> NULL ["backRefFieldName":"Doctrine\ORM\PersistentCollection":private]=> NULL ["typeClass":"Doctrine\ORM\PersistentCollection":private]=> NULL ["isDirty":"Doctrine\ORM\PersistentCollection":private]=> bool(false) ["initialized":"Doctrine\ORM\PersistentCollection":private]=> bool(false) ["coll":"Doctrine\ORM\PersistentCollection":private]=> object(Doctrine\Common\Collections\ArrayCollection)#358 (1) { ["_elements":"Doctrine\Common\Collections\ArrayCollection":private]=> array(0) { } } }
The same happens if I use getFollowing and all the combinations I've tried. Am I missing something? I mean it's pretty much like the documentation code, please help me out!
I'm using Zend Framework 2, if that's of any help.
All associations are LAZY by default, which means it is populated when you first access it. PersistentCollection actually is an iterator and a var_dump will not trigger iteration, that's why you see _intialized property set to false and the count of _elements is 0.
You can use getArrayCopy or simply iterate through the collection.
var_dump($followers->getArrayCopy());
or:
foreach ($followers as $follower) {
var_dump($follower);
}

How to count a group by result with JPA and CriteriaBuilder?

I think this is nearly impossible or very tricky. I'm using CriteriaBuilder, JPA 2.0, Hibernate and MariaDB and want to build the following query with CriteriaBuilder:
SELECT COUNT(*) FROM
(SELECT DISTINCT(SomeColumn) // I think this is not possible?
FROM MyTable
WHERE ... COMPLEX CLAUSE ...
GROUP BY SomeColumn) MyTable
My Question: Possible? And if, how?
Thanks for wrapping your mind around this!
Mark
This example assumes that you're using Metamodel generation.
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
Subquery<SomeColumnType> subcq = cq.subquery(SomeColumnType.class);
Root<MyTable> from = subcq.from(MyTable.class);
subcq.select(from.get(MyTable_.someColumn));
subcq.where(** complex where statements **);
subcq.groupBy(from.get(MyTable_.someColumn));
cq.select(cb.count(subcq));
I had a similar problem, counting distinct group by elements, and stumbled across this Thread, based on the already existing answers I came up with the following
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
CriteriaQuery<Long> countQuery = builder.createQuery(Long.class);
Root<Jpa1> countRoot = countQuery.from(Jpa1.class);
Subquery<Long> query = countQuery.subquery(Long.class);
Root<Jpa1> root = query.from(Jpa1.class);
applyFilter(query, root);
query.groupBy(root.get("groupBy_column1"), root.get("groupBy_column2"));
query.select(builder.min(root.get("id_column")));
countQuery.where(builder.in(countRoot.get("id_column")).value(query));
return countQuery.select(builder.count(countRoot.get("id_column")));
In case you have Spring Boot and you want to do the same, there is a better workaround that can only work for MySql 5/Maria by using the native SQL_CALC_FOUND_ROWS and FOUND_ROWS (It won't work on MySQL 8);
The main idea of this integration is flexibility for Spring-Data and the ability to override the default JpaRepositoryFactoryBean as follow
Step #1 - Create a custom CustomJpaRepositoryFactoryBean:
/**
*
* #author ehakawati
*
* #param <R>
* #param <T>
* #param <I>
*/
public class CustomJpaRepositoryFactoryBean<R extends JpaRepository<T, I>, T, I extends Serializable>
extends JpaRepositoryFactoryBean<R, T, I> {
public CustomJpaRepositoryFactoryBean(Class<? extends R> repositoryInterface) {
super(repositoryInterface);
}
#Override
protected RepositoryFactorySupport createRepositoryFactory(EntityManager entityManager) {
RepositoryFactorySupport repositoryFactorySupport = super.createRepositoryFactory(entityManager);
repositoryFactorySupport.setRepositoryBaseClass(CustomJpaRepository.class);
return repositoryFactorySupport;
}
}
Step #2 - Create a custom CustomJpaRepository
/**
*
* #author ehakawati
*
* #param <T>
* #param <ID>
*/
public class CustomJpaRepository<T, ID extends Serializable> extends SimpleJpaRepository<T, ID>
implements Repository<T, ID> {
private final EntityManager em;
/**
*
* #param entityInformation
* #param entityManager
*/
public CustomJpaRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.em = entityManager;
}
/**
*
* #param domainClass
* #param entityManager
*/
public CustomJpaRepository(Class<T> domainClass, EntityManager entityManager) {
super(domainClass, entityManager);
this.em = entityManager;
}
/**
* Reads the given {#link TypedQuery} into a {#link Page} applying the given
* {#link Pageable} and {#link Specification}.
*
* #param query must not be {#literal null}.
* #param domainClass must not be {#literal null}.
* #param spec can be {#literal null}.
* #param pageable can be {#literal null}.
* #return
*/
protected <S extends T> Page<S> readPage(TypedQuery<S> query, final Class<S> domainClass, Pageable pageable,
#Nullable Specification<S> spec) {
if (pageable.isUnpaged()) {
return super.readPage(query, domainClass, pageable, spec);
}
query.setFirstResult((int) pageable.getOffset());
query.setMaxResults(pageable.getPageSize());
return PageableExecutionUtils.getPage(query.getResultList(), pageable, () -> getNativeCount());
}
/**
*
*/
protected long getNativeCount() {
final Query query = em.createNativeQuery("SELECT FOUND_ROWS() as `count`");
return ((BigInteger) query.getSingleResult()).longValue();
}
}
Step #3 - PageableQueriesInterceptor:
/**
*
* #author ehakawati
*
* #param <T>
* #param <ID>
*/
public class PageableQueriesInterceptor extends EmptyInterceptor {
private static final Pattern PATTERN = Pattern.compile(".*?limit \\?(, \\?)?$");
private static final long serialVersionUID = 1L;
#Override
public String onPrepareStatement(String sql) {
if (PATTERN.matcher(sql).find()) {
sql = sql.replaceFirst("select", "select SQL_CALC_FOUND_ROWS ");
}
return sql;
}
}
Step #4 - Enable PageableQueriesInterceptor:
sprint.jpa.properties.hibernate.ejb.interceptor= com.****.****.******.betterpaging.PageableQueriesInterceptor
Enjoy
The CriteriaBuilder.countDistinct() method works nicely for counting groupBy results.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Long> cq = cb.createQuery(Long.class);
Root<MyEntity> root = cq.from(MyEntity);
cq.select(cb.countDistinct(root("entityField")));
cq.where(** your criteria **);
var result = em.createQuery(cq).getSingleResult();
var m = Math.toIntExact(result);
This would be like the SQL query
SELECT COUNT(DISTINCT myEntity.entityField) FROM myEntity WHERE **blah** ;
And, of course, the countDistinct (like the COUNT(DISTINCT ...)) can have multiple columns listed as needed for grouping purposes.

Testing not working CakePHP2.0

I have created the a test for a controller using Cake bake command.
Now, I want to test the function "index" of the controller and for it I do this:
public function testIndex() {
echo "printed";
$result = $this->testAction("/comments/1");
echo "not printed";
}
1 is the param, the id of the post where the comment is. Anyway, the controller works perfectly well, there's no problem with it.
As you can see, the test crashes after calling the testAction method. (it doesn't print the second echo)
I have seen that if the action called on the controller has any call to its model, testAction call won't work. But, if the action to test doesn't have any call to any Model, then, it works perfectly.
Whats happening here?
By the way, both databases, default and test has data in it so it's not either a problem with the database.
Thanks.
UPDATE:
here you have the rest of the testController generated by Cake bake command:
<?php
/* Comments Test cases generated on: 2012-04-12 11:49:17 : 1334224157*/
App::uses('CommentsController', 'Controller');
/**
* TestCommentsController *
*/
class TestCommentsController extends CommentsController {
/**
* Auto render
*
* #var boolean
*/
public $autoRender = false;
/**
* Redirect action
*
* #param mixed $url
* #param mixed $status
* #param boolean $exit
* #return void
*/
public function redirect($url, $status = null, $exit = true) {
$this->redirectUrl = $url;
}
}
/**
* CommentsController Test Case
*
*/
class CommentsControllerTestCase extends CakeTestCase {
/**
* Fixtures
*
* #var array
*/
public $fixtures = array('app.comment');
/**
* setUp method
*
* #return void
*/
public function setUp() {
parent::setUp();
$this->Comments = new TestCommentsController();
$this->Comments->constructClasses();
}
/**
* tearDown method
*
* #return void
*/
public function tearDown() {
unset($this->Comments);
parent::tearDown();
}
When you're testing controllers, make sure to extend the test case class by ControllerTestCase to take advantage of the testAction() method.