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.
Related
I've got Invoice entity, in which I'd like to generate subsequent numbers within a given month.
Entity code:
/**
* Class Invoice
* #package App\Entity
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks()
*/
class Invoice
{
(...)
/**
* #var int
* #ORM\Column(type="integer")
*/
private $year;
/**
* #var int
* #ORM\Column(type="integer")
*/
private $month;
/**
* #var int
* #ORM\Column(type="integer")
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="App\Helper\InvoiceNumberGenerator")
*/
private $counter;
(...)
/**
* #ORM\PrePersist
* #ORM\PreUpdate
*/
public function numberGenerator()
{
if ($this->getYear() === null) {
$this->setYear(date('Y'));
$this->setMonth(date('m'));
}
}
And App\Helper\InvoiceNumberGenerator code is:
<?php
namespace App\Helper;
use App\Entity\Invoice;
use Doctrine\Common\Persistence\ObjectRepository;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Id\AbstractIdGenerator;
use Exception;
class InvoiceNumberGenerator extends AbstractIdGenerator
{
/**
* Generates an invoice number
*
* #param EntityManager $em
* #param Invoice $entity
* #return mixed
* #throws Exception
*/
public function generate(EntityManager $em, $entity)
{
if (!$entity instanceof Invoice) {
throw new Exception('Generator służy tylko do generowania numerów faktur.');
}
/** #var ObjectRepository | EntityRepository $invoiceRepository */
$invoiceRepository = $em->getRepository(Invoice::class);
/** #var Invoice $lastInvoice */
$lastInvoice = $invoiceRepository->findOneBy(
array(
'year' => $entity->getYear(),
'month' => $entity->getMonth()
),
array(
'counter' => 'desc'
)
);
if (empty($lastInvoice)) {
return 1;
}
return $lastInvoice->getCounter() + 1;
}
}
When I dump $lastInvoice, it shows:
Invoice {#5522 ▼
-id: 1
-generated: false
-fileName: "example"
-year: 2019
-month: 11
-counter: 1
-name: "AG"
-company: "Gall"
-address: "Street 1"
-address2: "Gliwice"
-nip: "6314567890"
-reservation: Reservation {#5855 ▶}
-date: null
}
So it looks like the generator gets to selecting last one correctly, but nevertheless I got error when trying to create new Invoice:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'counter'
cannot be null
Any advise on what I'm doing wrong?
the #CustomIdGenerator annotation is only called when the column is also marked with #Id. From the docs:
This annotations allows you to specify a user-provided class to generate identifiers. This annotation only works when both #Id and #GeneratedValue(strategy="CUSTOM") are specified.
Ids are always a special kind of thing and thus must sometimes be perfect before inserting. To solve your problem - because the counter is not an id column -, you could use lifecycle events instead (prePersist, probably) and use the event's entity manager in an event listener/subscriber to run your query.
I'm using API-Platform and faced an issue with updating many-to-many with an empty value.
Here is the small example:
/**
* Many Organizations have Many Followers.
* #ORM\ManyToMany(targetEntity="App\Entity\User\User", inversedBy="organizations")
* #ORM\JoinTable(name="organizations_followers",
* joinColumns={#ORM\JoinColumn(name="organization_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="user_id", referencedColumnName="id", unique=true)}
* )
*/
protected $followers;
/**
* #return Collection
*/
public function getFollowers(): Collection
{
return $this->followers;
}
/**
* #param array $followers
*/
public function setFollowers(array $followers): void
{
$this->followers = $followers;
}
/**
* Organization constructor.
*/
public function __construct()
{
$this->id = Uuid::uuid4();
$this->followers = new ArrayCollection();
}
So, when I'm trying to delete all followers (PATCH request with empty followers in the relationships field) I always get one undeleted record. What am I doing wrong? Any Ideas?
I have two simple entities connected to each other with a OneToMany association.
/**
* #ORM\Entity(repositoryClass="Shopware\CustomModels\JoeKaffeeAbo\Client")
* #ORM\Table(name="joe_kaffee_abo_client")
*/
class Client extends ModelEntity
{
public function __construct() {
$this->abos = new ArrayCollection();
}
/**
* #ORM\OneToMany(targetEntity="Shopware\CustomModels\JoeKaffeeAbo\Abo", mappedBy="client", cascade= "all")
* #var \Doctrine\Common\Collections\ArrayCollection
*/
protected $abos;
public function addAbo($abo)
{
//$this->abos[] = $abo;
$this->abos->add($abo);
}
The second one is Abo:
/**
* #ORM\Entity(repositoryClass="Shopware\CustomModels\JoeKaffeeAbo\Abo")
* #ORM\Table(name="joe_kaffee_abo_abos")
*/
class Abo extends ModelEntity
{
/**
* #ORM\ManyToOne(targetEntity="Shopware\CustomModels\JoeKaffeeAbo\Client",inversedBy="abos")
* #ORM\JoinColumn(name="clientId", referencedColumnName="id")
*/
protected $client;
}
Somehow the join column gets not filled - it seems that doctrine ignores the association I set up. And if I call $client->addAbo($abo) there gets not relation created...and I cant receive the added abos via a getter function which returns the abo array collection.
i want to fetch the data of a menu including its categories in a custom repository by a dql-statement, but it doesn't return the associated entities. I need it as json data, so i added the hydration mode Query::HYDRATE_ARRAY to the function call.
/**
* #ORM\Entity(repositoryClass="Company\Repository\Doctrine\MenuRepository")
* #ORM\Table(name="menu")
*/
class Menu {
/**
* #var \Company\Entity\MenuCategory[]
*
* #ORM\OneToMany(targetEntity="Company\Entity\MenuCategory", mappedBy="menu")
*/
protected $categories;
}
/**
* #ORM\Entity(repositoryClass="Company\Repository\Doctrine\MenuCategoryRepository")
* #ORM\Table(name="menu_category")
*/
class MenuCategory {
/**
* #var \Company\Entity\Menu
*
* #ORM\ManyToOne(targetEntity="Company\Entity\Menu", inversedBy="categories")
* #ORM\JoinColumn(nullable=false)
*/
protected $menu;
}
class MenuRepository extends EntityRepository implements MenuRepositoryInterface {
public function findById($id, $hydration = Query::HYDRATE_OBJECT) {
$queryBuilder = $this->createQueryBuilder('menu')
->leftJoin('menu.categories', 'categories')
->where('menu.id = :menuId')
->setParameter('menuId', $id);
return $queryBuilder->getQuery()->getSingleResult($hydration);
}
}
The result looks like that:
array(4) {
["id"]=>int(1)
["name"]=> string(6) "Test"
}
I've a problem with my many-to-many relation. I want to have access to the reference table for a querybuilder query. With a many-to-many relation I don't have access to my reference table, so I've set up two one-to-many relationships. My structure look likes:
User ---> UserUserCategory <--- UserCategory
The above structure has two one-to-many relationships and are working fine with the database. When I have a user with the following data in the database (in UserUserCategory):
Table User
ID | Name
1 | Bart
2 | Gerard
Table Category
ID | Name
1 | Officer
2 | Medic
Table UserUserCategory
User | Category
1 | 1
2 | 2
So Bart is an Officer and Gerard is a Medic. But when I want to retrieve the data, it said that Bart is the Medic, and Gerard has a "null" value in the category.
My User-entity:
/**
* Entity Class representing a post of our User module.
*
* #ORM\Entity
* #ORM\Table(name="user")
* #ORM\Entity(repositoryClass="User\Repository\UserRepository")
*
*/
class User extends zfcUser implements UserInterface
{
/**
* Categories from user
*
* #ORM\OneToMany(targetEntity="User\Entity\UserUserCategory", mappedBy="user_id", cascade={"remove", "persist"})
* #var UserUserCategory
* #access protected
*/
protected $user_usercategories;
//name & user_id comes here
/**
* Constructor to make a new ArrayCollection for addresses
*
*
*/
public function __construct()
{
$this->user_usercategories = new ArrayCollection();
}
/**
* #param Collection $categories
*/
public function addUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setUser($this);
$this->user_usercategories->add($user_usercategorie);
}
}
/**
* #param Collection $categories
*/
public function removeUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setUser(null);
$this->user_usercategories->removeElement($user_usercategorie);
}
}
/**
* #return Collection
*/
public function getUserUserCategories()
{
return $this->categories;
}
}
My UserCategory-entity:
/**
* A User category entity.
* #ORM\Entity
* #ORM\Table(uniqueConstraints={#ORM\UniqueConstraint(name="unique_name_parentId", columns={"name", "parent_id"})})
* #ORM\HasLifecycleCallbacks
*/
class UserCategory extends Category
{
/**
* User_usercategories
*
* #ORM\OneToMany(targetEntity="User\Entity\UserUserCategory", mappedBy="category_id")
* #var UserUserCategory
* #access protected
*/
protected $user_usercategories;
/**
* Constructor
*/
public function __construct()
{
$this->user_usercategories = new ArrayCollection();
}
/**
* #param Collection $categories
*/
public function addUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setCategory($this);
$this->user_usercategories->add($user_usercategorie);
}
}
/**
* #param Collection $categories
*/
public function removeUserUserCategories(Collection $user_usercategories)
{
foreach ($user_usercategories as $user_usercategorie) {
$user_usercategorie->setCategory(null);
$this->user_usercategories->removeElement($user_usercategorie);
}
}
/**
* #return Collection
*/
public function getUserUserCategories()
{
return $this->categories;
}
}
My UserUserCategory-entity:
/**
* Entity Class representing a post of our User_UserCategory entity.
*
* #ORM\Entity
* #ORM\Table(name="user_usercategory")
*
*/
class UserUserCategory
{
/**
* User with a category
*
* #ORM\ManyToOne(targetEntity="User\Entity\User", inversedBy="user_usercategories")
* #ORM\JoinColumn(name="user_id", referencedColumnName="user_id", nullable=false, onDelete="CASCADE")
* #ORM\Id
*
* #var User
* #access protected
*/
protected $user_id;
/**
* Category from user
*
* #ORM\ManyToOne(targetEntity="User\Entity\UserCategory", inversedBy="user_usercategories")
* #ORM\JoinColumn(name="category_id", referencedColumnName="id", nullable=false, onDelete="CASCADE")
* #ORM\Id
*
* #var Category
* #access protected
*/
protected $category_id;
public function getUser()
{
return $this->user;
}
/**
* Set User
*
* #param User $user
* #return User
*/
public function setUser(User $user = null)
{
//die('setUser');
$this->user = $user;
return $this;
}
public function getCategory()
{
return $this->category;
}
/**
* Set Category
*
* #param Category $category
* #return Category
*/
public function setCategory(Category $category = null)
{
$this->category = $category;
return $this;
}
}
When I execute the following line, it gives back the wrong result. The wrong category pops up:
\Doctrine\Common\Util\Debug::dump($this->getEntityManager()->find('User\Entity\User', '49')->user_usercategories);
die;
Result:
array(1) {
[0]=>
object(stdClass)#452 (3) {
["__CLASS__"]=>
string(28) "User\Entity\UserUserCategory"
["user_id"]=>
string(16) "User\Entity\User"
["category_id"]=>
string(24) "User\Entity\UserCategory"
}
}
In the category_id is the medic printed, I expect the officer to get back.
In my other user, (id=60) the category_id field is "null". So it look likes Doctrine skips the first input in my UserUserCategory, starts with the second and can't get the last category anymore.
No offence, but I find your code very hard to read. I would suggest you to do few corrections and that might even help you in solving the problem.
1: Naming: Instead of UserCategory, rename it to Category. If your Category will have different types, create new column "type" with values from constansts like
class Category
{
const TYPE_USER = 1 ;
....
2: Instead of addCategories(Collection $array), do singular version like
public function addCategory(Category $category)
{
$reference = new UserCategory() ;
$reference->setUser($this) ;
$reference->setCategory($category) ;
$this->user_categories->add($reference) ;
}
public function removeCategory(Category $category)
{
foreach($this->user_categories as $reference) {
if ( $reference->getCategory() === $category )
$this->user_categories->removeElement($reference) ;
}
}
Symfony2 automaticaly recognizes methods like this. Even if your relation is plural (like categories), s2 will find singularified addCategory and removeCategory methods.
To get array of categories, use this:
public function getCategories()
{
$categories = new ArrayCollection() ;
foreach($this->user_categories as $reference) {
$categories->add( $reference->getCategory() ) ;
}
return $categories ;
}
If you do it like this, you will probably solve the problem you have.