Hibernate Criteria One to Many issues - java

I am trying to use Hibernate Criteria api to fetch only the topics based on the USER_ID but have no idea how to do it using the criteria.
My Tables are "topic_users" (below)
and "topics" table (below)
I know how to do it using SQL, this would be something like:
SELECT TOPICNAME
FROM topic_users INNER JOIN topics on topic_users.TOPICS_TOPICS_ID = topics.TOPICS_ID
WHERE topic_users.USER_ID = 1
This will return all TOPICNAME of USER_ID 1 which is exactly what I want but how I can do this with Hibernate Criteria. So far I have this in my Repository class (see below) but this will only return a highly nested JSON array. I could loop through the objects, use a DTO and build my response or try the Hibernate createSQLQuery method that will let me call a native SQL statement directly (haven't tried that yet)...but I am trying to learn the Criteria so I hope anyone can answer my query.
#Repository("userTopicsDao")
public class UserTopicsDaoImpl extends AbstractDao<Integer, UserTopics>implements UserTopicsDao {
#Override
public List<UserTopics> findMyTopics(int userId) {
Criteria crit = createEntityCriteria();
crit.add(Restrictions.eq("userId", userId));
List<UserTopics> userTopicsList = (List<UserTopics>)crit.list();
return userTopicsList;
}
and my TOPIC_USERS Entity where I have mapped the TOPICS
#Entity
#Table(name="TOPIC_USERS")
public class UserTopics {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="TOPICUSER_ID")
private Integer id;
#Column(name="USER_ID")
private Integer userId;
#OneToMany(fetch = FetchType.EAGER)
#JoinColumn(name = "TOPICS_ID")
private Set<Topics> topicsUser;
//getter and setters

Ok starting from the ground up.. you entity classes should look like this:
#Entity
#Table(name="TOPIC_USERS")
public class UserTopics {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="TOPICUSER_ID")
private Integer id;
#Column(name="USER_ID")
private Integer userId;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "TOPICS_TOPICS_ID")
private Topics topics;
Your Topics class should look like this:
#Entity
#Table(name="TOPICS")
public class Topic {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="TOPICUS_ID")
private Integer id;
#Column(name="TOPICNAME")
private Integer topicName;
#OneToMany(mappedBy = "topics")
private Set<UserTopics> userTopics;
Finally the Criteria:
Version 1) You get entire entity:
Criteria c = session.createCriteria(Topics.class, "topics");
c.createAlias("topics.userTopics", "userTopics");
c.add(Restrictions.eq("userTopics.userId", userId));
return c.list(); // here you return List<Topics>
Version 2) You project only the topicname:
Criteria c = session.createCriteria(Topics.class, "topics");
c.createAlias("topics.userTopics", "userTopics");
c.add(Restrictions.eq("userTopics.userId", userId));
c.setProjection(Projections.property("topics.topicName"));
List<Object[]> results = (List<Object[]>)c.list();
// Here you have to manually get the topicname from Object[] table.
}

Related

Spring Data/Hibernate Generates two queries instead of a JOIN

Context: I have two tables: Questionnaire and Question Section. A Questionnaire can have many Question Sections. Questionnaires and Question Sections both have Start and End Dates to determine if they are active records.
Here are my entities as written:
#Entity
#Data
public class Questionnaire {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private Date startDate;
private Date endDate;
private String description;
#OneToMany(cascade = CascadeType.All,
fetch = FetchType.LAZY,
mappedBy = "questionnaire")
#JsonManagedReference
private List<QuestionSection> questionSections = new ArrayList<QuestionSection>();
}
#Entity
#Data
public class QuestionSection {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String description;
private int sectionLevel;
private Date startDate;
private Date endDate;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "QUESTIONNAIRE_ID", nullable = false)
#JsonBackReference
private Questionnaire questionnaire;
}
Here is my Spring Data Repository with a single declared method:
public interface QuestionnaireRepository extends JpaRepository<Questionnaire, UUID> {
Questionnaire findByNameAndEndDateIsNull(String name);
// Previous goal query, but worked all the way back to the above simple query
// Questionnaire findByIdAndQuestionSectionsEndDateIsNull(UUID id);
}
The above derived query generates two queries shown below:
-- For brevity
select questionnaire.id as id
questionnaire.description as description
questionnaire.end_date as end_date
questionnaire.start_date as start_date
from questionnaire
where questionnaire.name='Foo' and (questionnaire.end_date is null)
select questionsection.questionnaire_id as questionnaire id
...rest of fields here...
from question_section
where questionsection.questionnaire_id = id from above query
Then Spring Data or Hibernate is combining those two above queries into one data object representative of the questionnaire object and returning that.
My problem with this is that I would have expected One query to run with a Join between the two tables, not two and then combine the results in memory. I'm pretty experienced with Spring Data and ORMs in general and have not been able to find any documentation as to why this is happening. Honestly I wouldn't care except that my original intention was to query at the parent entity and 'filter' out children that have end dates (not active). This derived query (commented out above) exhibited the same behavior which ultimately resulted in the data set that was returned containing the end dated question sections.
I know there's 100 other ways I could solve this problem (which is fine) so this is more of an educational interest for me at this point if anyone has any insight into this behavior. I could be missing something really simple.
You should be able to do this using the Entity Graph feature introduced in JPA 2.1.
https://www.baeldung.com/jpa-entity-graph
Spring Data offers support for Entity Graphs via the #NamedEntityGraph and #EntityGraph annotations:
https://www.baeldung.com/spring-data-jpa-named-entity-graphs
So in your code:
Entity:
#Entity
#NamedEntityGraph(name = "Questionnaire.questionSections",
attributeNodes = #NamedAttributeNode("questionSections ")
)
public class Questionnaire{
//...
}
Repository:
public interface QuestionnaireRepository extends JpaRepository<Questionnaire, UUID> {
#NamedEntityGraph("Questionnaire.questionSections")
Questionnaire findByNameAndEndDateIsNull(String name);
}
public interface QuestionnaireRepository extends JpaRepository<Questionnaire, UUID> {
#EntityGraph(attributePaths = { "questionSections" })
Questionnaire findByNameAndEndDateIsNull(String name);
}

JPA Criteria API Specification for Many to Many

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.

Error in filtering by child object in Spring Data JPA Query

My code structure looks like the following.
Article:
#Entity
public class NewsArticle{
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
[Other class properties such as title, publisher, publishedDate, etc.]
#OneToMany(mappedBy = "article")
private Set<UserReadNewsArticle> userReadNewsArticles = new HashSet<>();
[Getters and Setters]
}
Article read by User:
#Entity
public class UserReadNewsArticle {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private Long readAccountId;
private Long readArticleId;
#JsonIgnore
#ManyToOne
private Account account;
#JsonIgnore
#ManyToOne
private NewsArticle article;
[Getters and Setters]
}
Account:
#Entity
public class Account {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
[Other class properties]
#OneToMany(mappedBy = "account")
private Set<UserReadNewsArticle> userReadNewsArticles = new HashSet<>();
[Getters and Setters]
}
I want to have a query method in my NewsArticleRepository to get all the Read News Articles for a user.
public interface NewsArticleRepository extends PagingAndSortingRepository<NewsArticle, Long>{
Collection<NewsArticle> findByUserReadNewsArticlesReadAccountId(Long readAccountId);
}
This method works great. But how can I write a Spring Data JPA Query/Method to get the "Unread News Articles for a user". What I have tried is the following.
Collection<NewsArticle> findByUserReadNewsArticlesReadAccountIdNot(Long readAccountId);
This one does return a list of articles which have been read by other users. But my requirement is to get all the unread news articles. I have gone through Spring Data JPA Documentation but failed to come up with an easier soultion. How can I overcome this issue? Or am I doing something wrong?
You could achieve your result by using a JPQL query with also a subquery:
public interface NewsArticleRepository extends PagingAndSortingRepository<NewsArticle, Long> {
#Query("SELECT n FROM NewsArticle n WHERE n NOT IN "
+ "(SELECT ur.article FROM UserReadNewsArticle ur JOIN ur.account a WHERE a.id = :readAccountId)")
Collection<NewsArticle> findByUserReadNewsArticlesReadAccountIdNotIn(#Param("readAccountId") Long readAccountId);
}
http://localhost:8080/newsArticles/search/findByUserReadNewsArticlesReadAccountIdNotIn?readAccountId=1
So first get the read articels from the current user and then exlude them from the whole article list.
I don't think that spring data is able to get you the same, since a subquery is definitetly needed. If I'm wrong, somebody can correct me.

JPA Criteria API join

Help me plz with one moment. I read about 10 articles already, but don't understand join moment. I have 2 tables:
public class News implements Serializable {
#Id
#GeneratedValue (generator = "increment")
#GenericGenerator (name = "increment", strategy = "increment")
private int id;
#Column
private String name;
#Column
private Date created;
#Column
private String data;
#ManyToOne (cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn (name = "category_id")
private NewsCategoryDict category;
// getters, setters
}
and
public class NewsCategoryDict implements Serializable {
#Id
#GeneratedValue (generator = "increment")
#GenericGenerator (name = "increment", strategy = "increment")
private int id;
#Column
private String name;
#OneToMany (mappedBy = "category", cascade = CascadeType.ALL)
private List<News> news = new ArrayList<>();
}
I want a query works like
SELECT * FROM news, categorynews WHERE news.category_id = categorynews.id;
And then get the result in jsp with
<div id="list_news">
<c:forEach items="${news}" var="news">
<h5>${news.id} : ${news.name} - ${news.created} ; ${news.data} (${news.category.name})</h5>
</c:forEach>
</div>
And I just can't understand this JOIN with Criteria API. Can you help me ?
Try to use this snippet, but get a error
public List<News> getAll() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<News> cq = cb.createQuery(News.class);
Root<News> rootFromNews = cq.from(News.class);
Join<NewsCategoryDict, News> join = rootFromNews.join("category");
cq.select(join);
return em.createQuery(cq).getResultList();
}
PropertyNotFoundException: Property 'created' not found on type ru.r1k0.spring.model.NewsCategoryDict
Assuming you want to return an instance of News associated to an instance of NewsCategoryDict, your criteria query should look as follows:
public List<News> getAll() {
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<News> cq = cb.createQuery(News.class);
Root<News> rootFromNews = cq.from(News.class);
Join<News, NewsCategoryDict> join = rootFromNews.join("category"); // #1
cq.select(rootFromNews); // #2
return em.createQuery(cq).getResultList();
}
The modified lines are marked with #1 and #2 comments.
The query should return all News which have a matching NewsCategoryDict; but News records which are not associated to aNewsCategoryDict record will not be returned.
Your error has nothing to do with the join! What is actually happening is that in the JSP fragment you are trying to access ${news.created} which does not exist in the NewsCategoryDict. I believe the error is in the JSP fragment, not in the Criteria query.
The way I understand it is that you want to list the News object, but in the query you are selecting the NewsCategoryDict and this is why at the end you end up with missing attribute because the NewsCategoryDict does not contain ${news.created}

How to make Hibernate criteria to bring only needed fields

I have next classes:
#Entity
#Table(name="A")
public class A implements Serializable{
#Id
#Column(name="a_id")
private long aId;
#ManyToOne
#JoinColumn(name="b_id")
private B b;
}
#Entity
#Table(name="b")
public class B implements Serializable{
#Id
#Column(name="b_id")
private long bId;
#Column(name="b_name")
private String name;
#Column(name="b_age")
private String age;
#OneToMany(mappedBy="b")
private Set<A> a;
}
I have getters and setters for this classes.
when I try execute next Criteria
session.createCriteria(A.class, "a_table")
.createAlias("a_table.b", "b_table")
.add(Restrictions.eq("b_table.age", "11"))
.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY)
.list();
In SQL generated by hibernate I have all fields from B (I mean field "name" too).
How I can make to Hibernate bring only needed fields.
In case that in criteria exists many aliases to many tables, it can increase execution time.
Thank you.
Try using projections. Maybe something like this:
Criteria cr = session.createCriteria(User.class)
.setProjection(Projections.projectionList()
.add(Projections.property("id"), "id")
.add(Projections.property("Name"), "Name"))
.setResultTransformer(Transformers.aliasToBean(User.class));
List list = cr.list();

Categories