We have a Media object:
public class Media implements Serializable {
#Id
#GeneratedValue(strategy = IDENTITY)
#Column(insertable = false, updatable = false)
private Long id;
// other attributes
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "channelId", referencedColumnName = "id")
private Channel channel;
// getters, setters, hashCode, equals, etc.
}
The eager fetch of the channel parent works in regular repository methods, but not when using a Specification.
Here's the Specification:
public class MediaSpecs {
public static Specification<Media> search(final Long partnerId, final Integer width, final Integer height,
final String channelType) {
return new Specification<Media>() {
#Override
public Predicate toPredicate(Root<Media> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Predicate restrictions = cb.equal(root.get("archived"), false);
// other restrictions are and-ed together
if (channelType != null) {
Join<Media, ChannelType> join = root.join("channel").join("orgChannelType").join("type");
restrictions = cb.and(cb.equal(join.get("type"), channelType));
}
return restrictions;
}
};
}
The "search" spec works correctly when channelType is specified, so the join is working. How do I specify that the joins should be eagerly fetched?
I tried adding
Fetch<Media, ChannelType> fetch = root.fetch("channel").fetch("orgChannelType").fetch("type");
Then Hibernate throws an exception:
org.hibernate.QueryException: query specified join fetching, but the owner of the fetched association was not present in the select list [FromElement{explicit,not a collection join,fetch join,fetch non-lazy properties,classAlias=generatedAlias4 ...
How do I add the associations to the select list?
Thanks.
I think you have problem with count query. Usually the specification is use for data query a and count query. And for count query there is no "Media". I use this workaround :
Class<?> clazz = query.getResultType();
if (clazz.equals(Media.class)) {
root.fetch("channel");
}
This use fetch only for data query a not for count query.
For example:
public Predicate toPredicate(Root<Person> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
if (Long.class != query.getResultType()) {
root.fetch(Person_.addresses);
}
return cb.conjunction();
}
Related
I have three tables created with Hibernate. Product is the parent while barcodes is a collection end price is a child (one to many) of products.
#NotBlank
private String ref;
#ElementCollection(fetch = FetchType.EAGER)
#CollectionTable(name = "products_barcodes")
#Fetch(FetchMode.SELECT)
private List<String> barcodes;
#OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
private List<PriceEntity> prices;
I'm trying to query starting from the child (Price). I was able to query on a string but now i would like to query on the collection s element.
Specifications<PriceEntity> specifications = where(hasTenant(tid));
if (isNotBlank(ref)) {
specifications = specifications.and(hasRef(ref));
}
if (isNotBlank(barcode)) {
specifications = specifications.and(hasBarcode(barcode));
}
/*********************************/
public static Specification<PriceEntity> hasRef(final String ref) {
return new Specification<PriceEntity>() {
#Override
public Predicate toPredicate(Root<PriceEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.<PriceEntity>get("parent").get("ref"), ref);
}
};
}
public static Specification<PriceEntity> hasBarcode(final String barcode) {
return new Specification<PriceEntity>() {
#Override
public Predicate toPredicate(Root<PriceEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
return criteriaBuilder.equal(root.<PriceEntity>get("parent").get("barcodes"), barcode);
}
};
}
how would you write the specification? The one above is not working, i get this exception at runtime:
"IllegalArgumentException: Parameter value [8003921360408] did not match expected type [java.util.Collection (n/a)]"
Thanks
Question solved by Thomas s comment.
For collections criteriaBuilder.isMember should be used.
I have an entity like the following were I use #Formula to populate clientId from other tables.
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Formula("(SELECT cl.id from failure f " +
"INNER JOIN pvplant p ON f.pv_plant_id = p.id " +
"INNER JOIN company co ON p.company_id = co.id "+
"INNER JOIN client cl ON co.client_id = cl.id "+
"WHERE f.id = id) ")
public Integer clientId;
}
while CrudRepository<Failure,Integer> JPA method getByClientId works fine I am trying to make something more dynamic for filtering using a Map of keys and values with Specification and CriteriaBuilder.
public MySpecifications {
public static Specification<Failure> equalToEachColumn(HashMap<String,Object> map) {
return new Specification<Failure>() {
#Override
public Predicate toPredicate(Root<Failure> root, CriteriaQuery<?> cq, CriteriaBuilder builder) {
return builder.and(root.getModel().getAttributes().stream().map(a ->
{
if (map.containsKey(a.getName())) {
Object val = map.get(a.getName());
return builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString()));
}
return builder.disjunction();
}
).toArray(Predicate[]::new)
);
}
};
}
}
When I am passing id in the HashMap it works fine but when I have clientId it doesn't send anything back. It is interesting that getAttributes() actually returns clientId but it seems that builder.equal(root.<Integer>get(a.getName()), Integer.parseInt(val.toString())) is false and not true
This is how I am using the Specification:
failureRepository.findAll(Specifications.where(MySpecifications.equalToEachColumn(map)));
Am I missing something?
Thanks in advance!
I wouldn't expect this to work however you could make it work by using a database view as an alternative to #Formula and mapping the entity across the table and view using #SecondaryTable.
//failures_client_vw is a 2 column db view: failure_id, client_id
#Table(name = "failures")
#SecondaryTable(name = "failures_client_vw",
pkJoinColumns = #PrimaryKeyJoinColumn(name = "failure_id"))
#Entity
public class Failure {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
public int id;
public String name;
#ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH} )
public PVPlant pvPlant;
#Column(name = "client_id", table = "failures_client_vw")
public Integer clientId;
}
You can then query clientId as you would any other property.
https://en.wikibooks.org/wiki/Java_Persistence/Tables#Multiple_tables
The actual problem was that I was using
builder.disjunction()
in the and() which creates 0=1 false predicates.
When I replaced it with
builder.conjunction() (which creates 1=1 true predicates)
in my code it worked fine. So #Formula properties behave as native ones to the table and it seems there is no need for SecondaryTable and a new View. Apparently in my earlier tests I used an entity that had just an id in its class and when I added clientId it misled me to believe that #Formula properties don't work, while it was the disjunction from the id that broke clientId
I have 2 entities CallRecords and CallRecordOperators with one-to-many relation as given below
public class CallRecords {
#Id
#Column(name = "id", unique = true)
private String id;
#Column(columnDefinition = "varchar(255) default ''")
private String callerNumber = "";
#OneToMany(mappedBy="callrecord")
private List<CallRecordOperators> callRecordOperators = new ArrayList<CallRecordOperators>();
//getter setters
}
public class CallRecordOperators {
#Id
#Column(name = "id", length = 50, unique = true, nullable = false, insertable = false, updatable = false)
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#JsonIgnore
#ManyToOne
#JoinColumn(name = "callRecordId")
private CallRecords callrecord;
#ManyToOne
#JoinColumn(name = "operatorId")
private Operator operator;
#Formats.DateTime(pattern = "yyyy-MM-dd HH:mm:yy")
#Column(columnDefinition = "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP")
private Date startTime = new Date();
#Column(columnDefinition = "varchar(100) default ''")
private String dialStatus;
//getter setter
}
So if the user ask for all "CallRecords" data I also have to give "CallRecordOperators" as they are related.
Current code for Mapper and DTOs
#Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface CallRecordsMapper {
CallRecordsMapper INSTANCE = Mappers.getMapper(CallRecordsMapper.class);
#Mapping(source="callRecordOperators",target = "operators")
CallRecordsDto callRecordsToCallRecordsDto(CallRecords callRecords);
public abstract CallRecordOperatorsDto toTarget(CallRecordOperators source);
List<CallRecordsDto> callRecordsToCallRecordsDtos(List<CallRecords> callRecords);
}
public class CallRecordsDto {
private String callerNumber;
private List<CallRecordOperatorsDto> operators;
//getter setters
}
public class CallRecordOperatorsDto {
private String callRecordsId;
private String operatorId;
private String operatorName;
private String currentTime;
// getter setter
}
But for above code I am getting
{
"callerNumber": "9898989898",
"operators": [{
"callRecordsId": null,
"operatorId": null,
"operatorName": null,
"currentTime": null
}, {
"callRecordsId": null,
"operatorId": null,
"operatorName": null,
"currentTime": null
}]
}
the values of operator array are null. what could be he issue?
It seems your are lacking the mappings from CallRecordOperators to CallRecordOperatorsDto:
#Mapper
public interface CallRecordsMapper {
CallRecordsMapper INSTANCE = Mappers.getMapper(CallRecordsMapper.class);
#Mapping(source="callRecordOperators",target = "operators")
CallRecordsDto callRecordsToCallRecordsDto(CallRecords callRecords);
#Mapping(target = "callRecordsId", source = "callrecord.id")
#Mapping(target = "operatorId", source = "operator.id")
#Mapping(target = "operatorName", source = "operator.name")
#Mapping(target = "currentTime", source = "startTime")
CallRecordOperatorsDto callRecordOperatorsToDto(CallRecordOperators source);
}
When you do a Hibernate query of A elements, you can fetch the related B elements of the bs collection using different strategies. Some of them are:
If you use HQL to construct your queries, you can do a JOIN FETCH or LEFT JOIN FETCH to populate the bs collection:
String hql = "SELECT DISTINCT a FROM " + A.class.getName()
+ " a LEFT JOIN FETCH a.bs WHERE ...";
This query will load all data using a single SQL query.
Use eager fetching of the bs collection, changing the #OneToMany annotation:
#OneToMany(fetch=FetchType.EAGER)
private List<B> bs;
In this case, when you run a query of A elements, a SQL query will be launched to retrieve the A data, and for each A object in the result, a SQL query will be executed to load the corresponding bs collection.
If you use Criteria to build the query, you can change the fetch mode of the bs collection in a way similar to the HQL JOIN FETCH:
Criteria c = session.createCriteria(A.class);
c.setFetchMode("bs", FetchMode.JOIN);
c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
How about switching to a slightly different approach that also performs better? By using Blaze-Persistence Entity Views you can define your mapping directly on the DTO classes and apply that onto a query builder to generate efficient queries that perfectly fit your DTO structure.
#EntityView(CallRecords.class)
public interface CallRecordsDto {
// The id of the CallRecords entity
#JsonIgnore
#IdMapping("id") String getId();
String getCallerNumber();
#Mapping("callRecordOperators")
List<CallRecordOperatorsDto> getOperators();
}
#EntityView(CallRecordOperators.class)
public interface CallRecordOperatorsDto {
// The id of the CallRecordOperators entity
#JsonIgnore
#IdMapping("id") Long getId();
#Mapping("callrecord.id")
String getCallRecordId();
#Mapping("operator.id")
String getOperatorId();
#Mapping("operator.name")
String getOperatorName();
#Mapping("startTime")
String getCurrentTime();
// Whatever properties you want
}
See how you can map the entity attributes right in your DTOs? And here comes the code for querying
EntityManager entityManager = // jpa entity manager
CriteriaBuilderFactory cbf = // query builder factory from Blaze-Persistence
EntityViewManager evm = // manager that can apply entity views to query builders
CriteriaBuilder<User> builder = cbf.create(entityManager, CallRecords.class)
.where("callerNumber").eq("123456789");
List<CallRecordsDto> result = evm.applySetting(
builder,
EntityViewSetting.create(CallRecordsDto.class)
).getResultList();
Note that this will roughly generate the following optimized query
SELECT
c.id,
c.callerNumber,
o.callrecord.id,
o.id,
o.startTime,
op.id,
op.name
FROM CallRecords c
LEFT JOIN c.callRecordOperators o
LEFT JOIN o.operator op
WHERE c.callerNumber = :param_1
I am trying to create a dynamic query with Specification with two entities which have bidirectional relation. The entities are:
#Entity
#Table("SUPPLIERS")
public class Supplier implements Serializable {
#Id
Column("ID")
private Long id;
#Id
Column("COMPANY_ID")
private Long companyId;
}
#Entity
#Table("EMPLOYEES")
public class Employee implements Serializable {
#Id
private Long id;
#ManyToOne
#JoinColumns({
#JoinColumn(name = "FIRM_ID", referencedColumnName = "ID"),
#JoinColumn(name = "FIRM_COMPANY_ID", referencedColumnName = "COMPANY_ID")
})
private Supplier supplier;
}
When I want to select employees based on their supplier,
return new Specification<Employee>() {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
Long[] supplierCodes = {1L, 2L};
Subquery<Supplier> supplierBasicSubquery = query.subquery(Supplier.class);
Root<Supplier> supplierBasicRoot = supplierBasicSubquery.from(Supplier.class);
Join<Employee, Supplier> sqTfV = root.join("supplier", JoinType.INNER);
supplierBasicSubquery.select(sqTfV).where(sqTfV.<Long>get("id").in(supplierCodes));
return root.<Supplier>get("supplier").in(supplierBasicSubquery);
}
};
When its executed, it generates SQL like:
SELECT ....
FROM EMPLOYEES E
INNER JOIN ....
WHERE (E.FIRM_ID, E.FIRM_COMPANY_ID) in
(SELECT (s.ID, s.COMPANY_ID) FROM SUPPLIERS WHERE SUPPLIER.ID in (1, 2))
As you can see, the inner select columns are surrounded by parenthesis which causes Oracle to throw exception:
java.sql.SQLSyntaxErrorException: ORA-00920: invalid relational operator
How can I fix this issue, any suggestions?
Thanks a lot.
It's possible mapping custom native/named queries to entities? I have something like this
NamedQueries({
NamedQuery(name = "StateBo.findByCountry", query = "SELECT state FROM StateBo state WHERE state.country.id = ?"),
NamedQuery(name = "StateBo.showIdfindByCountry", query = "SELECT state.id FROM StateBo state WHERE state.country.id = ?")
})
#Table(name = "STATE")
#Entity(name = "StateBo")
public class StateBo extends BaseNamedBo {
private static final long serialVersionUID = 3687061742742506831L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "STATE_ID")
private Long id;
#Column(name = "ISO_CODE")
private String isoCode;
#ManyToOne
#JoinColumn(name = "COUNTRY_ID")
private CountryBo country;
// getters and setters ...
}
I have my method to call Native/Named queries like this.
#Override
public List<E> executeQuery(String queryName, List<Object> criteria) {
TypedQuery<E> query = entityManager.createNamedQuery(queryName, entityClass);
Integer argumentPosition = 1;
if ( (criteria != null) && (criteria.size() > 0) ){
for(Object object : criteria) {
query.setParameter(argumentPosition, object);
argumentPosition++;
}
}
return (List<E>) query.getResultList();
}
When I call the StateBo.findByCountry the result is mapped to StateBo, but if I call StateBo.showIdfindByCountry the result is not mapped to StateBo because I'm only selected on the query the state.id instead of the fields on the table.
I don't want to select all the fields of the STATE table, I only want in this case the state.id, but when I customize my native query, the result is not mapped to StateBo instead of this, the result is a Long type.
My question is, Is possible map to an Entity the result of StateBo.showIdfindByCountry? I case that I have more fields like state.isoCode, is possible map to StateBo, the custom query? or only is possible if I return all the fields from the query, like the first query StateBo.findByCountry
It is possible, but as JB Nizet said - "your collegues will suffer from such a design decision".
Anyway, in order to do that you should create custom constructor in your entity class. This constructor should accept Long argument and assign it to id field of your entity class.
Then you should change your query to include NEW keyword followed by full qualified entity class name as below:
SELECT NEW your.package.StateBo(sb.id)
FROM StateBo sb
WHERE state.country.id = ?
Please note that all entities retreived from database in such a way will not be managed by persistence context.