JPA Query Many To One nullable relationship - jpa

I have the following entities and would like to seek help on how to query for selected attributes from both side of the relationship. Here is my model. Assume all tables are properly created in the db. JPA provider I am using is Hibernate.
#Entity
public class Book{
#Id
private long id;
#Column(nullable = false)
private String ISBNCode;
#ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY, optional = false)
private Person<Author> author;
#ManyToOne(cascade = CascadeType.DETACH, fetch = FetchType.LAZY, optional = true)
private Person<Borrower> borrower;
}
#Inheritance
#DiscriminatorColumn(name = "personType")
public abstract class Person<T>{
#Id
private long id;
#OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
private Info information;
}
#Entity
#DiscriminatorValue(PersonType.Author)
public class Author extends Person<Author> {
private long copiesSold;
}
#Entity
#DiscriminatorValue(PersonType.Borrower)
public class Borrower extends Person<Borrower> {
.....
}
#Entity
public class Info {
#Id
private long id;
#Column(nullable=false)
private String firstName;
#Column(nullable=false)
private String lastName;
......;
}
As you can see, the book table has a many to one relation to Person that is not nullable and Person that is nullable.
I have a requirement to show, the following in a tabular format -
ISBNCode - First Name - Last Name - Person Type
How can I write a JPA query that will allow me to select only attributes that I would want. I would want to get the attributes ISBN Code from Book, and then first and last names from the Info object that is related to Person Object that in turn is related to the Book object. I would not want to get all information from Info object, interested only selected information e.g first and last name in this case.
Please note that the relation between the Borrower and Book is marked with optional=true, meaning there may be a book that may not have been yet borrowed by someone (obviously it has an author).

Example to search for books by the author "Marc":
Criteria JPA Standard
CriteriaQuery<Book> criteria = builder.createQuery( Book.class );
Root<Book> personRoot = criteria.from( Book.class );
Predicate predicate = builder.conjunction();
List<Expression<Boolean>> expressions = predicate.getExpressions();
Path<Object> firtsName = personRoot.get("author").get("information").get("firstName");
expressions.add(builder.equal(firtsName, "Marc"));
criteria.where( predicate );
criteria.select(personRoot);
List<Book> books = em.createQuery( criteria ).getResultList();
Criteria JPA Hibernate
List<Book> books = (List<Book>)sess.createCriteria(Book.class).add( Restrictions.eq("author.information.firstName", "Marc") ).list();
We recommend using hibernate criterias for convenience and possibilities.
Regards,

Related

Does hibernate search - 6.0 version support projection on embedded entities?

I am developing a search api where I need to return in response the resource with only the fields/properties asked in request. The fields can be of sub elements as well. E.g - book.author.name where book is the parent resource and author a sub resource under it, may be with a many to one relationship.
I have learned in earlier versions of hibernate (5.x.x) projections is not supported on embedded entities.
So wanted to know if this feature is added in 6.0
When there is a single author, yes, you can do a projection on the author (but you could already in Search 5, though in a less convenient way):
#Entity
#Indexed
class Book {
#Id
private Long id;
#GenericField
private String title;
#ManyToOne
#IndexedEmbedded
private Author author;
// ...getters and setters...
}
#Entity
class Author {
#Id
private Long id;
#GenericField(projectable = Projectable.YES)
private String firstName;
#GenericField(projectable = Projectable.YES)
private String lastName;
#OneToMany(mappedBy = "author")
private List<Book> books = new ArrayList<>();
// ...getters and setters...
}
class MyProjectedAuthor {
public final String firstName;
public final String lastName;
public MyProjectedAuthor(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
}
SearchSession searchSession = Search.session(entityManager);
List<MyProjectedAuthor> projectedAuthors = searchSession.search(Book.class)
.asProjection(f -> f.composite(
MyProjectedAuthor::new,
f.field("author.firstName", String.class),
f.field("author.lastName", String.class),
))
.predicate(f -> f.matchAll())
.fetchHits(20);
Multi-valued projections (e.g. if you have multiple authors per book) are not supported yet, but we will be working on it before the 6.0.0 release: https://hibernate.atlassian.net/browse/HSEARCH-3391
If you were talking about loading the authors from the database instead of projections, then there is no such built-in feature yet. We'll be looking into it when we address HSEARCH-3071, but I can't tell how long it will take.
As a workaround, for single-valued associations, you can implement loading manually:
#Entity
#Indexed
class Book {
#Id
private Long id;
#GenericField
private String title;
#ManyToOne
#IndexedEmbedded
private Author author;
// ...getters and setters...
}
#Entity
class Author {
#Id
#GenericField(projectable = Projectable.YES)
private Long id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "author")
private List<Book> books = new ArrayList<>();
// ...getters and setters...
}
SearchSession searchSession = Search.session(entityManager);
List<Author> authors = searchSession.search(Book.class)
.asProjection(f -> f.composite(
authorId -> entityManager.getReference(Author.class, authorId),
f.field("author.id", Long.class)
))
.predicate(f -> f.matchAll())
.fetchHits(20);
// Or, for more efficient loading:
SearchSession searchSession = Search.session(entityManager);
List<Long> authorIds = searchSession.search(Book.class)
.asProjection(f -> f.field("author.id", Long.class))
.predicate(f -> f.matchAll())
.fetchHits(20);
List<Author> authors = entityManager.unwrap(Session.class).byMultipleIds(Author.class)
.withBatchSize(20)
.multiLoad(authorIds);
EDIT: According to your comment, your problem was related to many fields, not just the author. Essentially, you are concerned about loading as few associations as possible.
The most common solution to this problem in Hibernate ORM is to set all your associations' fetch mode to lazy (which is what you should do by default, anyway).
Then when searching, do not even think about loading: just ask Hibernate Search to retrieve the entities you need. Associations will not be loaded at that point.
Then, when you serialize your entities to JSON, only the associations you actually use will be loaded. If you correctly set the default batch fetch size (with hibernate.default_batch_fetch_size, here, performance should be comparable to what you'll achieve with more complicated solutions, at a fraction of the development time.
If you really want to fetch some of the associations eagerly, the easiest solution would probably be to leverage JPA's entity graphs: they tell Hibernate ORM which associations to load exactly when it loads the Book entity.
There's no built-in functionnality for that in Hibernate Search 6 yet, but you can do it manually:
#Entity
#Indexed
class Book {
#Id
private Long id;
#GenericField
private String title;
#ManyToOne // No need for an #IndexedEmbedded with this solution, at least not for loading
private Author author;
// ...getters and setters...
}
#Entity
class Author {
#Id // No need for an indexed ID with this solution, at least not for loading
private Long id;
private String firstName;
private String lastName;
#OneToMany(mappedBy = "author")
private List<Book> books = new ArrayList<>();
// ...getters and setters...
}
SearchSession searchSession = Search.session(entityManager);
List<Long> bookIds = searchSession.search(Book.class)
.asProjection(f -> f.composite(ref -> (Long)ref.getId(), f.entityReference()))
.predicate(f -> f.matchAll())
.fetchHits(20);
// Note: there are many ways to build a graph, some less verbose than this one.
// See https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#fetching-strategies-dynamic-fetching-entity-graph
javax.persistence.EntityGraph<Book> graph = entityManager.createEntityGraph( Book.class );
graph.addAttributeNode( "author" );
// Ask for the author association to be loaded eagerly
graph.addSubgraph( "author" ).addAttributeNode( "name" );
List<Book> booksWithOnlySomeAssociationsFetched = entityManager.unwrap(Session.class).byMultipleIds(Book.class)
.with(graph, org.hibernate.graph.GraphSemantic.FETCH)
.withBatchSize(20)
.multiLoad(bookIds);
Note that, even with this solution, you should probably set the fetch mode to lazy in the mapping (#OnyToMany, ...) for as many associations as possible, because Hibernate ORM doesn't allow making an eager association lazy though a fetch graph.

Spring Data JPA order by value from OneToMany relation

I am trying to sort a result by nested collection element value. I have a very simple model:
#Entity
public class User {
#Id
#NotNull
#Column(name = "userid")
private Long id;
#OneToMany(mappedBy = "user")
private Collection<Setting> settings = new HashSet<>();
// getters and setters
}
#Entity
public class Setting {
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "userid")
private User user;
private String key;
private String value;
// getters and setters
}
public interface UserRepository extends JpaRepository<User, Long>, QuerydslPredicateExecutor<User> {
}
I want to have a result returned sorted by the value of one setting.
Is it possible to order by user.settings.value where settings.name = 'SampleName' using Spring Data JPA with QueryDSL?
I've used JpaSpecificationExecutor. let's see findAll for example.
Page<T> findAll(#Nullable Specification<T> spec, Pageable pageable);
Before call this method you can create your specification dynamically (where condition) and Pageable object with dynamic Sort information.
For example
...
Specification<T> whereSpecifications = Specification.where(yourWhereSpeficiation);
Sort sortByProperty = Sort.by(Sort.Order.asc("property"));
PageRequest orderedPageRequest = PageRequest.of(1, 100, sortByProperty);
userRepository.findAll(whereSpecifications, PageRequest.of(page, limit, orderedPageRequest));

Posting an entity with manyToMany, ManyToOne

I've always avoided #xToX relationships in my APIs but finally wanted to give it a go, and I'm struggling with it.
I've managed to make the get methods returning me the right informations with any recursiveness (at least I made a part), and now I want to post data to my API with those relationships in place (and seems working so far).
So I have a few objects a book, a book style, a book category and an author.
A book has only one author (even if it's false sometimes but as I only read Sci-Fi it will do the trick) and an author have many books (same for category).
And a style can have many books, also books can have many styles.
So I went with a #ManyToOne on the book's author property and with #OneToMany on the author's books property (same for category).
And between the style and the bok #ManyToMany
I want to be able to post a book to my API like follows, using the author and style ids :
{
"isbn": "15867jhg8",
"title": "Le portrait de Dorian Gray",
"author": 1,
"category":1,
"style": [1,2,3]
}
I've annotated my properties with #JsonBackReference, #JsonManagedReference, #JsonSerialize, #JsonIdentityInfo but I truly think I might have made it too much ...
I'm not sure about the way I used the #JsonSerialize, nor the #JsonIdentityInfo.
I've omitted the useless properties to keep the example 'simple'.
So let's dive in.
The Book class first :
#Entity
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class Book implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
private Integer id;
private String isbn;
private String title;
#ManyToOne
#JoinColumn(name="category_id", nullable = false)
#JsonBackReference(value = "category")
private BookCategory category;
#ManyToOne
#JoinColumn(name="author_id", nullable = false)
#JsonBackReference(value = "author")
private Author author;
#ManyToMany
#JsonManagedReference(value = "styles")
#JsonSerialize(using = BookStyleListSerializer.class)
private List <BookStyle> styles;
}
Then the author class :
#Entity
#JsonIdentityInfo (
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class Author implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
private Integer id;
#OneToMany(mappedBy = "author")
#JsonManagedReference(value = "authorBooks")
#JsonSerialize(using = BookListSerializer.class)
private List <Book> books;
}
As the category is quite the same, I'm only pasting the books property of it :
#OneToMany(mappedBy = "category")
#JsonManagedReference(value = "categoryBooks")
#JsonSerialize(using = BookListSerializer.class)
private List <Book> books;
And finally my bookStyle class :
#Entity
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id"
)
public class BookStyle implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(updatable = false, nullable = false)
private Integer id;
#ManyToMany
#JsonManagedReference(value = "styleBooks")
#JsonSerialize(using = BookListSerializer.class)
private List <Book> books;
}
The serializer are the same, it's only the type that changes :
public class BookStyleListSerializer extends StdSerializer <List <BookStyle>> {
public BookStyleListSerializer() {
this(null);
}
public BookStyleListSerializer(Class<List<BookStyle>> t) {
super(t);
}
#Override
public void serialize(List <BookStyle> bookStyles, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
List<Integer> ids = new ArrayList <> ( );
for (BookStyle style: bookStyles){
ids.add(style.getId ());
}
jsonGenerator.writeObject ( ids );
}
}
Of what I understood (and as english is not my native language i might misunderstood a few things here and there while coding) :
#JsonBackReference is to be used for the property we don't want to be serialized as opposite to #JsonManagedReference
#JsonSerialize is the custom serializer so that the elements will be serialized as we want them to (in this case, only using IDs)
And as it might be obvious to some of you : none of what I've coded works for posting data, here's the exception as i received it when i post something via API (and it's doubled, not a copy paste error) :
.c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.rz.librarian.domain.entity.Author]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'categoryBooks': no back reference property found from type [collection type; class java.util.List, contains [simple type, class com.rz.librarian.domain.entity.Book]]
.c.j.MappingJackson2HttpMessageConverter : Failed to evaluate Jackson deserialization for type [[simple type, class com.rz.librarian.domain.entity.Author]]: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot handle managed/back reference 'categoryBooks': no back reference property found from type [collection type; class java.util.List, contains [simple type, class com.rz.librarian.domain.entity.Book]]
I've tried many things but after three days on this issue i wanted to ask (cry?) for help.
Thank you guys and sorry for the long post!

spring data error when trying to sort by a field of joined entity inside a crudrepository

I am using springboot and springdata with Mysql.
I have 2 entities, Customer & Order:
#Entity
#Table(name = "customers")
public class Customer {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="id", nullable = false)
protected long id;
#Column(name = "name")
private String name;
}
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="id", nullable = false)
protected long id;
#Column(name="customer_id")
private long customerId;
}
I also have a repository:
#Repository
public interface OrdersRepository extends JpaRepository<Order, Long> {
#Query("select o from Order o, Customer c where o.customerId = c.id")
Page<Order> searchOrders(final Pageable pageable);
}
The method has some more arguments for searching, but the problem is when I send a PageRequest object with sort that is a property of Customer.
e.g.
Sort sort = new Sort(Sort.Direction.ASC, "c.name");
ordersRepository.search(new PageRequest(x, y, sort));
However, sorting by a field of Order works well:
Sort sort = new Sort(Sort.Direction.ASC, "id");
ordersRepository.search(new PageRequest(x, y, sort));
The error I get is that c is not a property of Order (but since the query is a join of the entities I would expect it to work).
Caused by: org.hibernate.QueryException: could not resolve property c of Order
Do you have any idea how I can sort by a field of the joined entity?
Thank you
In JPA , the thing that you sort with must be something that is returned in the select statement, you can't sort with a property that is not returned
You got the error because the relationship is not modeled properly. In your case it is a ManyToOne relation. I can recomend the wikibooks to read further.
#Entity
#Table(name = "orders")
public class Order {
#Id
#GeneratedValue(strategy= GenerationType.IDENTITY)
#Column(name="id", nullable = false)
protected long id;
#ManyToOne
#JoinColumn(name="customer_id", referencedColumnName = "id")
private Customer customer;
}
The query is not needed anymore because the customer will be fetched.
#Repository
public interface OrdersRepository extends PagingAndSortingRepository<Order, Long> {
}
Now you can use nested properties.
Sort sort = new Sort(Sort.Direction.ASC, "customer.name");
ordersRepository.findAll(new PageRequest(x, y, sort));

Child entity with Secondary Table and Compound Key spread over two tables

After much debate on choosing an approach for an internationalized database design I went with having two tables for each table that requires translation. I'm having some trouble with ORM in the following case.
So I have the following tables:
cat cat_t subcat subcat_t
------ ------- ---------- ------------
id (pk) cat_id(pk,fk) id(pk) subcat_id(pk,fk)
locale(pk) cat_id(fk) locale(pk)
name name
#Entity
#Table(name = "cat")
#SecondaryTable(name = "cat_t",
pkJoinColumns = #PrimaryKeyJoinColumn(name = "cat_id",
referencedColumnName = "id"))
#IdClass(TranslationKey.class)
public class Category {
#Id
private long id;
#Id
#Column(table = "cat_t")
private String locale;
#Column(table = "cat_t")
private String name;
#OneToMany(fetch = FetchType.LAZY)
private List<SubCategory> subCategories;
// getters and setters
}
#Entity
#Table(name = "subcat")
#SecondaryTable(name = "subcat_t",
pkJoinColumns = #PrimaryKeyJoinColumn(name = "subcat_id",
referencedColumnName = "id"))
#IdClass(TranslationKey.class)
public class SubCategory{
#Id
private long id;
#Id
#Column(table = "subcat_t")
private String locale;
#Column(table = "subcat_t")
private String name;
#Column(name = "cat_id")
private long categoryId;
// getters and setters
}
public class TranslationKey implements Serializable {
private long id;
private String locale;
// getters and setters
}
My goal is for subcategories to only pull back the subcategories for the locale of the parent. I think I have some options including, querying the subcategories separately and making the field transient or pull everything back (all subategories for all languages) and then just filter out the ones I want.
The issue I have had with #JoinColumn is that locale is part of the secondary table for both cat can subcat and so when I try the referencedColumn that may not be allowed since its not in the same table?. I'm using EclipseLink but I'm not really tied to a JPA Provider.
Any help/guidance is much appreciated
cat seems to have a one to many relationship with cat_t, since there can be many rows in cat_t for a single cat entry. This is not ideal to have in a single Entity, since it means you will have instances sharing data, and endless headaches if you make changes.
A better approach that makes updates possible is to map the cat and cat_t to separate Entities, where Cat has a collection of Cat_t?Local instances. The entity mapping to cat_t can use the local and its ManyToOne back pointer to Cat as its Id:
#Entity
#IdClass(TranslationKey.class)
public class Local {
#ID
private String locale;
#ID
#ManyToOne
private Cat cat;
}
public class TranslationKey{
string local;
long cat;
}