Selecting generic primary key with CriteriaQuery - java

When migrating from Hibernate Criteria api to CriteriaQuery I ran into a generic DAO for a abstract class that has a where on a common field but does a select on their id, even if the ids are totally different per class.
The old projection looks like this
criteria.setProjection(Projections.id());
Is there any way to do this in a similar way with CriteriaQuery?
Edit: Full criteria code
DetachedCriteria detachedCriteria = DetachedCriteria.forClass(MyEntity.class);
detachedCriteria.add(Restrictions.in("accountID", accounts));
detachedCriteria.setProjection(Projections.id());
EntityManager em = ...;
Criteria criteria = detachedCriteria.getExecutableCriteria((Session) em.getDelegate());
List<Integer> list = criteria.list();

I just managed to find it on my own.
criteriaQuery.select(
root.get(entityRoot.getModel().getDeclaredId(int.class))
);

Combining answers:
https://stackoverflow.com/a/16911313
https://stackoverflow.com/a/47793003
I created this method:
public String getIdAttribute(EntityManager em, String fullClassName) {
Class<? extends Object> clazz = null;
try {
clazz = Class.forName(fullClassName);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
Metamodel m = em.getMetamodel();
IdentifiableType<T> of = (IdentifiableType<T>) m.managedType(clazz);
return of.getId(of.getIdType().getJavaType()).getName();
}
then I have the entity manager injected
#PersistenceContext
private EntityManager em;
I get the root entity primary key like that:
String rootFullClassName = root.getModel().getJavaType().getName();
String primaryKeyName = getIdAttribute(em, rootFullClassName);
and I get the primary keys referenced on attributes like that:
return (Specification<T>) (root, query, builder) -> {
Set<Attribute<? super T, ?>> attributes = root.getModel().getAttributes();
for (Attribute a: attributes) {
if(a.isAssociation()) {
Path rootJoinGetName = root.join(a.getName());
String referencedClassName = rootJoinGetName.getJavaType().getName();
String referencedPrimaryKey = getIdAttribute(em, referencedClassName);
//then I can use it to see if it is equal to a value (e.g
//filtering actors by movies with id = 1 - in
//this case referencedPrimaryKey is "id")
Predicate p = rootJoinGetName.get(referencedPrimaryKey).in(1);
}
}
}
In this way I don't need to know the type of the primary key/referenced key in advance as it can be derived through the Entity Manager Meta Model. The above code can be used with CriteriaQuery as well as Specifications.

Related

How can I get specific item from collection? Criteria API

I have two entities with one-to-many relationships (simplified):
public class Action{
#OneToMany
private Set<ActionDetailParameter> detailParameters = new HashSet<>(0);
}
public class ActionDetailParameter {
private String parameterName;
private String parameterValue;
}
I need to select all Actions where detailParameters item has parameterName equals "newserviceDepartmentName". I tried using this code:
...
SetJoin<Action, ActionDetailParameter> detailParameters = actionRoot.joinSet("detailParameters", JoinType.LEFT);
Predicate namePredicate = criteriaBuilder.equal(detailParameters.get("parameterName"), "newserviceDepartmentName");
QueryBuildingCriteria<Action> queryBuildingCriteria = getQueryBuildingCriteria(Action.class);
CriteriaQuery<Action> query = (CriteriaQuery<Action>) queryBuildingCriteria.getQuery();
getResultList(createQuery(query.select(actionRoot).where(namePredicate)));
...
there was the following exception:
org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias1.parameterName' [select generatedAlias0 from common.dao.entities.Action as generatedAlias0 where generatedAlias1.parameterName=:param0]
please tell me what I am doing wrong
I believe your problem to be that you construct a new CriteriaQuery after creating the actionRoot. As you don't show the whole code, this is some speculation.
QueryBuildingCriteria<Action> queryBuildingCriteria = getQueryBuildingCriteria(Action.class);
CriteriaQuery<Action> query = (CriteriaQuery<Action>) queryBuildingCriteria.getQuery();
I have adjusted your mapping to something I had existing in my system and have simply replaced the working classes/field names with the ones from your question:
public void test() {
CriteriaBuilder cb = getSessionFactory().getCriteriaBuilder();
CriteriaQuery<Action> createQuery = cb.createQuery(Action.class);
Root<Action> x = createQuery.from(Action.class);
createQuery.select(x)
.where(cb.equal(x.joinList("detailParameters").get("parameterName"),
"newserviceDepartmentName"));
List<Action> resultList = getSession().createQuery(createQuery).getResultList();
System.out.println(resultList);
}
This should return all Actions which have an ActionDetailParameter where the parameterName equals newserviceDepartmentName

Hibernate Fetching with Multiselect in Criteria API not working

I am using Hibernate Criteria API to load the data from the DB, however when using multiselect and fetching the related entities with mapping #OneToOne, #ManyToOne & #ManyToMany, I am getting error.
Code to get the data
private Session getSession() {
return entityManager.unwrap(SessionImplementor.class);
}
#Override
public Account getGatewayAccount(Long appId, String accountNumber) {
Session session = getSession();
CriteriaBuilder criteria = session.getCriteriaBuilder();
CriteriaQuery<Account> query = criteria.createQuery(Account.class);
Root<Account> from = query.from(Account.class);
from.fetch(Account_.APP, JoinType.INNER);
query.multiselect(from.get(Account_.ID), from.get(Account_.ACCOUNT_NUMBER))
.where(criteria.equal(from.get(Account_.ACCOUNT_NUMBER), accountNumber),
criteria.equal(from.get(Account_.APP).get(App_.ID), appId));
try {
return session.createQuery(query)
.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
I have also tried using EntityGraph to lad the data like below
private Session getSession() {
return entityManager.unwrap(SessionImplementor.class);
}
#Override
public Account getGatewayAccount(Long appId, String accountNumber) {
Session session = getSession();
RootGraph<Account> entityGraph = session.createEntityGraph(Account.class);
entityGraph.addAttributeNodes("app");
CriteriaBuilder criteria = session.getCriteriaBuilder();
CriteriaQuery<Account> query = criteria.createQuery(Account.class);
Root<Account> from = query.from(Account.class);
query.multiselect(from.get(Account_.ID), from.get(Account_.ACCOUNT_NUMBER))
.where(criteria.equal(from.get(Account_.ACCOUNT_NUMBER), accountNumber),
criteria.equal(from.get(Account_.APP).get(App_.ID), appId));
try {
return session.createQuery(query)
.setHint("javax.persistence.fetchgraph", entityGraph)
.getSingleResult();
} catch (NoResultException e) {
return null;
}
}
The exception that I am getting is
org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.QueryException: query specified join fetching,
but the owner of the fetched association was not present in the select list
I am looking a way which can fetch the related objects with multiselect in select clause and get the data as POJO object, not like a Tuple.
What you want seems to be a constructor query
query.select(criteria.construct(Answer.class, from.get(Account_.ID), from.get(Account_.ACCOUNT_NUMBER))
This works by using a constructor on the defined class that accepts the two parameters define in the query, so bypasses all JPA hooks; What is returned is an unmanaged POJO. Since this isn't a managed entity fetch graphs will likely not have any meaning.
Otherwise, you can just return:
query.select(from)
This will return an Address instance that is managed and has all attributes described in your entityGraph loaded. Depending on your provider specifics, everything not defined in the entityGraph should be left as lazy and so unfetched.

Spring Data JPA Specification groupBy

sorry for my english first.
i want use jpa to groupby, like : select scrip, dustup, count(*) from data flow group by scrip, dstip.
so, write these code:
public class DataflowSpec {
public static Specification<Dataflow> search(final String[] group, final String[] sort, final String[] desc) {
return new Specification<Dataflow>() {
#Override
public Predicate toPredicate(Root<Dataflow> root1, CriteriaQuery<?> query1, CriteriaBuilder builder) {
// TODO Auto-generated method stub
CriteriaQuery<Tuple> query = builder.createQuery(Tuple.class);
Root<Dataflow> root = query.from(Dataflow.class);
query.multiselect(root.get("srcip"), root.get("dstip"), builder.count(root));
query.groupBy(root.get("srcip"), root.get("dstip"));
query.orderBy(builder.desc(root.get("srcip").as(BigInteger.class)));
return query.getRestriction();
}
};
}
}
but , SQL log is:
Hibernate:
select
count(dataflow0_.id) as col_0_0_
from
Dataflow dataflow0_
Hibernate:
select
dataflow0_.id as id1_2_,
dataflow0_.byteall as byteall2_2_,
dataflow0_.bytedn as bytedn3_2_,
dataflow0_.byteup as byteup4_2_,
dataflow0_.dstip as dstip5_2_,
dataflow0_.dstport as dstport6_2_,
dataflow0_.engieid as engieid7_2_,
dataflow0_.flag as flag8_2_,
dataflow0_.netid as netid9_2_,
dataflow0_.pkgall as pkgall10_2_,
dataflow0_.pkgdn as pkgdn11_2_,
dataflow0_.pkgup as pkgup12_2_,
dataflow0_.protocolid as protoco17_2_,
dataflow0_.rtt as rtt13_2_,
dataflow0_.srcip as srcip14_2_,
dataflow0_.srcport as srcport15_2_,
dataflow0_.updatetime as updatet16_2_
from
Dataflow dataflow0_ limit ?
so, how to resolve it? thanks!
For people still looking for how to apply "group by" in Spring jpa Specification, you can use something like the following snippet:
...
private Dataflow dataflowFilter;
#Override
public Predicate toPredicate(Root&ltDataflow&gt root, CriteriaQuery&lt?&gt cq, CriteriaBuilder cb) {
Predicate predicate = cb.conjunction();
predicate.getExpressions().add(cb.equal(root.get("id"), dataflowFilter.getId()));
...
cq.groupBy(root.get("id"));
...
return predicate;
}
You can achieve spring data group by by specification, just follow
[section 2.6][1] or [section 3.6][2] for version before or after 2.0. For single repository manipulation, the two versions have identical solution. For the *all * repository solution, before 2.0 use [customized factory bean][3], while after 2.0 this factory bean manipulation is omitted.
public Map<AlarmMsg.AlarmLevel, Long> testSpecification(String neId) {
SingularAttribute attribute = AlarmData_.isClear;
Specification<Object> where = Specification.where(
(root, query, cb) -> cb.equal(root.get(attribute), false)
);
final Map<AlarmMsg.AlarmLevel, Long> result = alarmDataRepository.groupAndCount(AlarmData_.alarmLevel, where );
return result;
}
repository:
public interface AlarmDataRepository extends JpaRepository<AlarmData, Long>, JpaSpecificationExecutor<AlarmData>, CustomizedGroupCountRepository {
Fragment repository and its implementation:
public interface CustomizedGroupCountRepository {
Map<AlarmMsg.AlarmLevel, Long> groupAndCount(SingularAttribute singularAttribute, Specification where);
}
public class CustomizedGroupCountRepositoryImpl implements CustomizedGroupCountRepository {
private final EntityManager entityManager;
public CustomizedGroupCountRepositoryImpl(EntityManager entityManager) {
Assert.notNull(entityManager, "EntityManager must not be null!");
this.entityManager = entityManager;
}
#Override
public Map<AlarmMsg.AlarmLevel, Long> groupAndCount(SingularAttribute singularAttribute, Specification where) {
final CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
final CriteriaQuery<Tuple> query = criteriaBuilder.createQuery(Tuple.class);
final Root<AlarmData> root = query.from(AlarmData.class);
final Path<AlarmMsg.AlarmLevel> expression = root.get(singularAttribute);
query.multiselect(expression, criteriaBuilder.count(root));
query.select(criteriaBuilder.tuple(expression, criteriaBuilder.count(root)));
query.where(where.toPredicate(root, query, criteriaBuilder));
query.groupBy(expression);
final List<Tuple> resultList = entityManager.createQuery(query).getResultList();
return resultList.stream()
.collect(toMap(
t -> t.get(0, AlarmMsg.AlarmLevel.class),
t -> t.get(1, Long.class))
);
}
}
The main difference between one-for-all-repository and one-for-single-repository is, in one-for-single-repository case, it can access the real entity class, like User in spring reference document. So that you don't need to use generic types to refer an any-typed entity, while in one-for-all-repository case, the implementation of the customized method uses generic types, and its class information could (or must) be gained from an injected JpaEntityInformation as stated in both section 3.6.
[1]: https://docs.spring.io/spring-data/jpa/docs/1.8.0.RELEASE/reference/html/#repositories.single-repository-behaviour
[2]: https://docs.spring.io/spring-data/jpa/docs/2.0.5.RELEASE/reference/html/#repositories.single-repository-behavior
[3]: https://jeroenbellen.com/spring-data-extending-the-jpa-specification-executor/
Specification doesn't support groupBy.
SimpleJpaRepository replaced query.select/multiselect by query.select(root)

Trouble deleting entity with specific property set in Java GAE

I have the entities created like this:
public String addNewStockName(String newStock) throws DelistedException {
Entity stock = new Entity("Stocks");
stock.setProperty("Name", newStock);
ds.put(stock);
return "OK";
}
Trying delete the specific entity like this:
public String deleteStockName(String stockName){
Key key = KeyFactory.createKey("Stocks", stockName);
ds.delete(key);
return "OK";
}
And it does not delete the entity which has property 'stockName'. Why?
If you want to create an entity that you can fetch by stockName, you need something like
public String addNewStockName(String stockName) throws DelistedException {
Key key = KeyFactory.createKey("Stocks", stockName);
Entity stock = new Entity(key);
stock.setProperty("foo", "bar");
ds.put(stock);
return "OK";
}
You can then use your deleteStockName() method as is. This of course assumes your key name is unique, but it also means you can always fetch the Stock by key, rather than query.
Your Stocks entity has a property named "Name". That is not the same as the key name.
You have to perform a query to get the entities or entity keys matching the filter of "Name=?".
Something like this:
public String deleteStockName(String stockName) {
DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
Filter f = new FilterPredicate("Name", FilterOperator.EQUAL, stockName);
Query q = new Query("Stocks").setFilter(f).setKeysOnly();
List<Entity> results = ds.prepare(q)
.asList(FetchOptions.Builder.withDefaults());
if (results.isEmpty())
return "Not Found!";
ds.delete(results.get(0).getKey());
return "OK";
}

How do I correctly paginate in Hibernate with nested objects with ManyToMany associations?

Ok, so I have the following (abbreviated) 3 entity and HibernateUtil classes.
public class Tag {
#Id
BigDecimal id;
String tag
#ManyToMany( mappedBy="tags" )
List<Label> labels;
}
public class Label {
#Id
BigDecimal id;
String label;
#ManyToMany( targetEntity=Tag.class )
List<Tag> tags;
}
public class Data {
#Id
BigDecimal id;
BigDecimal data;
#ManyToOne
Label label;
}
public class HibernateUtil {
public static List pagedQuery(DetachedCriteria detachedCriteria, Integer start, Integer size) throws WebApplicationException {
Session session = getSession();
try {
Transaction transaction = session.beginTransaction();
List records = detachedCriteria.getExecutableCriteria(session)
.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
.setFirstResult(start)
.setMaxResults(size)
.list();
transaction.commit();
return records;
} catch (Exception e) {
// Place Logger here...
throw new WebApplicationException(e);
} finally {
session.close();
}
}
}
The issue I have is that when I try to query the Data class with the HibernateUtil.pagedQuery( detatchedCriteria, start, size ), my result list doesn't match the size parameter. I have found that the reason for this is the way hibernate builds the query to include the tags (Data.Label.Tags).
For instance, when a Label has more than one associated Tags the result list for the Data object subquery used in the complete paginated query would look like the following (I found this by parsing the sql Hibernate spits out to the console)
Data-1;Label:Tag-1
Data-1;Label;Tag-2
Data-2;Label;Tag-1
Data-2;Label;Tag-2
etc...
If I were to call this with size=3, then the returned result set would be
Data-1;Label:Tag-1
Data-1;Label;Tag-2
Data-2;Label;Tag-1
However, Hibernate would then group the first two rows together (since they're the same Data object), and my returned List object would have a size of 2 (Data-1 & Data-2)
I attempted to replace the setResultTransformer method with a Projection approach that I found through Google, but that then only returned the id's of the Data objects.
Does anyone have any advice for me? I'm not sure where to go from here...
You are facing a common problem paginating with hibernate. The resultTransformer is applied in the "Java" side, so the pagination has already been made on the DB side.
The simplest (maybe not the most optimized) is to do two queries, one with the projection and pagination (like the one you already did) and another using the projection id's. Here is an example:
//get the projection
Criteria criteria = factory.getCurrentSession().createCriteria(getEntityClass());
criteria.setProjection(Projections.distinct((Projections.projectionList().add(Projections.id()).add(Projections.property("name")))));
//paginate the results
criteria.setMaxResults(pageSize);
criteria.setFirstResult(first);
List<Object[]> idList = criteria.list();
//get the id's from the projection
List<Long> longList = new ArrayList<Long>();
for (Object[] long1 : idList) {
Object[] record = long1;
longList.add((Long) record[0]);
}
if (longList.size() > 0) {
//get all the id's corresponding to the projection,
//then apply distinct root entity
criteria = factory.getCurrentSession().createCriteria(getEntityClass());
criteria.add(Restrictions.in("id", longList));
criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
} else {
//no results, so let's ommit the second query to the DB
return new ArrayList<E>();
}
return criteria.list();

Categories

Resources