I have a JPA entity User which contains a field (entity) City. I want to select one page of, for example, 10 users but from different cities.
In SQL I would use something like:
SELECT DISTINCT ON (u.city_id) u.username ,u.email, u.city_id ....
FROM user u LIMIT 0,10 ....
but I need to do it with JPQL or JPA criteria builder. How can I achieve this?
Recently I came across same situation, found that there is no direct way using criteria query to support it.
Here is my solution -
Create custom sql function for distinct on
register function to dialect
Update dialect in properties
call it from criteria query
1) Create Custom function
public class DistinctOn implements SQLFunction {
#Override
public boolean hasArguments() {
return true;
}
#Override
public boolean hasParenthesesIfNoArguments() {
return true;
}
#Override
public Type getReturnType(Type type, Mapping mapping) throws QueryException {
return StandardBasicTypes.STRING;
}
#Override
public String render(Type type, List arguments, SessionFactoryImplementor sessionFactoryImplementor) throws QueryException {
if (arguments.size() == 0) {
throw new QueryException("distinct on should have at least one argument");
}
String commaSeparatedArgs = String.join(",",arguments);
return " DISTINCT ON( " + commaSeparatedArgs + ") " + arguments.get(0) + " ";
}
}
2) Register Function
public class CustomPostgresSqlDialect extends PostgreSQLDialect {
public CustomPostgresSqlDialect() {
super();
registerFunction("DISTINCT_ON", new DistinctOn());
}
}
3) Update Dialect :
Here pass on your class name
spring.jpa.properties.hibernate.dialect = com.harshal.common.CustomPostgresSqlDialect
4) Use it in Criteria Query
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> user = query.from(User.class);
// SELECT DISTINCT ON (u.city_id) u.username
query.multiselect(
cb.function("DISTINCT_ON", String.class, user.get("city")),
user.get("userName")
);
return em.createQuery(query).getResultList();
You can do this by using Hibernate Criteria Query
sample code can be like this
Criteria criteria = session.createCriteria(user.class);
ProjectionList projectionList = Projections.projectionList();
projectionList.add(Projections.distinct(projectionList.add(Projections.property("city_id"), "cityId")));
projectionList.add(Projections.property("username"), "username");
projectionList.add(Projections.property("email"), "email");
criteria.setProjection(projectionList2);
criteria.setResultTransformer(Transformers.aliasToBean(user.class));
List list = criteria.list();
Related
I created the following predicate that matches a given set of string e.g. "a,b,c" against an column containing an #ElementCollection Set<String>.
I want all entries that contain "a" or "b" or "c". E.g. all these entries/set match:
a //because of a
b,d //because of b
c,f,g //because of c
I have a predicate that is working - i only need to make it case insensitive (don't want to change the data base):
private Predicate matchStringSet(Set<String> set, SetAttribute<DeliveryModeEntity, String> attribute,
CriteriaBuilder criteriaBuilder, Root<DeliveryModeEntity> deliveryMode) {
List<Predicate> matchEach = new ArrayList<>();
for (String string : set) {
matchEach.add(criteriaBuilder.isMember(string.toLowerCase(), deliveryMode.get(attribute)));
}
return criteriaBuilder.or(matchEach.toArray(new Predicate[] {}));
}
How to make deliveryMode.get(attribute) use lower case or how to make this case insensitive.
I tried to use a join as mentioned here: https://stackoverflow.com/a/10225074/447426
but this is not working within a for loop - it will create a join per string in set.
I have a similar entity, Widget, having tags:
#ElementCollection
#Column(name = "tag")
private List<String> tags = new ArrayList<>();
With what you have, to find all widgets having given tags use:
private Predicate matchTags(List<String> tags, Root<Widget> root, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>();
for (String tag : tags) {
predicates.add(cb.isMember(tag, root.get(Widget_.tags)));
}
return PredicateReducer.OR.reduce(cb, predicates); // cb.or(predicates.toArray(new Predicate[0]));
}
To find all Widgets having given tags in a case-insensitive way, first join the tags and then perform a like search:
private Predicate matchTagsCaseInsensitive(List<String> tags, Root<Widget> root, CriteriaBuilder cb) {
List<Predicate> predicates = new ArrayList<>();
Join<Widget, String> joinedTag = root.join(Widget_.tags);
for (String tag : tags) {
predicates.add(cb.like(cb.lower(joinedTag), tag.toLowerCase()));
}
return PredicateReducer.OR.reduce(cb, predicates); // cb.or(predicates.toArray(new Predicate[0]));
}
Edit: Adding PredicateReducer
I find myself AND-ing or OR-ing predicates often, so wrapped those operations into an enum:
public enum PredicateReducer {
AND {
#Override
public Predicate reduce(CriteriaBuilder cb, List<Predicate> predicates) {
return cb.and(predicates.toArray(new Predicate[0]));
}
},
OR {
#Override
public Predicate reduce(CriteriaBuilder cb, List<Predicate> predicates) {
return cb.or(predicates.toArray(new Predicate[0]));
}
};
public abstract Predicate reduce(CriteriaBuilder cb, List<Predicate> predicates);
public Predicate reduce(CriteriaBuilder cb, Predicate... predicates) {
return reduce(cb, Arrays.asList(predicates));
}
}
Thank you for help :)
I tried to get last id, and read many post about it, but i don't arrive to apply it in my case.
First Class
private Date date;
private List<AdsEntity> adsDetails;
... getters and setters
Second Class (AdsEntity)
private int id;
private String description;
There is the code where i try to get the last id :
Mapper
#Insert({
"<script>",
"INSERT INTO tb_ads_details (idMyInfo, adDate)"
+ " VALUES"
+ " <foreach item='adsDetails' index='index' collection='adsDetails' separator=',' statement='SELECT LAST_INSERT_ID()' keyProperty='id' order='AFTER' resultType='java.lang.Integer'>"
+ " (#{adsDetails.description, jdbcType=INTEGER}) "
+ " </foreach> ",
"</script>"})
void saveAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails);
In debugging mode, when I watch List I see the id still at 0 and don't get any id.
So what I wrote didn't workout :(
Solution Tried with the answer from #Roman Konoval :
#Roman Konoval
I apply what you said, and the table is fully well set :)
Just one problem still, the ID is not fulfill
#Insert("INSERT INTO tb_ads_details SET `idMyInfo` = #{adsDetail.idMyInfo, jdbcType=INTEGER}, `adDate` = #{adsDetail.adDate, jdbcType=DATE}")
#SelectKey(statement = "SELECT LAST_INSERT_ID()", before = false, keyColumn = "id", keyProperty = "id", resultType = Integer.class )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
default void saveManyAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails)
{
for(AdsDetailsEntity adsDetail:adsDetails) {
saveAdsDetails(adsDetail);
}
}
Thank for your help :)
Solution add to #Roman Konoval proposal from #Chris advice
#Chris and #Roman Konoval
#Insert("INSERT INTO tb_ads_details SET `idMyInfo` = #{adsDetail.idMyInfo, jdbcType=INTEGER}, `adDate` = #{adsDetail.adDate, jdbcType=DATE}")
#SelectKey(statement = "SELECT LAST_INSERT_ID()", before = false, keyColumn = "id", keyProperty = "adsDetail.id", resultType = int.class )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
default void saveManyAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails)
{
for(AdsDetailsEntity adsDetail:adsDetails) {
saveAdsDetails(adsDetail);
}
}
Thanks to all of you, for the 3 suggestions!!!
yes. it doesnt work.
please take a look at mapper.dtd
foreach-tag doesnt support/provide the following properties statement, keyProperty order and resultType
if you need the id for each inserted item please let your DataAccessObject handle iteration and use something like this in your MapperInterface
#Insert("INSERT INTO tb_ads_details (idMyInfo, adDate) (#{adsDetail.idMyInfo, jdbcType=INTEGER}, #{adsDetail.adDate, jdbcType=DATE})")
#SelectKey(before = false, keyColumn = "ID", keyProperty = "id", resultType = Integer.class, statement = { "SELECT LAST_INSERT_ID()" } )
void saveAdsDetails(#Param("adsDetail") AdsDetailsEntity adsDetail);
please ensure AdsDetailsEntity-Class provides the properties idMyInfoand adDate
Edit 2019-08-21 07:25
some explanation
referring to the mentioned dtd the <selectKey>-tag is only allowed as direct child of <insert> and <update>. it refers to a single Object that is passed into the mapper-method and declared as parameterType.
its only executed once and its order property tells myBatis wether to execute it before or after the insert/update statement.
in your case, the <script> creates one single statement that is send to and handled by the database.
it is allowed to combine #Insert with <script> and <foreach> inside and #SelectKey. but myBatis doesnt intercept/observe/watch database handling the given statement. and as mentioned before, #SelectKey gets executed only once, before or after #Insert-execution. so in your particular case #SelectKey returns the id of the very last inserted element. if your script inserts ten elements, only the new generated id of tenth element will be returned. but #SelectKey requires a class-property with getter and setter to put the selected id into - which List<?> doesnt provide.
example
lets say you want to save an Advertisement and its AdvertisementDetails
Advertisement has an id, a date and details
public class Advertisement {
private List<AdvertisementDetail> adDetails;
private Date date;
private int id;
public Advertisement() {
super();
}
// getters and setters
}
AdvertisementDetail has its own id, a description and an id the Advertisementit belongs to
public class AdvertisementDetail {
private String description;
private int id;
private int idAdvertisement;
public AdvertisementDetail() {
super();
}
// getters and setters
}
the MyBatis-mapper could look like this. #Param is not used, so the properties are accessed direct.
#Mapper
public interface AdvertisementMapper {
#Insert("INSERT INTO tb_ads (date) (#{date, jdbcType=DATE})")
#SelectKey(
before = false,
keyColumn = "ID",
keyProperty = "id",
resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" })
void insertAdvertisement(
Advertisement ad);
#Insert("INSERT INTO tb_ads_details (idAdvertisement, description) (#{idAdvertisement, jdbcType=INTEGER}, #{description, jdbcType=VARCHAR})")
#SelectKey(
before = false,
keyColumn = "ID",
keyProperty = "id",
resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" })
void insertAdvertisementDetail(
AdvertisementDetail adDetail);
}
the DataAccessObject (DAO) could look like this
#Component
public class DAOAdvertisement {
#Autowired
private SqlSessionFactory sqlSessionFactory;
public DAOAdvertisement() {
super();
}
public void save(
final Advertisement advertisement) {
try (SqlSession session = this.sqlSessionFactory.openSession(false)) {
final AdvertisementMapper mapper = session.getMapper(AdvertisementMapper.class);
// insert the advertisement (if you have to)
// its new generated id is received via #SelectKey
mapper.insertAdvertisement(advertisement);
for (final AdvertisementDetail adDetail : advertisement.getAdDetails()) {
// set new generated advertisement-id
adDetail.setIdAdvertisement(advertisement.getId());
// insert adDetail
// its new generated id is received via #SelectKey
mapper.insertAdvertisementDetail(adDetail);
}
session.commit();
} catch (final PersistenceException e) {
e.printStackTrace();
}
}
}
What Chris wrote about inability to get ids in the foreach is correct. However there is a way to implement id fetching in mapper without the need to do it externally. This may be helpful if you use say spring and don't have a separate DAO layer and your mybatis mappers are the Repository.
You can use default interface method (see another tutorial about them) to insert the list of items by invoking a mapper method for single item insert and single item insert method does the id selection itself:
interface ItemMapper {
#Insert({"insert into myitem (item_column1, item_column2, ...)"})
#SelectKey(before = false, keyColumn = "ID",
keyProperty = "id", resultType = Integer.class,
statement = { "SELECT LAST_INSERT_ID()" } )
void saveItem(#Param("item") Item item);
default void saveItems(#Param("items") List<Item> items) {
for(Item item:items) {
saveItem(item);
}
}
MyBatis can assign generated keys to the list parameter if your DB/driver supports multiple generated keys via java.sql.Statement#getGeneratedKeys() (MS SQL Server, for example, does not support it, ATM).
The following example is tested with MySQL 5.7.27 + Connector/J 8.0.17 (you should include version info in the question).
Be sure to use the latest version of MyBatis (=3.5.2) as there have been several spec changes and bug fixes recently.
Table definition:
CREATE TABLE le tb_ads_details (
id INT PRIMARY KEY AUTO_INCREMENT,
description VARCHAR(32)
)
POJO:
private class AdsDetailsEntity {
private int id;
private String description;
// getters/setters
}
Mapper method:
#Insert({
"<script>",
"INSERT INTO tb_ads_details (description) VALUES",
"<foreach item='detail' collection='adsDetails' separator=','>",
" (#{detail.description})",
"</foreach>",
"</script>"
})
#Options(useGeneratedKeys = true, keyProperty="adsDetails.id", keyColumn="id")
void saveAdsDetails(#Param("adsDetails") List<AdsDetailsEntity> adsDetails);
Note: You should use batch insert (with ExecutorType.BATCH) instead of multi-row insert (=<foreach/>) when inserting a lot of rows.
Current project I used Spring data jpa and spring boot, most time I found it's very convinence until meet this requirement, I found hard to deal.
That is I have Buyer Entity and Supplier Entity and the middle Entity(BuyerSupplier), if I'd like to search Buyer by phone or name or both or all, I have to supply many methods, like below:
if(phone!=null&&name!=null)
List<Buyer> findByPhoneAndName(phone,name)
else if(phone!=null)
List<Buyer> findByPhone(phone)
else if(name!=null)
List<Buyer> findByName(name)
else
List<Buyer> findAll()
Obviously, above code is very bad.And actually, my business logic is more complex, meantime I want to search buyer belong to a special Supplier and maybe a special status buyer. Corresponding sql like below:
select b.* from buyer b, buyer_supplier bs
where b.id = bs.buyer_id
and bs.supplier_id = 1
and bs.stauts = 'Activated'
and b.name like '%foo%'
and b.phone like '%123%'
and I also want to build dynamic sql, like below:
select b.* from buyer b, buyer_supplier bs
where b.id = bs.buyer_id
if(name != null)
and b.name like '%foo%'
if(phone!=null)
and b.phone like '%123%'
if(supplierId>0)
and b.supplier_id = 1
if(status!=null)
and bs.stauts = 'Activated'
Could anyone give me some code sample or documents could teach me how to implement my above target?
IMO, you still need to write code like if(phone!=null&&name!=null).But you can enscape the code,and write once.
First, you should know the Specification<T> that support the dynamic query well.Just paste my code below.
Specification<Goods> spec = new Specification<Goods>() {
#Override
public Predicate toPredicate(Root<Goods> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate predicate = cb.conjunction();
if (goodsSearch.getGoodsClassId() != -1) {
predicate = cb.and(predicate, cb.equal(root.get("goodsClassId"), goodsSearch.getGoodsClassId()));
}
return query.where(predicate).getRestriction();
}
};
return goodsDao.findAll(spec, pageable);
ok,you can use lambda instead of anonymous class.Then you can just enscape the Specification<T>, put the code like if(phone!=null&&name!=null) in it.
the sample code:
public Specification<T> term(String fieldName, Operator operator, Object value) {
return new Specification<T>() {
#Override
public Predicate toPredicate(Root<T> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder builder) {
if (value == null) {
return null;
}
Path expression = null;
// if a.b, then use join
if (fieldName.contains(".")) {
String[] names = StringUtils.split(fieldName, ".");
expression = root.get(names[0]);
for (int i = 1; i < names.length; i++) {
expression = expression.get(names[i]);
}
} else {
expression = root.get(fieldName);
}
// the different operation
switch (operator) {
case EQ:
return builder.equal(expression, value);
case NE:
return builder.notEqual(expression, value);
case LT:
return builder.lessThan(expression, (Comparable) value);
case GT:
return builder.greaterThan(expression, (Comparable) value);
case LIKE:
return builder.like((Expression<String>) expression, "%" + value + "%");
case LTE:
return builder.lessThanOrEqualTo(expression, (Comparable) value);
case GTE:
return builder.greaterThanOrEqualTo(expression, (Comparable) value);
case BT:
List<Object> paramList = (List<Object>) value;
if (paramList.size() == 2) {
return builder.between(expression, (Comparable) paramList.get(0), (Comparable) paramList.get(1));
}
case IN:
return builder.in(expression).value(value);
default:
return null;
}
}
};
}
I did not write the sample code if(phone!=null&&name!=null) in it.
I'm want to create a specification that matches a group id of a user object against a list of ids. I was thinking about using isMember (like in the code) but the method won't take the list.
public static Specification<User> matchCompanyIdsList(final List<Long> groupIds){
return new Specification<User>() {
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder){
final Path<Group> group = root.<Group> get("group");
return builder.isMember(company.<Long>get("id"), companyIds);
}
};
}
If I'm off, the how would I do it otherwise?
Do you want to create a specification that matches all users that have group id, which are in the groupsIds list?
If so, you could use something like this (Which will use SQL IN clause):
public static Specification<User> matchCompanyIdsList(final List<Long> groupIds){
return new Specification<User>() {
public Predicate toPredicate(Root<User> root, CriteriaQuery<?> query, CriteriaBuilder builder){
final Path<Group> group = root.<Group> get("group");
return group.in(groupIds);
}
};
}
lamba way
with org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor to generate the underscore classes
static Specification<User> hasRoles(List<String> roles) {
return (root, query, cb) -> {
query.distinct(true);
Join<User, Account> joinUserAccount = root.join(User_.account);
Join<Account, AccountRole> acctRolesJoin = joinUserAccount.join(Account_.accountRoles);
Join<AccountRole, Role> rolesJoin = acctRolesJoin.join(AccountRole_.role);
return rolesJoin.get(Role_.name).in(roles);
};
}
I have following query:
SELECT DISTINCT *
FROM Projekt p
WHERE p.bewilligungsdatum = to_date('01-07-2000', 'dd-mm-yyyy')
but i have problems to build the conditions. Here my code:
condition = criteriaBuilder.equal((Expression<String>) projekt.get(criterion), "to_date('" + projektSearchField + "', 'dd-mm-yyyy')");
this generate following:
SELECT DISTINCT *
FROM Projekt p
WHERE p.bewilligungsdatum = 'to_date('01-07-2000', 'dd-mm-yyyy')'
and ufcorse doesn't work. Which method should i use for date comparision (or how to remove the outer ' chars in the pattern part)?
why don't you try to work with parameters like that. Then you can do the String->Date conversion in java and pass a real java.util.Date to the database.
EntityManager em; // initialized somewhere
Date datum; // initialized somewhere
...
String queryString = "SELECT p "
+ "FROM Projekt p"
+ "WHERE p.bewilligungsdatum = :datum";
Query query = em.createQuery(queryString)
query.setParameter("datum", datum);
List<Projekt> projekte = query.getResultList();
This is the way to stay DB independent because your are not using the specific to_date function
viele Grüße aus Bremen ;o)
This should work too, by passing a date as parameter of a restriction
Date datum; // initialized somewhere
CriteriaQuery query = ...
query.add(Restrictions.eq( "bewilligungsdatum ", datum );
...
Sorry. I had the hibernate CriteriaQuery in mind.
Then try via the CriteriaBuilder somthing like that
Date datum; // initialized somewhere
...
final CriteriaQuery<Projekt> query = criteriaBuilder.createQuery(Projekt.class);
final Root<Projekt> projekt = query.from(Projekt.class);
Predicate condition = criteriaBuilder.equals(projekt.get("bewilligungsdatum"),datum);
query.where(condition)
I did not use this before, so have a try on your own
you can use https://openhms.sourceforge.io/sqlbuilder/ ,then use the Condition like
Object value1 = hire_date
Object value2 = new CustomObj("to_date('2018-12-01 00:00:00','yyyy-MM-dd HH:mm:ss')")
//CustomObj
public class CustomObj extends Expression {
private Object _value;
public CustomObj(Object value) {
_value = value;
}
#Override
public boolean hasParens() {
return false;
}
#Override
protected void collectSchemaObjects(ValidationContext vContext) {
}
#Override
public void appendTo(AppendableExt app) throws IOException {
app.append(_value);
}
}
BinaryCondition.greaterThan(value1, value2, inclusive);
the sql like hire_date >= to_date('2011-02-28 00:00:00','yyyy-MM-dd HH:mm:ss'))