Hibernate search with ResultTransformer (DTO) - java

I need to collect a specific DTO from the Hibernate search results, I connected all the dependencies in the maven, wrote the following request based on the official documentation (I deleted the unnecessary code, which can only confuse, left only what is needed for the search):
public List<QuestionDto> search(String text) {
FullTextQuery query = fullTextEntityManager.createFullTextQuery(queryBuilder
.simpleQueryString()
.onField("description")
.matching(text)
.createQuery())
.setProjection("id", "description", "title", "countValuable", "persistDateTime", "user.fullName", "tags")
.setResultTransformer(new ResultTransformer() {
#Override
public Object transformTuple(Object[] tuple, String[] aliases) {
return QuestionDto.builder()
.id(((Number) tuple[0]).longValue())
.title((String) tuple[2])
.description((String) tuple[1])
.countValuable(((Number) tuple[3]).intValue())
.persistDateTime((LocalDateTime) tuple[4])
.username((String) tuple[5])
.tags((List<TagDto>) tuple[6])
.build();
}
#Override
public List transformList(List collection) {
return collection;
}
});
return query.getResultList();
}
BUT for some reason instead of tags comes NULL
May be someone have any idea?
Entity Question
#Indexed
public class Question {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Field(store = Store.YES)
private String title;
private Integer viewCount = 0;
#Field(store = Store.YES)
private String description;
#Field(store = Store.YES)
private LocalDateTime persistDateTime;
#Field(store = Store.YES)
private Integer countValuable = 0;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#IndexedEmbedded(includePaths = "fullName")
private User user;
#ManyToMany(fetch = FetchType.LAZY)
#IndexedEmbedded(includeEmbeddedObjectId = true, includePaths = {"name", "description"})
private List<Tag> tags;
Entity Tag
public class Tag {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Field(store = Store.YES)
private String name;
#Field(store = Store.YES)
private String description;
private LocalDateTime persistDateTime;
#ManyToMany(mappedBy = "tags", fetch = FetchType.LAZY)
#ContainedIn
private List<Question> questions;

There are two problems here:
Projections on multi-valued fields are not supported.
You are trying to project on an "object" field: "tags" does not hold a value by itself, it just has sub-fields ("tags.name", "tags.description"). Projections on "object" fields is not supported at the moment.
If you're using the Elasticsearch backend, you can take advantage of the _source projection (org.hibernate.search.elasticsearch.ElasticsearchProjectionConstants#SOURCE), which will return the string representation of the Elasticsearch document, formatted as JSON. You can then parse it (with Gson, for example) to extract whatever information you need.
And of course, you always have the option of not using projections at all, then extracting the information from the entities loaded from the database.
Note that in Hibernate Search 6 (currently in Beta), we're going to add built-in support for projections on multi-valued fields, but it's unlikely to be added to Hibernate Search 5, which is in maintenance mode (no new features or improvements, only bugfixes).
In a more distant future, we will probably add more direct support for DTOs.

Related

Translate native sql to JPA code or JPQL and make it pageable and sortable

I have two Entities like Below:
#Entity
#Table(name="tb_sm_config")
class Config {
#Id
private Long id;
private String code;
private String name;
#Enumerated(EnumType.STRING)
private State state;
#JsonIgnore
#OneToMany(mappedBy = "config", fetch = FetchType.LAZY)
private List<ConfigItem> items;
}
#Entity
#Table(name="tb_sm_config_item")
class ConfigItem {
#Id
private Long id;
private String code;
private String name;
#Enumerated(EnumType.STRING)
private State state;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "config_id", referencedColumnName = "id", foreignKey = #ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Config config;
}
the State is an Enum, there are three states : VALID, INVALID, DELETED
Then I define a ConfigVO like below:
class ConfigVO {
private Long id;
private String code;
private String name;
private State state;
private Long validItemCount;
private Long invalidItemCount;
pirvate Long deletedItemCount;
}
I want to provide fuzzy query based on config.name, config.code and precise query based on config.state and return a list of ConfigVO with pageable and sortable.
I know native sql will like :
select a.id, a.code, a.name, a.state,
coalesce(b._valid, 0) validItemCount,
coalesce(b._invalid, 0) invalidItemCount,
coalesce(b._deleted, 0) deletedItemCount
from tb_sm_config a left join
(select config_id, sum(if(state = 'VALID', 1, 0)) _valid, sum(if(state = 'INVALID', 1, 0)) _invalid, sum(if(state = 'DELETED', 1, 0)) _deleted from tb_sm_config_item group by config_id) b
on a.id = b.config_id
where code like %:code% and name like %:name% and state = :state
But here are the problems I don't know how to deal with:
The frontend will not always pass the code, name, state to query, so these three parameters is nullable.
If I use the #Query(nativeQuery=true, value=xxxx) in the ConfigRepository interface method, I don't know how to deal with the null value and the paging and sorting.
Is that any possible to achieve this goal through JpaSpecificationExecutor interface, Example interface, or something else?
I want to query base on the code, name, state, sometimes they will be null and want to count the total number of individual configItem states and pageable and sortable.

Column 'brand_id' cannot be null

I create new Product using Spring Boot / Hibernate / JPA and get Column 'brand_id' cannot be null error. I don't know why this error happen. Does anyone can explain where I was wrong?
Product:
#Entity
public class Product {
#javax.persistence.Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
#NotBlank(message = "Product name is required")
private String name;
private String image;
private String description;
private double price;
private int countInStock;
private double rating;
private int numReviews;
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "brand_id", updatable = false, nullable = false)
#JsonIgnore
private Brand brand;
#ManyToMany
#JoinTable(name = "product_category", joinColumns = #JoinColumn(name = "product_id"), inverseJoinColumns = #JoinColumn(name = "category_id"))
#JsonIgnore
private List<Category> categories;
Brand:
#Entity
public class Brand {
#javax.persistence.Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
#NotBlank(message = "Brand name is required")
private String name;
#OneToMany(cascade = CascadeType.REFRESH, fetch = FetchType.LAZY, mappedBy = "brand", orphanRemoval = true)
private List<Product> products;
Category:
#Entity
public class Category {
#javax.persistence.Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
#NotBlank(message = "Category name is required")
private String name;
#ManyToMany(mappedBy = "categories")
private List<Product> products;
And my Product Object that used to input create new Product:
{
"name": "test",
"image": "/images/test.jpg",
"description": "test",
"brand_id": 4,
"category_id": 1,
"price": 99.99,
"countInStock": 100,
"rating": 4.5,
"numReviews": 120
}
Wherever you are creating this Product object, you must provide it with a Brand object.
Currently, you are only providing it with an integer as a Brand.
What you need to do is fetch the brand object you are referring to from your DB and include it in the instantiation of your new Product.
Probably the best and simplest approach to this would be using the EntityManager to get a reference to that Brand.
Getting the EntityManager is very simple when using spring.
#PersistenceContext
private EntityManager entityManager;
Now, simply use it to get the reference to the target Brand.
Brand brand = entityManager.getReference(Brand.class , brand_id);
Use this brand to instantiate your new Product and insert it to the DB without any exceptions.
How to automate this logic into the unmarshalling process
If you are always going to want to use this logic when creating a Product, you can use this logic in the constructor. If you only want to use this method when unmarshalling, here is an example that is based of something I wrote recently. I am using an XmlAdapter, but there are also JSONAdapater classes you can look into and should work about the same way.
Create you adapter class. This class is going to be used to parse JSON to java object.
//Once again, I am using an XmlAdapter, but the idea should be similar with JSONAdapters
#Service
public class BrandIdXmlAdapter extends XmlAdapter<String, Brand> {
#PersistenceContext
private EntityManager entityManager;
//v is the String that is going to be unmarshalled.
//In our case, its going to be the brand_id String.
#Override
public Brand unmarshal(String v) {
Brand brand = entityManager.getReference(Brand.class, v);
return brand;
}
There is also a possibility to override the marshal() method for parsing from a POJO to XML/JSON.
The only problem here is that to be able to use use the PersistenceContext annotation, this class has to be an EJB.
We are going to workaround that by telling Spring this is a necessary service.
First step is to give the adapter class the Service annotation(Done in example above).
The next step is to go to where you would want to unmarshall the input into a POJO (either the controller if you receive it as a request or the service if you are going to request it from another service) and autowire the adapter
#Autowired
private BrandIdXmlAdapter xmlAdapter;
Next step is to create the unmarshaller that will use this adapter.
JAXBContext context = JAXBContext.newInstance(Product.class);
brandIdUnmarshaller = context.createUnmarshaller();
brandIdUnmarshaller.setAdapter(xmlAdapter);
Now when receiving the data, use the brandIdUnmarshaller.unmarhsall() method.
Last step is to annotate your Brand variable in Product to tell it to use the adapter when parsing this specific variable.
public class Product {
.
.
.
//again, find the right annotation according to your JSONAdapter
#XmlJavaTypeAdapter(BrandIdXmlAdapter.class)
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "brand_id", updatable = false, nullable = false)
#JsonIgnore
private Brand brand;
}
Now everytime you parse from JSON to Product, you will automatically get a Product that contains a valid Brand object.

efficiently loading collection of collections from database in hibernate

I have an web application with hibernate which manages data in multiple languages. Currently basically every request generates a shower of select statements on the languagetranslations. The models are roughly as following:
Data <1-1> Placeholder <1-many> languageTranslation <many-1> language
If I query for all/many Dataobjects, I see lots of single selects which select one languageTranslation for the placeholder. The SQL I optimally would want to generate:
SELECT * FROM data join placeholder join languagetranslation
WHERE data.placeholder_id = placeholder.id
AND languagetranslation.placeholder_id = placeholder.id
AND languagetranslation.language_id = ?
so that I get every data with placeholder with translation in one single call. The languagetranslations have an composite primary key of language_id and placeholder_id.
I have no HBM file, everything is managed with annotations. Modelcode (only relevant sections are shown):
#Entity
public class Data {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL, optional = false)
#Fetch(FetchMode.JOIN)
private Placeholder content;
}
public class Placeholder {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#OneToMany(mappedBy = "primaryKey.placeholder", cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
#Fetch(FetchMode.JOIN)
private Set<LanguageTranslation> languageTranslations = new HashSet<>();
}
public class LanguageTranslation {
#EmbeddedId
private LanguageTranslationPK primaryKey = new LanguageTranslationPK();
#Type(type = "org.hibernate.type.StringClobType")
private String text;
}
#Embeddable
public class LanguageTranslationPK {
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
private TextPlaceholder textPlaceholder;
#ManyToOne(fetch = FetchType.EAGER)
#Fetch(FetchMode.JOIN)
private Language language;
}
public class Language {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
}
I experimented with FetchType and FetchMode but couldn't generate the behavior I want, it always single selects for single languageTranslations.
I also tried multiple ways to query, criteria based, HQL, and raw SQL. My current raw SQL query is the following:
String sql_query = "select data.*, lt.* from Data as data join languagetranslation as lt on data.content_id = lt.textplaceholder_id";
Query q = getSession().createSQLQuery(sql_query).addEntity("data", Data.class).addJoin("data.content_id", "data.title").addJoin("lt", "data.content.languageTranslations").setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
return q.list();
Am I doing something generally wrong here? How can I convince hibernate to get all entities in one single database call? Or is there some other methods to improve performance in my case (e.g. batch selecting)?
You may create proxy pojo which have your all entity variables with getter setter and constructor. then initialize this constructor in hibernate query so that you just get all needed data from database.
import com.proxy;
class userProxy{
private string name;
private string password;
private string address;
private int pincode;
private byte[] profilePic;
private int age;
public userProxy(string name,string password){
this.name = name;
this.password = password;
}
//Getter and setter of all variable...
}
Then use this constructor to Hibernate query like
select new com.proxy.userProxy(user.name,user.password) from usertable
Am I doing something generally wrong here?
No, you are not. That is how Hibernate works.
How can I convince hibernate to get all entities in one single database call
You have to use HQL or SQL query to do that. You do not need to have HBM file. It can be done through #NamedQueries / #NamedQuery annotation with list method.
There are many samples on Internet as example simple one:
http://www.mkyong.com/hibernate/hibernate-named-query-examples/

Ebean and Play! not filtering columns with .select()

I'm trying to fetch just a part of the model using Ebean in Play! Framework, but I'm having some problems and I didn't found any solutions.
I have these models:
User:
#Entity
#Table(name = "users")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class User extends Model{
#Id
private int id;
#NotNull
#Column(name = "first_name", nullable = false)
private String firstName;
#Column(name = "last_name")
private String lastName;
#NotNull
#Column(nullable = false)
private String username;
#NotNull
#Column(nullable = false)
private String email;
private String gender;
private String locale;
private Date birthday;
private String bio;
#NotNull
#Column(nullable = false)
private boolean active;
private String avatar;
#Column(name = "created_at",nullable = false)
private Date createdAt;
#OneToMany
private List<UserToken> userTokens;
// Getters and Setters omitted for brevity
}
UserToken:
#Entity
#Table(name = "user_tokens")
public class UserToken extends Model {
#Id
private int id;
#Column(name = "user_id")
private int userId;
private String token;
#Column(name = "created_at")
#CreatedTimestamp
private Date createdAt;
#ManyToOne
private User user;
// Getters and Setters omitted for brevity
}
And then, I have a controller UserController:
public class UserController extends Controller{
public static Result list(){
User user = Ebean.find(User.class).select("firstName").where().idEq(1).findUnique();
return Results.ok(Json.toJson(user));
}
}
I expected that, when using the .select(), it would filter the fields and load a partial object, but it loads it entirely.
In the logs, there is more problems that I don't know why its happening.
It is making 3 queries. First is the one that I want. And then it makes one to fetch the whole Model, and another one to find the UserTokens. I don't know why it is doing these last two queries and I wanted just the first one to be executed.
Solution Edit
After already accepted the fact that I would have to build the Json as suggested by #biesior , I found (out of nowhere) the solution!
public static Result list() throws JsonProcessingException {
User user = Ebean.find(User.class).select("firstName").where().idEq(1).findUnique();
JsonContext jc = Ebean.createJsonContext();
return Results.ok(jc.toJsonString(user));
}
I render only the wanted fields selected in .select() after using JsonContext.
That's simple, when you using select("...") it always gets just id field (cannot be avoided - it's required for mapping) + desired fields, but if later you are trying to access the field that wasn't available in first select("...") - Ebean repeats the query and maps whole object.
In other words, you are accessing somewhere the field that wasn't available in first query, analyze your controller and/or templates, find all fields and add it to your select (even if i.e. they're commented with common HTML comment in the view!)
In the last version of Play Framework (2.6) the proper way to do this is:
public Result list() {
JsonContext json = ebeanServer.json();
List<MyClass> orders= ebeanServer.find(MyClass.class).select("id,property1,property2").findList();
return ok(json.toJson(orders));
}

PersistenceException, Column 'id' specified twice

I have the following files in Play Framework 2.2.3
Controller:
public class Comment extends Controller
{
public Result create(UUID id)
{
models.blog.Blog blog = models.blog.Blog.finder.byId(id);
Result result;
if(blog == null)
{
result = notFound(main.render("404", error404.render()));
}
else
{
Form<models.blog.Comment> commentForm = Form.form(models.blog.Comment.class);
commentForm = commentForm.bindFromRequest();
if(commentForm.hasErrors())
{
result = badRequest(Json.toJson(commentForm));
}
else
{
models.blog.Comment comment = commentForm.get();
comment.setId(UUID.randomUUID());
comment.setTimeCreated(new Date());
comment.setBlogId(blog.getId());
comment.save();
result = ok(Json.toJson(comment));
}
}
return result;
}
}
And two models
#Entity
#Table(name="blog")
public class Blog extends Model
{
private static final SimpleDateFormat MONTH_LITERAL = new SimpleDateFormat("MMMMM"),
DAY_NUMBER = new SimpleDateFormat("d"),
YEAR_NUMBER = new SimpleDateFormat("yyyy");
public static Finder<UUID, Blog> finder = new Finder<UUID, Blog>(UUID.class, Blog.class);
#Id
#Column(name="id",length=36, nullable=false)
public UUID id;
#OneToOne
#JoinColumn(name="author_id")
public User author;
#Column(name="title",length=255)
public String title;
#Column(name="summary",length=255)
public String summary;
#Column(name="url",length=255)
public String url;
#Column(name="content")
public String content;
#Column(name="time_updated")
public Date time_created;
#Column(name="time_created", nullable=false)
public Date time_updated;
#OneToMany(cascade = CascadeType.ALL)
#JoinColumn(name="blog_id")
public List<Comment> comments;
#ManyToMany(cascade = CascadeType.ALL)
#JoinTable(
name="blog_tag_map",
joinColumns={ #JoinColumn(name="blog_id", referencedColumnName="id") },
inverseJoinColumns={ #JoinColumn(name="tag_id", referencedColumnName="id") }
)
public List<Tag> tags;
public List<Comment> getComments()
{
return this.comments;
}
}
#Entity
#Table(name="blog_comment")
public class Comment extends Model
{
private static final SimpleDateFormat MONTH_LITERAL = new SimpleDateFormat("MMMMM"),
DAY_NUMBER = new SimpleDateFormat("d"),
YEAR_NUMBER = new SimpleDateFormat("yyyy");
#Id
#Column(name="id",length=36, nullable=false)
public UUID id;
#Column(name="blog_id", length=36)
public UUID blog_id;
#ManyToOne
public Blog blog;
#Column(name="content", length=500)
public String content;
#Column(name="website", length=255)
public String website;
#Column(name="name", length=255)
public String name;
#Column(name="time_created", updatable=false)
public Date time_created;
}
I have excluded some setters and getters from these models for brevity, so it doesn't clog up this post.
When I attempt to make a POST request to the aforementioned controller, everything goes fine until I get to the "comment.save()" statement in the controller file, then I get the following error.
I'm unsure why this save isn't going through, and why there is a column conflict.
Help much appreciated
The issue lies in the fact that you have defined basically two foreign key columns for Blog in your Comment's entity:
#Column(name = "blog_id", length = 36)
public UUID blog_id;
#ManyToOne
public Blog blog;
The default column name for your 'blog' field is: blog_id
However, you've already named your 'blog_id' column that.
Interestingly, no error/warning is thrown when creating this table...
So when you call comment.save(), the following insert statement is generated:
insert into blog_comment (id, blog_id, content, website, name, time_created, blog_id) values (?,?,?,?,?,?,?)
Notice a reference to 'blog_id' column twice, which is invalid.
And this is because of the above double mapping.
To fix, just give your 'blog' property a different name to use for the foreign key column:
#Column(name = "blog_id", length = 36)
public UUID blog_id;
#ManyToOne
#JoinColumn(name = "blogId")
public Blog blog;
I'm not sure why you're mapping your entities like this (perhaps legacy schema?) but the 'blog_id' fields seem to be redundant (and confusing) as you already have an entity mapping in the form of your 'blog' property.
This question is pretty old, but for any future reference i have found this answer that solved my problem.
After numerous searchers around the web I found this answer here - thanks to jtal!
Just to summaries the problem:
Using Ebean i have made a #ManyToOne entity that is not implemented in the database in anyway,
even more the join field, in your case
blogId
is a valid field that has values of its own.
when trying to join the column on that field, it will always fail because it creates this sql query:
SELECT
*
FROM
blog_comment;
select
t0.id c0,
t0.blog_id c1,
t0.content c2,
t0.website c3,
t0.time_created c4,
t0.blog_id c5 <---- notice this duplicate
from
blog_comment t0
in order to solve this, i tell ebean not to use the second set of properties.
your new ebean element should look something like this:
#ManyToOne
#JoinColumn(name = "blogId", insertable = false, updatable = false)
public Blog blog;
hope this helps! =)

Categories

Resources