Sorting on embedded entity name in hibernate search - hibernate-search

I have the following Entity definitions.
public class Order {
#Id
#DocumentId
private Long id;
#Field
#IndexedEmbedded(includePaths = {"name"})
#OneToOne
#JoinColumn(name = "ACCOUNT_ID")
private Account account;
// the rest are omitted for brevity purpose
}
public class Account {
#Id
#DocumentId
private Long id;
#SortableField(forField = "name_Sort")
#Field(name = "name_Sort", store = Store.YES, normalizer= #Normalizer(definition = SearchConstants.LOWER_CASE_NORMALIZER))
#Column(name = "NAME")
private String name;
}
If I search on Order and want to have the search results sorted by account name, is there good way of doing so possibly using the embedded indexed annotation? I know we can do it by adding an extra string field in Order that is called accountName, then just add sorting annotation on top of that. Is it possible to achieve this without specifying the sorting annotation in Order but just use the sorting annotation that is already defined in Account?

Change this:
#IndexedEmbedded(includePaths = {"name"})
To this:
#IndexedEmbedded(includePaths = {"name", "name_Sort"})
Then you can use the field account.name_Sort for sorts on orders:
QueryBuilder builder = fullTextSession.getSearchFactory()
.buildQueryBuilder().forEntity( Order.class ).get();
Query luceneQuery = /* ... */;
FullTextQuery query = s.createFullTextQuery( luceneQuery, Order.class );
query.setSort(
builder.sort().byField("account.name_Sort").createSort()
);
List results = query.list();

Related

Check searchable field of an entity based on indexing

I have an entity defined as following.
public class Deal {
#Id
#DocumentId
#Column(name = "ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#Field
#Column(name = "NAME")
private String name;
#Field
#Column(name = "ADVERTISER_NAME")
private String advertiserName;
#Field
#Column(name = "BRAND_NAME")
private String brandName;
//other fields and getters/setters omitted for brevity
}
If I want to search on all searchable fields, I can do something like the following, which demonstrates the usage of onField, onFields and andFields.
Query luceneQuery1 = mythQB
.simpleQueryString()
.onFields("name", "history", "description")
.matching("teststring")
.createQuery();
Query luceneQuery2 = mythQB
.simpleQueryString()
.onField("name")
.boostedTo(5f)
.andFields("advertiserName", "brandName")
.boostedTo(2f)
.withAndAsDefaultOperator()
.matching("teststring")
.createQuery();
If I add Index.NO to #Field(making an entity field not searchable),for example, change the annotation of brandName to #Field(index = Index.NO), now I only have two searchable fields: name and advertiserName, if we don't consider id. In this case, the example query above will throw runtime exception because it tries to search on brandName that is not searchable.
I've tried something like the following to dynamically get the full list of searchable fields based on whether a field has annotation or not. But this won't work if the index is Index.NO.
My question is that is there a way to dynamically get the full list of searchable fields based on the actual index value?
protected String[] getSearchableFields() {
List<String> fields = Lists.newArrayList();
Class<?> c = clazz;
while (c != null) {
for (Field field : c.getDeclaredFields()) {
if (field.isAnnotationPresent(org.hibernate.search.annotations.Field.class)
|| field.isAnnotationPresent(org.hibernate.search.annotations.Fields.class)) {
if (field.getType().isAssignableFrom(String.class)) {
fields.add(field.getName());
}
}
}
c = c.getSuperclass();
}
return fields.toArray(new String[fields.size()]);
}
There is a metadata API in Hibernate Search.
FullTextSession ftSession = ...;
IndexedTypeDescriptor indexedType = ftSession.getSessionFactory().getIndexedTypeDescriptor(clazz);
for (PropertyDescriptor property : indexedType.getIndexedProperties()) {
for (FieldDescriptor field : property.getIndexedFields()) {
if (field.getIndex() == Index.YES) {
// do something
}
}
}
See this section of the reference documentation for more information.
There are a few limitations, like not being able to "see" the extra fields contributed by custom bridges. But it works well apart from that.

Spring projections select collection

I am attempting to have a station projection include a list of associated logos. Below is my domain:
#Table(name = "Station")
public class Station implements Serializable {
#Id
#Column(name = "Id")
private int id;
#OneToMany(cascade = CascadeType.ALL,
fetch = FetchType.LAZY,
mappedBy = "station")
private Set<Logo> logos;
}
The #OneToMany associated logos:
#Table(name = "Logo")
public class Logo {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Transient
private String fullUrl; // calculated
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "StationId", nullable = false)
private Station station;
}
My repository and query is as follows:
#Query(value = "SELECT s.id AS Id, s.logos As Logos FROM Station s JOIN s.users su WHERE su.username = ?1")
Collection<StationListViewProjection> findStationsByUsername(String username);
My station projection expects the Id and a list of logoProjections
#Projection(name = "StationListViewProjection", types = Station.class)
public interface StationListViewProjection {
int getId();
Set<LogoProjection> getLogos();
}
The logoProjection only needs the url
#Projection(name = "LogoProjection", types = Logo.class)
public interface LogoProjection {
String getFullUrl();
}
When i execute my query I get a strange response:
MySQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'as col_5_0_, . as col_6_0_, stationlog3_.id as id1_20_0_'
If I understand this
#Transient
private String fullUrl; // calculated
correct, your fullUrl gets calculated inside your java code and more important, it doesn't have a matching column in the database. You can't use such field in projections directly. You might be able to use an Open Projection and specify the calculation to obtain the fullUrl using a #Value annotation.

JPA Criteria Query: how to automatically create LEFT JOIN instead of WHERE conditions

I have two entity:
public class public class Person implements Serializable {
private static final long serialVersionUID = -8729624892493146858L;
#Column(name="name")
private String name;
...
#JoinColumn(name = "idcity",referencedColumnName = "id",nullable = true)
#ManyToOne(targetEntity = City.class, fetch = FetchType.EAGER)
private City city
...
}
and the related entity (extract):
public class City{
Long id;
String name;
...
}
Now i'm creating a criteria query in a standard way, querying the Person class:
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
CriteriaQuery query = cb.createQuery(selectClass);
Root<T> root = query.from(this.entityClass);
Selection selezioni[] = new Selection[selections.length];
for(int i=0; i< selections.length; i++){
selezioni[i] = CriteriaHelper.getField(selections[i], cb, root);
}
query.select(cb.construct(selectClass, selezioni));
where entityClass is Person and selection and selectClass are used to compile the SELECT clause. In the select i've person.city.name field.
This system create a query with where clause:
select person.name, ..., city.name from person, city WHERE person.idcity = city.id...
but city is not required, so the records without city are not fetched.
Without changing all my automatic system, does exists a simpler way to force the use on LEFT JOIN for the relationship than adding a system to create root.join("field",LEFT)?
Note: the method CriteriaHelper.getField() return a Path starting from the root object

Hibernate Filter being ignored

My application uses Hibernate 5.02 and Wildfly 10 with a PostgreSQL 9.5 database. I'm trying to enable a filter on a #OneToMany collection held within an entity that is constructed via a NamedQuery. Unfortunately, it seems as if the filter is just ignored. Here are the different components, redacted for ease of reading.
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "analystprocess_id IN (:processIds) AND id NOT IN (SELECT msg_id FROM analysis.analyzedmsg WHERE analyst_id IN (:analystIds) AND analystprocess_id IN (:processIds)) ORDER BY process_msg_id")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
private void buildAnalystProcess() {
LOG.info("Building AnalystProcessEntity");
analystUser.getJdbcSession().enableFilter(AnalystProcessEntity.MSG_FILTER)
.setParameter(AnalystProcessEntity.MSG_FILTER_PROC_ID_PARAM, analystProcessId)
.setParameter(AnalystProcessEntity.MSG_FILTER_ANALYST_ID_PARAM, analystUser.getId());
Query query = analystUser.getJdbcSession().getNamedQuery(AnalystProcessEntity.GET_PROCESS)
.setParameter("processId", analystProcessId);
// Query query = analystUser.getJdbcSession().createNativeQuery("SELECT * FROM analysis.analystprocess WHERE id = :processId")
// .setParameter("processId", analystProcessId)
// .addEntity(AnalystProcessEntity.class);
analystProcess = (AnalystProcessEntity) query.getSingleResult();
CREATE TABLE analysis.analystprocess (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
description TEXT,
created_date TIMESTAMP NOT NULL DEFAULT now(),
...
);
CREATE TABLE analysis.msg (
id SERIAL PRIMARY KEY,
analystprocess_id INTEGER NOT NULL REFERENCES analysis.analystprocess(id) ON DELETE CASCADE ON UPDATE CASCADE,
process_msg_id INTEGER NOT NULL,
constraint tbl_statusid_analystprocessid unique(status_id, analystprocess_id)
);
As seen above, I have also tried the filter on constructing the AnalystProcessEntity class via createNativeQuery instead of getNamedQuery and no luck.
I also added a defaultCondition with hardcoded values into the #FilterDef just to see if it would execute the default condition and it still didn't.
I've tried the #Filter above the entity definition as well as above the class definition. I even came across a blog post which made it sound like the condition references entity fields (variable names) and not table fields (column names). Trying to stick to Java naming conventions in the Entity and Postgres naming conventions in the table, so I tried switching the references in the condition and to no avail.
I have sql logging turned on in Hibernate and the condition doesn't show up anywhere, as if it's just simply being ignored.
Any help would be greatly appreciated!
So, the problem was that I had the #FilterDef applied to the wrong class. It was my presumption that because I was constructing the AnalystProcessEntity which holds the MsgEntity collection (which I am trying to filter), that the #FilterDef would be applied to the AnalystProcessEntity class. Instead, it needs to be applied to the entity that it's actually filtering (hindsight being 20/20, that's pretty obvious).
Also, the actual condition needed to be modified to use complete references within the sub-select query.
I hope this helps someone at some point...
#NamedNativeQueries({
#NamedNativeQuery(
name = "getAnalystProcess",
query = "SELECT * FROM analysis.analystprocess WHERE id = :processId",
resultClass = AnalystProcessEntity.class
)})
#Filter(name = "analystProcessUnanalyzedMsgsFilter", condition = "id NOT IN (SELECT amsg.msg_id FROM analysis.analyzedmsg amsg WHERE amsg.analyst_id IN (:analystIds) AND amsg.analystprocess_id IN (:processIds))")
#Entity
#Table(name = "analystprocess", schema = "analyst")
public class AnalystProcessEntity implements JPAEntity {
public static final String GET_PROCESS = "getAnalystProcess";
public static final String MSG_FILTER = "analystProcessUnanalyzedMsgsFilter";
public static final String MSG_FILTER_PROC_ID_PARAM = "processIds";
public static final String MSG_FILTER_ANALYST_ID_PARAM = "analystIds";
private static final long serialVersionUID = 1L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, mappedBy = "process")
#OrderColumn(name = "process_msg_id")
#LazyCollection(LazyCollectionOption.EXTRA)
private List<MsgEntity> msgList;
#FilterDef(
name = "analystProcessUnanalyzedMsgsFilter",
parameters = { #ParamDef(name = "processIds", type = "integer"), #ParamDef(name = "analystIds", type = "integer") })
#Entity
#Table(name = "msg", schema = "analyst")
public class MsgEntity implements JPAEntity {
...
#ManyToOne(cascade = CascadeType.ALL, optional = false)
#JoinColumn(name = "analystprocess_id", referencedColumnName = "id")
private AnalystProcessEntity process;
#Column(name = "process_msg_id")
private Integer processMsgId;
Additionally, I ran into another problem with null's appearing in the collection, despite the fact that I am using an #OrderColumn, which I thought fixed that issue. It seems that with the use of the #Filter, null's are inserted in place of what ended up being filtered OUT (excluded).

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,