Simple project to solve business task:
#Entity
#Table(name = "parent")
public class Parent {
#Id
#SequenceGenerator(name = "SEQ_GEN", sequenceName = "SEQ_JUST_FOR_TEST", allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_GEN")
#Column(name = "id", nullable = false)
private Long id;
#Column(name = "name", nullable = false)
private String name;
#OneToMany(fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST})
private List<Child> childs;
repository method:
#Query("select parent from Parent parent"
+ " join parent.childs childs "
+ " where parent.id = :id "
+ " and :search is null or childs.name = :search ")
Page<Parent>getPageById(Pageable pageable, #Param("id")Long id, #Param("search") String search);
and controller:
#GetMapping("/parents/{id}/{search}")
public ResponseEntity<Page<Parent> >parents(Pageable pageable, #PathVariable(value = "id") final String id,
#PathVariable(value = "search") final String search) {
Page<Parent>parentPage = parentRepository.getPageById(pageable, Long.getLong(id), search);
return new ResponseEntity<>(parentPage, HttpStatus.OK);
Is it possible to filter child collection and retrieve only childs with specific attribute, here it's a name for brievety?
Related
I am trying to sort my API result by a nested entity field.
My Entities look like that:
#Entity
#Table(name = "book_item")
public class BookItem {
#Id
#Column(name = "id", nullable = false)
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
#ManyToOne
#NotNull
private Book book;
...
}
#Entity
#Table(name = "book")
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Book {
#Id
#Column(name = "id")
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "sequenceGenerator")
#SequenceGenerator(name = "sequenceGenerator")
private Long id;
#Column(name = "title", nullable = false)
private String title;
private String subtitle;
#ManyToMany(fetch = FetchType.EAGER, cascade = { CascadeType.PERSIST, CascadeType.MERGE })
#JoinTable(name = "rel_book_author", joinColumns = #JoinColumn(name = "book_id"), inverseJoinColumns = #JoinColumn(name = "author_id"))
#Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
#JsonIgnoreProperties(value = { "books" }, allowSetters = true)
private Set<Author> authors = new HashSet<>();
#Column(name = "isbn", nullable = false)
private String isbn;
#Column(name = "pages", nullable = false)
private Integer pages;
...
}
Now I want to query all BookItems sorted by the Book titles.
My repository lloks like that:
public interface BookItemRepository extends JpaRepository<BookItem, UUID> {
#Query(
"select distinct b from BookItem b left join b.book.authors authors where (upper(authors.name) like upper(concat('%', ?2, '%')) or authors is NULL ) and upper(b.book.title) like upper(concat('%', ?1, '%'))"
)
Page<BookItem> findAllByTitleAndAuthor(String title, String name, Pageable pageable);
}
I've implemented an Angular component which does the following request:
GET http://localhost:9000/api/book/items?size=5&sort=title,asc
I get a 500 error code:
org.hibernate.QueryException: could not resolve property: title of: org.pickwicksoft.libraary.domain.BookItem [select distinct b from org.pickwicksoft.libraary.domain.BookItem b left join b.book.authors authors where (upper(authors.name) like upper(concat('%', ?2, '%')) or authors is NULL ) and upper(b.book.title) like upper(concat('%', ?1, '%')) order by b.title asc, b.id asc]; nested exception is java.lang.IllegalArgumentException
I also tried the following and it also does not work:
GET http://localhost:9000/api/book/items?size=5&sort=book_title,asc
How can I solve this problem?
I have a performance issue on search queries with multiple joins on the table with 250000+ records. The best time that I achieve is 1.5 seconds with default pagination and sorting provided by JPA. Also, I tried to add indexes on columns, but the time remains the same because of joins. Is there any way to boost the performance of the query?
"select new com.app.e_library.service.dto.BookDto(book.id,book.isbn," +
" book.title, book.publicationYear, book.pageCount, genre.name," +
" book.bookStatus, publisher.publisherName, author.name) " +
"from BookEntity book " +
"inner join book.bookGenre genre " +
"inner join book.publisher publisher " +
"inner join book.author author " +
"where book.isbn like :key% or " +
"book.title like :key% or " +
"trim(book.publicationYear) like :key% or " +
"genre.name like :key% or " +
"publisher.publisherName like :key% or " +
"author.name like :key%"
And query generated by hibernate.
Hibernate:
select
bookentity0_.id as col_0_0_,
bookentity0_.isbn as col_1_0_,
bookentity0_.title as col_2_0_,
bookentity0_.publication_year as col_3_0_,
bookentity0_.page_count as col_4_0_,
bookgenree1_.name as col_5_0_,
bookentity0_.book_status as col_6_0_,
publishere2_.name as col_7_0_,
authorenti3_.name as col_8_0_
from
book bookentity0_
inner join
book_genre bookgenree1_
on bookentity0_.genre_id=bookgenree1_.id
inner join
publisher publishere2_
on bookentity0_.publisher_id=publishere2_.id
inner join
author authorenti3_
on bookentity0_.author_id=authorenti3_.id
where
bookentity0_.isbn like ?
or bookentity0_.title like ?
or trim(bookentity0_.publication_year) like ?
or bookgenree1_.name like ?
or publishere2_.name like ?
or authorenti3_.name like ?
order by
bookentity0_.id asc limit ?
Hibernate:
select
count(bookentity0_.id) as col_0_0_
from
book bookentity0_
inner join
book_genre bookgenree1_
on bookentity0_.genre_id=bookgenree1_.id
inner join
publisher publishere2_
on bookentity0_.publisher_id=publishere2_.id
inner join
author authorenti3_
on bookentity0_.author_id=authorenti3_.id
where
bookentity0_.isbn like ?
or bookentity0_.title like ?
or trim(bookentity0_.publication_year) like ?
or bookgenree1_.name like ?
or publishere2_.name like ?
or authorenti3_.name like ?
Book Entity
#Entity
#Table(
name = "book",
uniqueConstraints = {
#UniqueConstraint(name = "book_isbn_unique",column Names = "isbn")
},
indexes = {
#Index(name = "isbn_index", columnList = "isbn"),
#Index(name = "title_index", columnList = "title"),
}
)
public class BookEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "isbn", nullable = false)
#NotBlank
private String isbn;
#Column(name = "title", nullable = false)
#NotBlank
private String title;
#Column(name = "publication_year")
#Valid
private short publicationYear;
#Column(name = "page_count")
#Valid
#Range(min = 50, max = 5000)
private int pageCount;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name="genre_id",referencedColumnName = "id", nullable=false)
#ToString.Exclude
private BookGenreEntity bookGenre;
#Column(name = "book_status", nullable = false)
#Enumerated(EnumType.STRING)
private BookStatusType bookStatus;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "publisher_id", referencedColumnName = "id", nullable = false)
#NonNull
#ToString.Exclude
private PublisherEntity publisher;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "pick_detail_id", referencedColumnName = "id")
#ToString.Exclude
private PickDetailEntity pickDetail;
#ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "author_id", referencedColumnName = "id", nullable = false)
#NonNull
#ToString.Exclude
private AuthorEntity author;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
#JoinColumn(name = "book_image_id", referencedColumnName = "id")
#ToString.Exclude
private BookImageEntity bookImage;
Book Genre Entity
#Entity
#Table(
name = "book_genre",
uniqueConstraints = {
#UniqueConstraint(name = "book_genre_name_unique", columnNames = "name")
},
indexes = {
#Index(name = "name_index", columnList = "name"),
}
)
public class BookGenreEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "name", nullable = false)
#NotBlank
private String name;
#OneToMany(
targetEntity = BookEntity.class,
mappedBy = "bookGenre",
cascade=CascadeType.ALL,
fetch = FetchType.LAZY,
orphanRemoval = true)
#ToString.Exclude
private List<BookEntity> books;
Publisher Entity
#Entity
#Table(
name = "publisher",
uniqueConstraints = {
#UniqueConstraint(name = "publisher_name_unique", columnNames = "name")
},
indexes = {
#Index(name = "name_index", columnList = "name")
}
)
public class PublisherEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name", nullable = false)
#NotBlank
private String publisherName;
#OneToMany(
targetEntity = BookEntity.class,
mappedBy = "publisher",
cascade=CascadeType.ALL,
fetch = FetchType.LAZY)
#ToString.Exclude
private List<BookEntity> books;
Author Entity
#Entity
#Table(
name = "author",
uniqueConstraints = {
#UniqueConstraint(name = "author_name_unique", columnNames = "name")
},
indexes = {
#Index(name = "name_index", columnList = "name")
}
)
public class AuthorEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "name", nullable = false)
#NotBlank
private String name;
#OneToMany(
targetEntity = BookEntity.class,
mappedBy = "author",
cascade=CascadeType.ALL,
fetch = FetchType.LAZY)
#ToString.Exclude
private List<BookEntity> books;
You don't have to specify the referencedColumnName in your #JoinColumn
You have a #UniqueConstraint on BookEntity#isbn so there's no need to add an extra index.
You need an index on BookEntity#publication_year :
indexes = {
#Index(name = "title_index", columnList = "title"),
#Index(name = "publication_year_index", columnList = "publication_year"),
}
You can try this query :
#NamedQuery(name="BookEntity.search", query="select book from BookEntity book"
+ " inner join fetch book.genre genre"
+ " inner join fetch book.publisher publisher"
+ " inner join fetch book.author or author"
+ " left join fetch book.pickDetail"
+ " where book.isbn like :key"
+ " or book.title like :key"
+ " or book.publicationYear = :year"
+ " or genre.name like :key"
+ " or publisher.publisherName like :key"
+ " or author.name like :key")
And this function :
private EntityManager em;
public List<BookEntity> search(String key, short year) {
return em.createNamedQuery("BookEntity.search", BookEntity.class)
.setParameter("key", key + "%")
.setParameter("year", year)
.getResultList();
}
Are you aware that you are doing a sensitive case search ? That's not very friendly. Also, the user can't search with a word in the middle of the title.
To avoid this you have to use something like :
upper(book.title) like :key
And
setParameter("key", "%" + key.toUpperCase() + "%")
But this will trigger a full scan.
As a followup to this answer(on approach 1 ) I want to go a step further :
I want to Filter the grand child objects based on certain criteria's. I tried the following query, but it still does not filter out the objects under the grandchild entity.
#Query("select ch from ChildEntity ch "
+ " join ch.parentEntity pr "
+ " join fetch ch.grandChildEntities gc "
+ " where pr.bumId = :bumId and ch.lastExecutionTimestamp in "
+ "( select max(ch1.lastExecutionTimestamp) from ChildEntity ch1 "
+ "join ch1.grandChildEntities gc ON ch1.id = gc.childEntity where "
+ "gc.field1 in ('\"Criteria1\"','\"Criteria2\"','\"Criteria3\"') and "
+ "gc.field2 = '\"soldout\"'"
+ "ch1.parentEntity = pr group by ch1.c1))")
List<ChildEntity> findLastExecutedChildFromBumId(#Param("bumId") String bumId);
Associated Class Entities
Class Relation ParentEntity <1-oneToMany-x> ChildEntity<1-oneToMany-x>GrandChildEntity
#Entity
#Getter
#Setter
#Table(name = "table_parent")
#RequiredArgsConstructor
#NoArgsConstructor
#AllArgsConstructor
public class ParentEntity implements Serializable {
private static final long serialVersionUID = -271246L;
#Id
#SequenceGenerator(
name="p_id",
sequenceName = "p_sequence",
initialValue = 1,
allocationSize = 1)
#GeneratedValue(generator="p_id")
#Column(name="id", updatable=false, nullable=false)
private Long id;
#NonNull
#Column(name ="bum_id", nullable = false, unique = true)
private String bumId;
#NonNull
#Column(nullable = false, length = 31)
private String f1;
#NonNull
#Column(nullable = false, length = 31)
private String f2;
#NonNull
#Column( nullable = false, length = 255)
#Convert(converter = JpaConverterJson.class)
private List<String> f3;
#NonNull
#Column(nullable = false)
private String f4;
#NonNull
#Column(name = "es_status", nullable = false, length = 255)
#Enumerated(EnumType.STRING)
private ExecutionStatus esStatus;
#JsonManagedReference
#OneToMany(mappedBy = "parentEntity", cascade = CascadeType.ALL,
fetch = FetchType.EAGER)
#Setter(AccessLevel.NONE)
private List<ChildEntity> childEntities;
public void setChildEntities(List<ChildEntity> childEntities) {
this.childEntities = childEntities;
childEntities.forEach(entity -> entity.setParentEntity(this));
}
}
#Entity
#Getter
#Setter
#Table(name= "table_child")
#NoArgsConstructor
public class ChildEntity implements Serializable {
private static final long serialVersionUID = -925587271547L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#JsonBackReference
#ManyToOne(fetch = FetchType.EAGER )
#JoinColumn(name = "parent_id")
private ParentEntity parentEntity;
#Column(name = "c1",nullable = false)
#NonNull
#Convert(converter = JpaConverterJson.class)
private String c1;
#Column(name = "last_exec_status",nullable = false)
#NonNull
#Enumerated(EnumType.STRING)
private ExecutionStatus lastExecStatus;
#Column(name = "c4",nullable = false)
#NonNull
private String c4;
#Column(name = "last_execution_timestamp",nullable = false)
#NonNull
private long lastExecutionTimestamp;
#JsonManagedReference
#NonNull
#OneToMany(mappedBy = "childEntity", cascade = CascadeType.ALL,
fetch = FetchType.EAGER)
#Setter(AccessLevel.NONE)
private List<GrandChildEntity> grandChildEntities;
public void setGrandChildEntities(List<GrandChildEntity> grandChildEntities) {
this.grandChildEntities = grandChildEntities;
grandChildEntities.forEach(entity -> entity.setChildEntity(this));
}
}
#Entity
#Getter
#Setter
#Table(name="table_grand_child")
#NoArgsConstructor
//#AllArgsConstructor
public class GrandChildEntity implements Serializable {
private static final long serialVersionUID = -925567241248L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#JsonBackReference
#ManyToOne(fetch = FetchType.EAGER )
#JoinColumn(name = "child_entity_id")
private ChildEntity childEntity;
#Column(name="gc1",nullable = false)
private String gc1;
#Column(name="gc2",nullable = false)
private String gc2;
#Column(name="gc3",nullable = false)
private String gc3;
#Column(name="gc3",nullable = true)
private List<String> gc3;
}
Filtering a collection that is join fetched is a bad idea as that alters the "persistent state" and might cause entities to be removed due to that. I suggest you use a DTO approach instead.
I think this is a perfect use case for Blaze-Persistence Entity Views.
I created the library to allow easy mapping between JPA models and custom interface or abstract class defined models, something like Spring Data Projections on steroids. The idea is that you define your target structure(domain model) the way you like and map attributes(getters) via JPQL expressions to the entity model.
A DTO model for your use case could look like the following with Blaze-Persistence Entity-Views:
#EntityView(ChildEntity.class)
public interface ChildEntityDto {
#IdMapping
Long getId();
String getC1();
ParentEntityDto getParentEntity();
#Mapping("grandChildEntities[field1 in ('\"Criteria1\"','\"Criteria2\"','\"Criteria3\"') and gc.field2 = '\"soldout\"']")
Set<GrandChildEntityDto> getGrandChildEntities();
#EntityView(ParentEntity.class)
interface ParentEntityDto {
#IdMapping
Long getId();
String getF1();
}
#EntityView(GrandChildEntity.class)
interface GrandChildEntityDto {
#IdMapping
Long getId();
String getGc1();
}
}
Querying is a matter of applying the entity view to a query, the simplest being just a query by id.
UserDto a = entityViewManager.find(entityManager, UserDto.class, id);
The Spring Data integration allows you to use it almost like Spring Data Projections: https://persistence.blazebit.com/documentation/entity-view/manual/en_US/index.html#spring-data-features
I'm trying to apply the where condition on the related entity, but the result set contains all the related entity data. It appears like the filter is ignored.
I have the following entities:
Entity Audit:
#Entity
#Table(name = "entity_audit")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#org.springframework.data.elasticsearch.annotations.Document(indexName = "entityaudit")
public class EntityAudit implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#org.springframework.data.elasticsearch.annotations.Field(type = FieldType.Keyword)
private Long id;
#NotNull
#Column(name = "entity_id", nullable = false)
private Long entityId;
#NotNull
#Column(name = "entity_class_name", nullable = false)
private String entityClassName;
#NotNull
#Column(name = "entity_name", nullable = false)
private String entityName;
#NotNull
#Enumerated(EnumType.STRING)
#Column(name = "action_type", nullable = false)
private EntityAuditType actionType;
#NotNull
#Column(name = "timestamp", nullable = false)
private Instant timestamp;
#NotNull
#Column(name = "user", nullable = false)
private String user;
#NotNull
#Column(name = "transaction_uuid", nullable = false)
private String transactionUuid;
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "entity_audit_id")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
private Set<EntityAuditUpdateData> entityAuditUpdateData = new HashSet<>();
#OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "entity_audit_id")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
private Set<EntityAuditStatus> entityAuditStatuses = new HashSet<>();
Getters and setters...
Entity Audit Status
#Entity
#Table(name = "entity_audit_status")
#Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
#org.springframework.data.elasticsearch.annotations.Document(indexName = "entityauditstatus")
public class EntityAuditStatus implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#org.springframework.data.elasticsearch.annotations.Field(type = FieldType.Keyword)
private Long id;
#NotNull
#Column(name = "user_login", nullable = false)
private String userLogin;
#NotNull
#Column(name = "jhi_read", nullable = false)
private Boolean read;
#ManyToOne
private EntityAudit entityAudit;
Getters and setters...
I'm trying to achieve this query:
#Query("select distinct entityAudit from EntityAudit entityAudit " +
"join entityAudit.entityAuditStatuses entityAuditStatus " +
"where entityAuditStatus.userLogin =:userLogin " +
"order by entityAudit.timestamp desc")
Page<EntityAudit> retrieveAllByUserLogin(#Param(value = "userLogin") String userLogin, Pageable pageable);
But when I retrieve the data the EntityAuditStatuses are not filtered. I don't understand where the problem is.
Note: I removed the date property from the minimum reproducible example.
Use left join fetch instead of left join to make sure the dependent entityAuditStatuses are fetched as part of the join query itself, and not as multiple queries after finding the entityAudit. And since the result needs to be paginated, an additional countQuery will need to be specified (without the fetch). Working Query -
#Query(value = "select entityAudit from EntityAudit entityAudit " +
"left join fetch entityAudit.entityAuditStatuses entityAuditStatus " +
"where entityAuditStatus.userLogin = :userLogin ",
countQuery = "select entityAudit from EntityAudit entityAudit " +
"left join entityAudit.entityAuditStatuses entityAuditStatus " +
"where entityAuditStatus.userLogin = :userLogin ")
Without left join fetch, three queries are being generated - one which fetches the entityAuditId 1 (based on the userLogin 1) and then two more to fetch the entityAuditStatuses (from the entity_audit_status table only without the join) given the entityAuditId 1.
That is why, when you ask for userLogin = '1' - you retrieve the EntityAudit 1 which brings with it - entityAuditStatus 1 - entityAuditStatus 3 (which has userLogin = '2')
After adding left join fetch, there is only one query using join as per the defined entity relationships. So the results are correctly fetched.
So, I have two tables, a Group and a Person table. They have a many to many relationship. Group have a collection of Persons and Person got a collection of Groups. Group is set as managing part of the relationship. My question is, is it possible for me to create a new Person, without a group, then create a new Group, add that newly created Person, then save that group, and as a result have that the new Person is related to the new group if for example "getGroups()" is called?
My classes:
Group, Person
#Entity
#Getter
#Setter
#Table(name = Group.TABLE_NAME)
public class Group {
static final String TABLE_NAME = "GROUP";
#Id
#Column(name = "GROUP_ID", nullable = false)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "GroupGenerator")
#SequenceGenerator(name="GroupGenerator", sequenceName = "GROUP_SEQUENCE", allocationSize = 100)
#Getter(AccessLevel.NONE)
#Setter(AccessLevel.NONE)
private Long id;
#ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#JoinTable(name = "GROUP_PERSON",
joinColumns = #JoinColumn(name = "GROUP_ID", referencedColumnName = "GROUP_ID"),
inverseJoinColumns = { #JoinColumn(name="PERSON_ID", referencedColumnName = "PERSON_ID")} )
private Set<Person> persons= new HashSet<>();
public void addPerson(Person person){
if(!persons.contains(person)){
persons.add(person);
}
}
Person
#Entity
#Getter
#Setter
#Table(name = Person.TABLE_NAME)
public class Person {
static final String TABLE_NAME = "PERSON";
#Id
#GeneratedValue(strategy = SEQUENCE, generator = "personIdGenerator")
#SequenceGenerator(name = "personIdGenerator", sequenceName = "PERSON_SEQUENCE", allocationSize = 100)
#Column(name = "PERSON_ID", nullable = false)
#Getter(AccessLevel.NONE)
#Setter(AccessLevel.NONE)
private Long id;
#ManyToMany(mappedBy = "persons", fetch = FetchType.EAGER)
private Set<Group> groups = new HashSet<>();
public void addToGroup(Group group){
if(!groups.contains(group)) {
groups.add(group);
}
}
Test that fails because getGroups() is empty. The repository is just standard CRUD:
#Test
public void testSaveGroupReflectedInPersonTable() throws Exception {
Group group1 = new Group;
Person person1 = new Person;
group1.addPerson(person1);
groupRepository.save(group1);
person1 = personRepository.findAll().get(0);
assertFalse(person1.getGroups().isEmpty);
}