I have a Table
Products ( ProductId, Name ) and
ProductPrices (ProductId, Market, Price)
ProductPrices has a compositeKey (ProductId, Market). For a given Market, a Product has 0..1 Prices in that Market.
First approach #Formula
The Market is known at runtime, and can possibly be changed per request.
In an first attempt to model the ProductEntity I took an #Formula annotation, like so:
#Entity
#Table(...)
public class Product {
#Id
private int ProductId;
private String name;
#Formula("(SELECT TOP 1 Price FROM ProductPrices p WHERE p.ProductId = ProductId AND p.Market='Berlin')")
private double price;
}
But obviously, the market is then hard-compiled as annotations need to be static final Strings. [ so no #Formula("..." + getCurMarket() ) ].
Second approach, #OneToMany
Take a separate entity class for the prices, and reference them in the product entity as:
#OneToMany(mappedBy = "product")
private List<Price> price;
In a getPrice(), I could always return the first entry (there will never be more...) or nothing if the list is empty.
I then want to create a Predicate/Specification to use from within the ProductService. Example:
public static Specification<Product> marketEquals(final String market) {
return new Specification<Product>() {
#Override
public Predicate toPredicate(Root<Product> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
CriteriaQuery<String> q = cb.createQuery(String.class);
Root<Price> price = q.from(Price.class);
return price.get("Market").in("Berlin");
}
};
}
However, that only results in a (and I tried writing "market", "Market", ...)
org.hibernate.hql.internal.ast.QuerySyntaxException: Invalid path: 'generatedAlias1.market' [select generatedAlias0 from ...backend.entities.Product as generatedAlias0 where generatedAlias1.market in (:param0)]
Third approach, Hibernate/JPA Filter
This time, I write in the product entity
#OneToMany(mappedBy = "product")
#Filters( {
#Filter(name="marketFilter", condition="Market = :market")
} )
private List<Price> price;
Again, I want to fill this filter in the ProductService, but I cannot gelt hold of the CurrentSession. I tried the Spring-way, adding an #Autowired private SessionFactory sessionFactory; and configuring it through
Filter filter = sessionFactory.getCurrentSession().enableFilter("marketFilter");
filter.setParameter("market", "Berlin" );
but I cannot get hold of the right context, as org.springframework.beans.factory.BeanCreationException: Could not autowire field: private org.hibernate.SessionFactory
Who could advise on how to model the database schema as entities, or could point working solutions to approach 2 and 3 ? Thanks!
Second approach is actually correct.Try it changing your entities and creteria query.
Products table:
#Entity
#Table(...)
public class Product {
#Id
private int ProductId;
private String name;
#OneToMany(mappedBy = "products")
private List<ProductPrices> ProductPrices;
}
ProductPrices table:
#Entity
#Table(...)
public class ProductPrices {
#Id
private int ProductPriceId;
private String market;
private double price;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_id") //foreign key reference
private Product products
}
ProductService:
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Product> qry = cb.createQuery(Product.class);
Root<Product> root = qry.from(Product.class);
Join<Product, ProductPrices> price = root.join("ProductPrices");
List<Predicate> conditions = new ArrayList<>();
conditions.add(cb.equal(price.get("products"), "Berlin"));
TypedQuery<Product> typedQuery = em.createQuery(qry
.select(root)
.where(conditions.toArray(new Predicate[] {}))
.orderBy(cb.asc(root.get("Berlin")))
.distinct(true)
);
return typedQuery;
Related
I am having trouble to converting the following postgresql query (with a join and a group by) to JPA criteria API for a Spring Boot, JPA, Hibernate application:
select u.id, u.full_name, count(*) project_applications_count from users u
join project_applications pa on pa.created_by = u.id
group by u.id, u.full_name
having count(*) >= 1 and count(*) <= 5
The tables look like this:
create table project_applications (
id serial primary key,
...
city_id integer not null references cities (id),
created_by integer not null references users (id)
);
create table users (
id serial primary key,
...
full_name varchar(100) not null
);
And the entities look like this:
#Entity
#Table(name = "project_applications")
public class ProjectApplication {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#ManyToOne
#JoinColumn(name = "created_by")
private User createdBy;
...
}
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "full_name")
private String fullName;
...
}
I tried searching online for a solution but every exemple I found was using either a join or group by, but not both.
Using #akortex's idea with projections, I think something like this should work:
public class UserSummary {
private Long id;
private String fullName;
private Long count;
public UserSummary() {
}
public UserSummary(Long id, String fullName, Long count) {
this.id = id;
this.fullName = fullName;
this.count = count;
}
... (getters and setters)
}
public List<UserSummary> getSummaries(Integer minProjectAppsCount, Integer maxProjectAppsCount) {
CriteriaBuilder cb = _entityManager.getCriteriaBuilder();
CriteriaQuery<UserSummary> query = cb.createQuery(UserSummary.class);
Root<ProjectApplication> projectApp = query.from(ProjectApplication.class);
Join<ProjectApplication, User> userJoin = projectApp.join("createdBy", JoinType.INNER);
query.multiselect(userJoin.get("id"), userJoin.get("fullName"), cb.count(projectApp))
.groupBy(userJoin.get("id"), userJoin.get("fullName"));
List<Predicate> predicates = new ArrayList<>();
if (minProjectAppsCount != null ) {
Predicate p = cb.ge(cb.count(projectApp), minProjectAppsCount);
predicates.add(p);
}
if (maxProjectAppsCount != null ) {
Predicate p = cb.le(cb.count(projectApp), maxProjectAppsCount);
predicates.add(p);
}
query.having(predicates.toArray(new Predicate[0]));
return _entityManager.createQuery(query).getResultList();
}
You could potentially look into projections in order to achieve what you want.
For example consider the following projection and repository:
#Data
#AllArgsConstructor
public class ProjectApplicationSummary {
private Long id;
private String fullName;
private Long count;
}
And:
#Repository
public interface ProjectApplicationRepository extends JpaRepository<ProjectApplication, Long> {
#Query(
"""
SELECT new com.example.springdemo.entities.ProjectApplicationSummary(u.id, u.fullName, count(pa))
FROM User u, ProjectApplication pa
GROUP BY u.id, u.fullName
"""
)
List<ProjectApplicationSummary> getSummaries();
}
You will most likely need to tweak the query a bit (which revolves experimenting with JPQL) but other than that, the basic idea is there.
I'm not sure in my solution, but it should be similar. I took an idea from here. Maybe it helps you to resolve your problem.
public static Specification<User> getUsers() {
return Specification.where((root, query, criteriaBuilder) -> {
CriteriaQuery<User> criteriaQuery = criteriaBuilder.createQuery(User.class);
Subquery<Long> subQuery = criteriaQuery.subquery(Long.class);
Root<ProjectApplication> subRoot = subQuery.from(ProjectApplication.class);
subQuery
.select(criteriaBuilder.count(subRoot))
.where(criteriaBuilder.equal(root.get("id"), subRoot.get("createdBy").get("id")));
query
.multiselect(criteriaBuilder.construct(root.get("id"), root.get("fullName")))
.groupBy(root.get("id"), root.get("fullName"))
.having(criteriaBuilder.and(
criteriaBuilder.greaterThanOrEqualTo(subQuery.getSelection(), 1L),
criteriaBuilder.lessThanOrEqualTo(subQuery.getSelection(), 5L)));
return query.getRestriction();
});
}
I am using JPA repositories. I have a Company table with one-to-many relationship with Employee table. I want to get list of companies with employee count who's last name is x. I am trying to use multiselect but the count is not getting populated. Here is what I am trying to do.
public class CompanyEntity {
#Id
private UUID id;
#Column
private String name;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "company", orphanRemoval = true)
private List<EmployeeEntity> employees;
#Transient
private Integer employeeCount;
}
...
#Autowired
private CompanyRepository repo;
...
public Page<CompanyEntity> findAllCompanies(Pageable pageable, String lastname) {
Specification<CompanyEntity> specification = (root, criteriaQuery, criteriaBuilder) -> {
Join employees = root.join("employees");
criteriaQuery.groupBy(root);
criteriaQuery.multiselect(root, criteriaBuilder.count(employees).alias("employeeCount"));
Predicate predicate = criteriaBuilder.equal(employees.get("lastname"), lastname);
return predicate;
};
Page<EmployeeEntity> page = repo.findAll(specification, pageable);
return page;
}
Where is the count value supposed to go?
A Specification just configures the where part of a query.
If you want to control the full query including the select list do a custom method implementation and use the EntityManager directly.
I ended up writing a query on repository method.
#Query("select c, count(e.id) from company c left join employee e on c.id = e.company.id where e.lastname = :lastname group by c.id")
Page<Object[]> findAll(#Param("lastname") String lastname, Pageable pageable);
Here Object[0] contains CompanyEntity and Object[1] contains count as Long.
I have got three classes as mentioned below. I am trying to create a specification to filter data where there is a match in the linked table.
public class Album {
private Long id;
private List<AlbumTag> albumTags;
}
public class Tag {
private Long id;
private String category;
}
public class AlbumTag{
private Long id;
private Album album;
private Tag tag;
}
In the schema given above what I am trying to find is a list of all albums from Album table with the link in AlbumTag. The SQL that I want to achieve, doesn't have to be same, is below
select *
from Album A
where (A.Id in (select [AT].AlbumId
from AlbumTag [AT]))
What I have tried so far which is not working, of course, is below
public class AlbumWithTagSpecification implements Specification<Album> {
#Override
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
final Subquery<Long> personQuery = cq.subquery(Long.class);
final Root<Album> album = personQuery.from(Album.class);
final Join<Album, AlbumTag> albumTags = album.join("albumTags");
personQuery.select((albumTags.get("album")).get("id"));
personQuery.where(cb.equal(album.get("id"), (albumTags.get("album")).get("id")));
return cb.in(root.get("id")).value(personQuery);
}
}
Using spring boot and spring data JPA, you can prefer entity relationship to fetch the data.
1.Annotate the domain class with the entity relationship which given below:
#Entity
#Table(name="Album")
public class Album {
#Id
#Column(name="id")
private Long id;
#OneToMany(targetEntity = AlbumTag.class, mappedBy = "album")
private List<AlbumTag> albumTags;
//getter and setter
}
#Entity
#Table(name="Tag")
public class Tag {
#Id
#Column(name="id")
private Long id;
#Column(name="category")
private String category;
//getter and setter
}
#Entity
#Table(name="AlbumTag")
public class AlbumTag{
#Id
#Column(name="id")
private Long id;
#ManyToOne(optional = false, targetEntity = Album.class)
#JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
private Album album;
#ManyToOne(optional = false, targetEntity = Tag.class)
#JoinColumn(name = "id", referencedColumnName="id", insertable = false, updatable = false)
private Tag tag;
//getter and setter
}
2.use the spring data to fetch the details using the below:
Album album = ablumRepository.findOne(1); // get the complete details about individual album.
List<AlbumTag> albumTags = ablum.getAlbumTags(); // get the all related albumTags details for particular album.
I hope this will help you to solve it.
Subqueries in JPA only really work with CriteriaBuilder.exists() so i would try:
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
final Subquery<Long> subQuery = cq.subquery(Long.class);
final Root<AlbumTag> albumTag = subQuery.from(AlbumTag.class);
// it doesn't really matter what we select
subQuery.select(cb.literal(1));
subQuery.where(cb.equal(root.get("id"), (albumTag.get("album")).get("id")));
return cb.exists(subQuery);
}
which is equivalent to
select *
from Album A
where exists(
select 1 from AlbumTag AT
where AT.AlbumId = A.Id
)
Well, I wouldn't go for in operation in this case - it just complicates the query and the specification. The problem you described is actually matter of joining records from Table A with related records from Table B so the query in your case would be like:
SELECT a from Album a join AlbumTag at on a.id = at.albumId - as you needed it will return all albums that have album tags. Inner join explained
So in your case I would create this "factory" method that would create for you this specification.
public static Specification<Album> withTags() {
return new Specification<Album>() {
#Override
public Predicate toPredicate(Root<Album> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
return root.join("albumTags").getOn();
}
};
}
Also I would suggest you to have a look at static metamodel library from hibernate - link to introduction. It generates for you static model from your entity classes that helps you avoid creating queries/specifications using hardcoded strings.
creteria query for join tables
CriteriaQuery<Album> query = cb.createQuery(Album.class);
Root<Album> album = query.from(Teacher.class);
Join<Album, AlbumTag> tag = teacher.join("id");
query.select(tag).where(cb.equal(album.get("album")));
List<Album> results = em.createQuery(query).getResultList();
for (Album al : results) {
System.out.println("album-->+al.get(name));
}
This looks like a classic many to many example. The three classes you have map directly to the tables you would expect in the database. JPA is an Object Relational Mapping (ORM) library which means we can structure the classes in a more OO style and map to the underlying relational database.
The AlbumTag class can be omitted and the #ManyToMany relationship added to both Album and Tag.
public class Album {
private Long id;
#ManyToMany
#JoinTable(name="AlbumTag",
joinColumns=
#JoinColumn(name="album", referencedColumnName="id"),
inverseJoinColumns=
#JoinColumn(name="tag", referencedColumnName="id"))
private List<Tag> tags;
}
public class Tag {
private Long id;
private String category;
#ManyToMany(mappedBy="tags")
private List<Album> albums;
}
To find albums by Tag you would first retrieve the Tag from the repository using something like findById(1l); or findByCategory("Rock"); and then simply call getAlbums() on the Tag object.
Note: One slight difference here is that the AlbumTag table would have only two columns (album and tag). The extra id column on AlbumTag is unnecessary since the combination of album and tag would be a unique id and you would never need to find by id in this table anyway.
Since you are using spring-data-jpa you should really take advantage of the features it provides.
My first question is related to your entity classes. I do not understand why is it necesary to store a list of album tags in the album class. Since you have a join table this information is reduntant.
Secondly you should adnotate your entity clases:
#Entity
public class Album {
#Id
#Column
private Long id;
}
#Entity
public class Tag {
#Id
#Column
private Long id;
#Column
private String category;
}
#Entity
#Table
public class AlbumTag{
#Id
#Column
private Long id;
#ManyToOne
#JoinColumn
private Album album;
#ManyToOne
#JoinColumn
private Tag tag;
}
Next you should create repositories for your entity classes.
interface AlbumRepository extends JpaRepository<Album, Long>{
#Query
("select DISTINCT(a) from AlbumTag at "+
"join at.album a "
"where at.tag is not null")
List<Album> findAlbumWithTag();
}
Then simply call the repository function which will return a list of albums which have at least one tag.
I have entity Person
#Entity(name = "Person")
public class Person {
#Id
#GeneratedValue
private Long id;
private String name;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "person")
private Set<Phone> phones=new HashSet<Phone>();
public Person() {
}
public Person(String name) {
this.name = name;
}
Ad entity Phone :
#Entity(name = "Phone")
public class Phone {
#Id
#GeneratedValue
private Long id;
#Column(name = "`number`")
private String number;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "person_id", nullable = false)
private Person person;
public Phone() {
}
They have one-to-many relation.
Now I want to build in jpa criteria such query:
select p.phones from person p join phone ph where p.name = :name;
So I want to extract Set<Phone> phones from Person entity where person's name is parameter.
I've written this jpa criteria query:
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
CriteriaQuery<Person> where = query.where(builder.equal(root.get("name"), "Mary Dick"));
CompoundSelection<Set> projection = builder.construct(Set.class, root.get("phones"));
where.select(projection); //compile error: The method select(Selection<? extends Person>) in the type CriteriaQuery<Person> is not applicable for the arguments (CompoundSelection<Set>)
}
But it gives compile error:
The method select(Selection<? extends Person>) in the type CriteriaQuery<Person> is not applicable for the arguments (CompoundSelection<Set>)
How is it correct? Do I need metamodel classes?
CompoundSelection<Y> construct(Class<Y> result, Selection<?>... terms)
This method is useful only when the query would involve certain projections which are not entirely encapsulated by a single entity class. If that is the case, first parameter would be the custom POJO class (with suitable constructor) with fields which corresponding to the select clause of the query.
In this case, the selection is already a part of the entity class. So, you can simply choose the fields you need.
CriteriaQuery<Person> query = builder.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
query.where(builder.equal(root.get("name"), "Mary Dick"));
query.select(root.get("phones"));
Above query will return a list of person. But if you are looking for just an iterable list of phones, try with a slightly different query.
select ph from phone ph join ph.person p where p.name = :name;
And its equivalent CriteriaQuery:
CriteriaBuilder builder = session.getCriteriaBuilder();
CriteriaQuery<Phone> query = builder.createQuery(Phone.class);
Root<Phone> root = query.from(Phone.class);
Join<Phone, Person> join = root.join(root.get("person"))
query.where(builder.equal(join.get("name"), "Mary Dick"));
How can the sql expression below be expressed using CriteriaBuilder?
select * from Ref where prac_id = (select prac_id from loc l join staff_loc sl where sl.loc = l.id and sl.pracstaff_id = 123)
Model Classes
#Entity
public class Ref {
private Long id;
private Prac prac;
}
#Entity
public class Loc {
Long id;
#ManyToOne
Prac prac;
#ManyToMany
Set<PracStaff> pracStaff;
}
#Entity
public class Prac {
Long id;
#OneToMany
Set<Loc> locs;
}
#Entity
public class PracStaff {
Long id;
#ManyToMany
Set<Loc> locs;
}
There's a join table that maps Loc to PracStaff; it has two columns: pracstaff_id and loc_id
A Loc can belong to only one Prac.
What I'm trying to get is all Ref objects that have a PracStaff with id 123 using CriteriaBuilder.
Here's the solution I got to work though I haven't tested it thoroughly. Using
Expression<Collection<PracStaff>>
to return the collection is what I was missing
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<Ref> criteriaQuery = criteriaBuilder.createQuery(Ref.class);
Root<Ref> from = criteriaQuery.from(Ref.class);
criteriaQuery.select(from);
Subquery<Prac> subquery = criteriaQuery.subquery(Prac.class);
Root<Loc> fromLoc = subquery.from(Loc.class);
Expression<Collection<PracStaff>> pracStaffInLoc = fromLoc.get("pracStaff");
subquery.where(criteriaBuilder.isMember({pracStaffObj}, pracStaffInLoc));
subquery.select(fromLoc.<Prac>get("prac"));
Path<Prac> specialist = from.get("{field in Ref class}");
Predicate p = criteriaBuilder.equal(specialist, subquery);