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

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.

Related

how to develop JPA bi-directional entities in spring toot

I am new to spring boot and jpa/hibernate, please bear my inaccurate usage of the terminologies.
I have two entities: book and address. A book is published in a certain city which is stored in "address", a "address" can publish multiple books.
The DB schema for book is: id, name, author, price, addressid
schema for address: addressid, addressCountry, addressCity
The entity for book:
#Entity
#Table(name = "test_book")
public class Book implements Serializable {
private static final long serialVersionUID = 8025948150436422040L;
#Id
long id;
#Column(name = "name")
String name;
#Column(name = "author")
String author;
#Column(name = "price")
long price;
#ManyToOne
#JoinColumn(name = "addressid")
private Address address;
...//getter and setter
The entity for address
#Entity
#Table(name = "test_address")
public class Address implements Serializable{
private static final long serialVersionUID = -3541059157210384355L;
#Id
#Column(name= "addressid")
private long addressId;
#Column(name="addresscountry")
private String addressCountry;
#Column(name="addresscity")
private String addressCity;
#OneToMany(mappedBy = "address")
private Collection<Book> books;
...//getter setter
But when I call the Restful service, I get infinite loop...
[{"id":11,"name":"Java Book","author":"Jame Gosling","price":100,"address":{"addressId":1,"addressCountry":"China","addressCity":"Shanghai","books":[{"id":11,"name":"Java Book","author":"Jame Gosling","price":100,"address":...
I did some search. And my request is:
when I search a book, I can get the information: id, name, author, price, address..
And also I can query a address to get all the books the city published.
When I add Json Annotation #JsonManagedReference in address and #JsonBackReference in book entity, I can query book but cannot get address information.
Could you please help how to solve the problems? Thank you very much.
You can ignore the #JsonIgnore on getter for books. This will exclude the Collection<Book> books property from serialization the Address.
Link : JacksonAnnotations - Faster XML Wiki
Example:
#JsonIgnore
public Collection<Book> getBooks() {
...
}
#JsonIgnore
The Jackson annotation #JsonIgnore is used to tell Jackson to ignore a
certain property (field) of a Java object. The property is ignored
both when reading JSON into Java objects, and when writing Java
objects into JSON.
In your case this is happening as there is bidirectional relationship and so it will go into loop. To stop this you need to provide JsonIgnore
And so your code will be like :
-> The entity for address
#Entity
#Table(name = "test_address")
public class Address implements Serializable{
private static final long serialVersionUID = -3541059157210384355L;
#Id
#Column(name= "addressid")
private long addressId;
#Column(name="addresscountry")
private String addressCountry;
#Column(name="addresscity")
private String addressCity;
#OneToMany(mappedBy = "address")
#JsonIgnore
private Collection<Book> books;

How to map existing JPA entities to PicketLink

I am trying to migrate a Seam 2 app to CDI and use PicketLink for security. After all the reading and researching, it seems like all the examples are having one to one mapping between PicketLink model and the backend entity. e.g. Account to AccountEntity, Partition to PartitionEntity. Since I already have entities in place representing identity model, I am stuck on trying to map them to PicketLink. Here is what I have:
#MappedSuperClass
public class ModelEntityBase implement Serializable {
#Id #Generated
Long id;
Date creationDate;
}
#Entity
public Account extends ModelEntityBase {
String username;
String passwordHash;
#OneToOne(mappedBy = "account")
Person person;
}
#Entity
public Person extends ModelEntityBase {
String name;
String email;
#OneToOne
#JoinColumn(name = "account_id")
Account account;
}
Two entities (plus a super class) representing a single identity model in PicketLink, e.g. stereo type User.
Based on this why IdentityType id is String not Long, I tried to add a new Entity in:
#Entity
#IdentityManaged(BaseIdentityType.class);
public class IdentityTypeEntity implement Serializble {
#Id #Identifier
private String id;
#OneToOne(optional = false, mappedBy = "identityType")
#OwnerReference
private Account account;
#IdentityClass
private String typeName;
#ManyToOne #OwnerReference
private PartitionEntity partition;
}
I've tried a few different ways with the annotation and model classes. But when using IdentityManager.add(myUserModel), I just can't get it to populate all the entities. Is this even possible?
Got help from Pedro (PicketLink Dev). Post the answer here to help others.
This is the model class I ended up using.
#IdentityStereotype(USER)
public class User extends AbstractAttributedType implements Account {
#AttributeProperty
private Account accountEntity;
#AttributeProperty
#StereotypeProperty(IDENTITY_USER_NAME)
#Unique
private String username;
#AttributeProperty
private boolean enabled;
#AttributeProperty
private Date createdDate;
#AttributeProperty
private Date expiryDate;
#AttributeProperty
private Partition partition;
// getter and setter omitted
}
And created a new entity to map to this model:
public class IdentityTypeEntity implements Serializable {
#Id
#Identifier
private String id;
#OneToOne(optional = false, mappedBy = "identityType",
cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#AttributeValue
// #NotNull
private HAccount accountEntity;
#IdentityClass
private String typeName;
#ManyToOne
#OwnerReference
private PartitionEntity partition;
#AttributeValue
private String username;
#AttributeValue
// #Transient
private boolean enabled;
#AttributeValue
private Date createdDate;
#AttributeValue
private Date expiryDate;
}
PL can map property with #AttributeProperty to entity property with #AttributeValue. But it can only map to one entity. Therefore there is no way to map, say User and its properties over to Account and Person. But you can have the entity (in my case accountEntity) in the model. I also have to duplicate a few fields in the new IdentityTypeEntity and my existing Account entity (username, eanbled, createdDate) because PL requires these. Use a #PrePersist and similar to sync them.

#ManyToOne Lazy loading not working

I've seen other posts about this problem, but have found no answer to my own troubles. I have
#Entity
#Table(name= ServerSpringConstants.COMPANY)
public class Company implements Serializable {
private static final long serialVersionUID = -9104996853272739161L;
#Id #GeneratedValue(strategy=GenerationType.AUTO)
#Column (name = "companyID")
private long companyID;
#OneToMany (targetEntity = Division.class, cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.REFRESH},
fetch = FetchType.EAGER)
#JoinTable (name = "companyDivisionJoinTable",
joinColumns = #JoinColumn(name="companyID"),
inverseJoinColumns = #JoinColumn(name="divisionID")
)
private Set<Division> divisions = new HashSet<>();
public long getCompanyID() {
return companyID;
}
public Set<Division> getDivisions() {
return divisions;
}
public void setDivisions(Set<Division> divisions) {
this.divisions = divisions;
}
}
On the other side:
#Entity
#Table(name= ServerSpringConstants.DIVISION)
public class Division implements Serializable {
private static final long serialVersionUID = -3685914604737207530L;
#Id
#GeneratedValue(strategy= GenerationType.AUTO)
#Column(name = "divisionID")
private long divisionID;
#ManyToOne
(fetch = FetchType.LAZY, optional = false, targetEntity = Company.class,
cascade = {CascadeType.PERSIST, CascadeType.MERGE
}
)
#JoinColumn(name="companyID", referencedColumnName = "companyID")
private Company company;
public long getDivisionID() {
return divisionID;
}
public void setDivisionID(long divisionID) {
this.divisionID = divisionID;
}
public Company getCompany() {
return company;
}
public void setCompany(Company company) {
this.company = company;
}
}
Yet for some reason, LAZY loading not working. I'm using JPA. I'm calling back the companies, and their enclosing divisions from within a 'User' class -- the pertinent part
#ManyToMany (targetEntity = Company.class,
cascade={
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.REFRESH},
fetch=FetchType.EAGER )
#JoinTable (
name="companyUserJoinTable",
joinColumns=#JoinColumn(name="userID"),
inverseJoinColumns=#JoinColumn(name="companyID")
)
private Set<Company> company = new HashSet<>();
I've searched out existing threads, and have tried adding various suggestions, but nothing has helped!
Any help appreciated.
Thanks!
Since you are loading the divisions set eagerly (with fetch = FetchType.EAGER) and you have a bidirectional association, divisions will be initialized with the parent reference to company. I can't see any problem with it. Jpa loaded the full object tree because you just told it so. A company contains divisions which contain a back reference to the company that loaded them.
To understand it better, since the reason for lazy loading is to reduce the data loaded from database, the owning company is already loaded in session for the divisions, so why not setting the association too?
The #ManyToOne association on the other side takes effect if you load divisions first.
To be more correct with your mapping add also a #MappedBy attribute to the one part of the association. This does not affect fetching behavior but will prevent double updates to the database issued by both ends of the association.

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;
}

JPA Query Many To One nullable relationship

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,