Conditional load of relations using spring data jpa - java

I'm developing a multi language application and My tables are designed for this purpose as well. for example I have a Country class like this:
#Entity
#Table(name = "province")
public class Province {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
#ManyToOne
#JoinColumn(name = "country_id", nullable = false)
private Country country;
#OneToMany
#JoinColumn(name = "province_id")
private List<ProvinceTranslation> translations;
}
#Entity
#Table(name = "province_translation")
public class ProvinceTranslation {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String name;
private Language language;
#ManyToOne
#JoinColumn(name = "province_id")
private Province province;
}
I want that translations field load only translation with specified language, and country field load translation with specified language too(Country class has a list of CountryTranslation obviously!). I don't want to write queries and I want that spring data jpa load relations with the language that I specify explicitly.

It seems, a little bit of writing JPA query is required in this case.
Because the countryTranslation class is missing I put the focus on the Province class.
The Language class is also unknown, maybe this is an enum one, like this:
public enum Language {UNKNOWN, GERMAN, ENGLISH, SPAIN}
To avoid loading of all translations of the entity you have to select the translation according to the given language while fetching the entities from database. I prefer to do this with utilizing spring repositories (which I hope you already have involved). In the ProvinceRepository declared like this
public interface ProvinceRepository extends CrudRepository<Province, Long> {
...
}
you have to provide the required find- or count methods.
To get a List of all provinces with specific translation you may declare a function like this one, inside the ProvinceRepository:
#Query("SELECT new org.your.package.goes.here.Province(p.id, p.country, pt.name) FROM Province p inner join p.translations pt where pt.language = ?1")
List<Province> findAllWithTranslation(Language language);
To make this working, there must a exist a constuctor that accepts the three parameters id, country, name. The name parameter may be set to a new translation property of the Province class, respectively the created province object. If the Language class is indeed an enum class the #Enumerated annotation must be added to the language field.
Nevertheless, I am convinced that the provision of translation strings should be better done with the help of an internationalization library (i18n).

Related

Spring Data/Hibernate Generates two queries instead of a JOIN

Context: I have two tables: Questionnaire and Question Section. A Questionnaire can have many Question Sections. Questionnaires and Question Sections both have Start and End Dates to determine if they are active records.
Here are my entities as written:
#Entity
#Data
public class Questionnaire {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private Date startDate;
private Date endDate;
private String description;
#OneToMany(cascade = CascadeType.All,
fetch = FetchType.LAZY,
mappedBy = "questionnaire")
#JsonManagedReference
private List<QuestionSection> questionSections = new ArrayList<QuestionSection>();
}
#Entity
#Data
public class QuestionSection {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;
private String name;
private String description;
private int sectionLevel;
private Date startDate;
private Date endDate;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "QUESTIONNAIRE_ID", nullable = false)
#JsonBackReference
private Questionnaire questionnaire;
}
Here is my Spring Data Repository with a single declared method:
public interface QuestionnaireRepository extends JpaRepository<Questionnaire, UUID> {
Questionnaire findByNameAndEndDateIsNull(String name);
// Previous goal query, but worked all the way back to the above simple query
// Questionnaire findByIdAndQuestionSectionsEndDateIsNull(UUID id);
}
The above derived query generates two queries shown below:
-- For brevity
select questionnaire.id as id
questionnaire.description as description
questionnaire.end_date as end_date
questionnaire.start_date as start_date
from questionnaire
where questionnaire.name='Foo' and (questionnaire.end_date is null)
select questionsection.questionnaire_id as questionnaire id
...rest of fields here...
from question_section
where questionsection.questionnaire_id = id from above query
Then Spring Data or Hibernate is combining those two above queries into one data object representative of the questionnaire object and returning that.
My problem with this is that I would have expected One query to run with a Join between the two tables, not two and then combine the results in memory. I'm pretty experienced with Spring Data and ORMs in general and have not been able to find any documentation as to why this is happening. Honestly I wouldn't care except that my original intention was to query at the parent entity and 'filter' out children that have end dates (not active). This derived query (commented out above) exhibited the same behavior which ultimately resulted in the data set that was returned containing the end dated question sections.
I know there's 100 other ways I could solve this problem (which is fine) so this is more of an educational interest for me at this point if anyone has any insight into this behavior. I could be missing something really simple.
You should be able to do this using the Entity Graph feature introduced in JPA 2.1.
https://www.baeldung.com/jpa-entity-graph
Spring Data offers support for Entity Graphs via the #NamedEntityGraph and #EntityGraph annotations:
https://www.baeldung.com/spring-data-jpa-named-entity-graphs
So in your code:
Entity:
#Entity
#NamedEntityGraph(name = "Questionnaire.questionSections",
attributeNodes = #NamedAttributeNode("questionSections ")
)
public class Questionnaire{
//...
}
Repository:
public interface QuestionnaireRepository extends JpaRepository<Questionnaire, UUID> {
#NamedEntityGraph("Questionnaire.questionSections")
Questionnaire findByNameAndEndDateIsNull(String name);
}
public interface QuestionnaireRepository extends JpaRepository<Questionnaire, UUID> {
#EntityGraph(attributePaths = { "questionSections" })
Questionnaire findByNameAndEndDateIsNull(String name);
}

Using a Single DTO object for projection

I'm having trouble about having to use a single DTO object or a DTO object for every Entity object.
For example I have 3 classes: Book, Author and Publisher.
Book.java
#Entity
#Table(name = "tbl_book")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "description")
private String description;
other different fields...
}
Author.java
#Entity
#Table(name = "tbl_author")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "description")
private String description;
other different fields...
}
Publisher.java
#Entity
#Table(name = "tbl_publisher")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private Long id;
#Column(name = "description")
private String description;
other different fields...
}
and MyDTO.java
public class MyDTO {
private Long id;
private String description;
constructor id, description...
getters and setters...
}
I want to select only specific fields (id and description) with EntityManager:
em.createQuery("SELECT NEW MyDTO(id,description) FROM Book");
But my question is should I use a single DTO object(which is MyDTO) for all projection? Something like:
em.createQuery("SELECT NEW MyDTO(id,description) FROM Author");
em.createQuery("SELECT NEW MyDTO(id,description) FROM Publisher");
Tutorials about using DTO for projection just saying use DTO for read and use Entity for write but they are not telling about having a single object DTO or not. Will you please provide an example why single DTO or why DTO for every Entity. Thank you!
I've been working with DTOs for a while now, and here's some things I can share:
Try building a DTO hierarchy based on your needs (abstract classes and/or interfaces)
Even if your BookDTO 'just' extends a superclass (AbstractIdDescriptionDTO for instance), it's always more readable (IMO), and it's less tedious to maintain, and you can't misinterpret an object for another
I never use the new DTO syntax, I implemented an automatic converter instead
In my opinion create a DTO for each field. Having a single DTO couples your objects a lot. For example if in Book class you change the description to something else all your code will not work anymore.
Even if you start with thinking of a good name for the DTO you will see the problem. "MyDTO" doesn't sounds good and doesn't show its purpose but you won't be able to think of a meaningful name.
And last but not least there is the single responsibility principle that is a good thing and having one DTO to present multiple objects kind of breaks it. It's not a good practice and breaks the SOLID principle to have one class that represents different types of objects. If your projects share some common properties it's better to have separate classes and create an Interface for the common part. That's just standard OOP design. It's not related to DTOs
That's just my opinion. I might be wrong.
Also creating the DTOs like that in the JPQL query kind of makes it hard coded and harder to maintain. You can do something like:
.setResultTransformer( Transformers.aliasToBean( PostDTO.class ) )
for your query or there are other ways to do it.

Crnk JsonApiRelation, OneToMany and filtering implementation

I use crnk (JSON-API) in java project and I have 3 questions regarding its usage with spring boot and jpa - haven't found exact implementation details in documentation.
For example, I have 2 entities and respective tables:
#Entity
#JsonApiResource(type = "employee")
public class Employee {
#Id
#JsonApiId
private int id;
private String name;
#ManyToOne
#JoinColumn(name = "typeId")
private EmployeeType employeeType; //stored in table as typeId
}
#Entity
#JsonApiResource(type = "type")
public class EmployeeType {
#Id
#JsonApiId
private int id;
private String typeName;
private int salary;
}
How should JsonApiRelation be introduced in order to be able to call "/employee/1" and "/employee/1/type" urls?
For example there is one more entity.
#Entity
#JsonApiResource(type = "project")
public class Project {
#Id
#JsonApiId
private int id;
private String supervisorName;
private String projectName;
}
First, I'd like to have List of Projects for each Employee, where he is a supervisor, joint by name and have it listed as attribute in Json.
Tried implementing it with #OneToMany and #JoinColumn annotations but got StackOverflowException. How could this be implemented. And second, how could this be implemented with Relation? Like "/employee/1/projects" url.
How should I implement custom filtering of results for findAll method? For example, I have a List of all Employees, but I'd like to exclude some of them from the response. Which class/method should be introduced for this behaviour?
#JsonApiRelation annotation should not be necessary. Crnk will detect the #ManyToOne annotation and map it accordingly.
in case of crnk-jpa it is sufficient to specify all relationships in JPA. Matching JSON API relationships. So your approach seems good. What was the StackoverflowException stacktrace? (next to the examples, there are also many example entities in crnk-jpa)
I would make use of a decorator. See http://www.crnk.io/documentation/#_request_filtering. RepositoryDecoratorFactory allows to place a custom repository between the caller and crnk-jpa (or any other kind of repository). There you can do any kind of modification perform (maybe) calling the "real" repository. => Will add an example for this
feel free also make open up tickets in crnk for any documentation/example clarifications.

Hibernate disable default behaviour

I have a problem with my Hibernate making assumptions on what to call columns.
Specifically, when I do a #ManyToOne field, where I refer to a column in the other Table.
What happens is that, If I do not enter a #JoinColumn annotation as well, it maps the field with an underscore in it's name.
For example, i have this class:
#Entity
public class User extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
private String username;
private String password;
#OneToMany(fetch = FetchType.LAZY, mappedBy = "userFK")
private List<TwitterAccount> twitterAccounts;
/* GETTERS & SETTERS OMITTED */
}
And then I have the TwitterAccount class:
#Entity
public class TwitterAccount extends AbstractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
#ManyToOne(fetch = FetchType.LAZY)
private User userFK;
}
What happens is that when it tries to get the Twitter accounts for a certain user, I get the exception: MySQLSyntaxErrorException: Unknown column 'twitteracc1_.userFK_id' in 'field list'
Look at what it tries to map the userFK to: userFK_id. Of course it doesn't exist! I haven't given it that name.
So the question comes down to: Is it possible to turn off this functionality? (The functionality that turns the column name into 'field_'foreignkey')
I am aware that using #JoinColumn(name = "userFK") would solve it, but I'd rather turn it off instead.
Regards
This is the default as specified by the JPA specification
The concatenation
of the following: the name
of the referencing relationship
property or field of the referencing
entity or embeddable class;
""; the name of the referenced
primary key column. If there is no
such referencing relationship
property or field in the entity, or if
the join is for an element collection,
the join column name is
formed as the concatenation of the
following: the name of the entity;
""; the name of the referenced
primary key column.
In hibernate this is implemented in a NamingStrategy in this case the EJB3NamingStrategy. You can implement your own version of this deciding whatever you want to use. But that will probably only complicate/confuse people (which might expect the standards to apply).

Hibernate and #JoinFormula: org.hibernate.mapping.Formula cannot be cast to org.hibernate.mapping.Column

I'm trying to write a hibernate adapter for an old database schema. This schema does not have a dedicated id column, but uses about three other columns to join data.
On some tables, I need to use coalesce. This is what I came up with so far:
About the definition:
A car can have elements, assigned by the car's user or by the car's group of users.
If FORIGN_ELEMENT holds a user's name, definition will be 'u'
If FORIGN_ELEMENT holds a group's name, definition will be 'g'
This also means, one table (CAR_TO_ELEMENT) is misused to map cars to elements and cargroups to elements. I defined a superclass CarElement and subclasses CarUserElement and CarGroupElement.
state is either "active" or an uninteresting string
I set definitition and state elsewhere, we do not need to worry about this.
Use DEP_NR on the join table. If it's zero, use USR_DEP_NR. I did this with COALESCE(NULLIF()) successfully in native SQL and want to achieve the same in Hibernate with Pojos.
Okay, here we go with the code:
#Entity
#Table(name="CAR")
public class Car extends TableEntry implements Serializable {
#Id
#Column(name="DEP_NR")
private int depnr;
#Id
#Column(name="USER_NAME")
#Type(type="TrimmedString")
private String username;
#ManyToOne(fetch = FetchType.EAGER, targetEntity=CarGroup.class)
#JoinColumns(value={
#JoinColumn(name="GROUP_NAME"),
#JoinColumn(name="DEP_NR"),
#JoinColumn(name="state"),
})
private CarGroup group;
#OneToMany(fetch=FetchType.EAGER, targetEntity=CarUserElement.class, mappedBy="car")
private Set<CarUserElement> elements;
}
#Entity
#Table(name="CAR_GROUP")
public class CarGroup extends TableEntry implements Serializable {
#Id
#Column(name="DEP_NR")
private int depnr;
#Id
#Column(name="GROUP_NAME")
#Type(type="TrimmedString")
private String group;
#ManyToOne(fetch = FetchType.EAGER, targetEntity=Car.class)
#JoinColumns(value={
#JoinColumn(name="GROUP_NAME"),
#JoinColumn(name="DEP_NR"),
#JoinColumn(name="state"),
})
private Set<Car> cars;
#OneToMany(fetch=FetchType.EAGER, targetEntity=CarGroupElement.class, mappedBy="car")
private Set<CarGroupElement> elements;
}
#MappedSuperclass
public class CarElement extends TableEntry {
#Id
#ManyToOne(fetch = FetchType.EAGER, targetEntity=Element.class)
#JoinColumns(value={
#JoinColumn(name="ELEMENT_NAME"),
#JoinColumn(name="state"),
})
private Element element;
}
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarUserElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="USER_NAME")),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE"))
})
private Car car;
}
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarGroupElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="GROUP_NAME")),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE"))
})
private Car car;
}
I tried all available versions of hibernate (from 3.5.1 [first version with #JoinColumnsOrFormulas] up to 4.x.x), but I always get this error:
Exception in thread "main" java.lang.ClassCastException: org.hibernate.mapping.Formula cannot be cast to org.hibernate.mapping.Column
at org.hibernate.cfg.annotations.TableBinder.bindFk(TableBinder.java:351)
at org.hibernate.cfg.annotations.CollectionBinder.bindCollectionSecondPass(CollectionBinder.java:1338)
at org.hibernate.cfg.annotations.CollectionBinder.bindOneToManySecondPass(CollectionBinder.java:791)
at org.hibernate.cfg.annotations.CollectionBinder.bindStarToManySecondPass(CollectionBinder.java:719)
at org.hibernate.cfg.annotations.CollectionBinder$1.secondPass(CollectionBinder.java:668)
at org.hibernate.cfg.CollectionSecondPass.doSecondPass(CollectionSecondPass.java:66)
at org.hibernate.cfg.Configuration.originalSecondPassCompile(Configuration.java:1597)
at org.hibernate.cfg.Configuration.secondPassCompile(Configuration.java:1355)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1737)
at org.hibernate.cfg.Configuration.buildSessionFactory(Configuration.java:1788)
Other hibernate users seem to have the same problem: They can't get it working with any version, see this thread and other stackoverflow questions:
https://forum.hibernate.org/viewtopic.php?f=1&t=1010559
To be more complete, here's my TrimmedString Class:
https://forum.hibernate.org/viewtopic.php?p=2191674&sid=049b85950db50a8bd145f9dac49a5f6e#p2191674
Thanks in advance!
PS: It works with joining just these three colulmns with just one DEP-NR-Column (i.e. either DEP_NR OR USR_DEP_NR using just #JoinColumns). But I need this coalesce(nullif()).
I ran into a similar problem, and it seems that the issue is that you are using a #Formula inside an #Id. Hibernate wants Ids to be insertable, and Formulas are read-only.
In my case I was able to work around the problem by making the individual columns Id properties on their own, and making the joined object a separate property. I don't know if this would work in your case since you're using two different columns in your formula, but if so your code might look something like:
#Entity
#Table(name="CAR_TO_ELEMENT")
public class CarUserElement extends CarElement {
#Id
#Column(name="DEFINITION")
private char definition;
#Id
#Column(name="DEP_NR")
private Integer depNr;
#Id
#Column(name="USR_DEP_NR")
private Integer usrDepNr;
#Id
#Column(name="FORIGN_ELEMENT")
private String userName;
#Id
#Column(name="STATE")
private String state;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumnsOrFormulas(value = {
#JoinColumnOrFormula(formula=#JoinFormula(value="COALESCE(NULLIF(DEP_NR, 0), USR_DEP_NR)", referencedColumnName="DEP_NR")),
#JoinColumnOrFormula(column=#JoinColumn(name="FORIGN_ELEMENT", referencedColumnName="USER_NAME", insertable = false, updatable = false)),
#JoinColumnOrFormula(column=#JoinColumn(name="STATE", referencedColumnName="STATE", insertable = false, updatable = false))
})
private Car car;
}
Join formulas are very fragile in Hibernate for the time being; I always had a difficult time to get them work properly.
The workaround that helped me often was to create database views which exposed the proper columns (including foreign keys that don't exist in the original tables). Then I mapped the entities to the views using classing Hibernate/JPA mappings.
Sometimes there are redundant joins in the generated SQL when using such entities, but the database optimizes such queries in most cases so that the execution plan is optimal anyway.
Another approach could be using #Subselects, which are some kind of Hibernate views, but I expect them to be less performant than the classic database views.
I ran into the cast exception as well and I'm on Hibernate 5.x.
Until Hibernate dedicates time to fix the issue, I found that while this guy's approach may not be cleanest (he even eludes to that fact!), it works.
You just need to add the #Column mappings (and get/set methods) to your association table objects that are returning null and manually set the values when you populate the relation data. Simple but effective!

Categories