Pour faire suite à une question que j’ai posée hier: Comment renseigner la classe POJO à partir d’une requête Hibernate personnalisée?
Quelqu'un peut-il me montrer un exemple de la manière de coder le code SQL suivant dans Hibernate et d’obtenir les résultats correctement?
SQL:
select firstName, lastName
from Employee
Ce que j'aimerais faire, si cela est possible dans Hibernate, est de placer les résultats dans leur propre classe de base:
class Results {
private firstName;
private lastName;
// getters and setters
}
Je pense que c'est possible dans JPA (en utilisant EntityManager
), mais je n'ai pas trouvé comment le faire dans Hibernate (en utilisant SessionFactory
et Session
).
J'essaie de mieux apprendre à utiliser Hibernate, et même cette requête "simple" s'avère déconcertante de savoir quelle forme Hibernate renvoie les résultats et comment mapper les résultats dans ma propre classe (de base). Donc, à la fin de la routine DAO, je ferais:
List<Results> list = query.list();
retournant une List
de Results
(ma classe de base).
select firstName, lastName from Employee
query.setResultTransformer(Transformers.aliasToBean(MyResults.class));
Vous ne pouvez pas utiliser le code ci-dessus avec Hibernate 5 et Hibernate 4 (au moins Hibernate 4.3.6.Final), à cause d'une exception
Java.lang.ClassCastException: com.github.fluent.hibernate.request.persistent.UserDto cannot be cast to Java.util.Map
at org.hibernate.property.access.internal.PropertyAccessMapImpl$SetterImpl.set(PropertyAccessMapImpl.Java:102)
Le problème est que Hibernate convertit les alias des noms de colonnes en majuscules - firstName
devient FIRSTNAME
. Et il essaie de trouver un getter avec le nom getFIRSTNAME()
et le setter setFIRSTNAME()
dans la DTO
en utilisant de telles stratégies
PropertyAccessStrategyChainedImpl propertyAccessStrategy = new PropertyAccessStrategyChainedImpl(
PropertyAccessStrategyBasicImpl.INSTANCE,
PropertyAccessStrategyFieldImpl.INSTANCE,
PropertyAccessStrategyMapImpl.INSTANCE
);
Seul PropertyAccessStrategyMapImpl.INSTANCE
convient, selon Hibernate, eh bien. Donc, après cela, il essaie de faire la conversion (Map)MyResults
.
public void set(Object target, Object value, SessionFactoryImplementor factory) {
( (Map) target ).put( propertyName, value );
}
Je ne sais pas, c'est un bug ou une fonctionnalité.
Comment résoudre
Utilisation d'alias avec des guillemets
public class Results {
private String firstName;
private String lastName;
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}
String sql = "select firstName as \"firstName\",
lastName as \"lastName\" from Employee";
List<Results> employees = session.createSQLQuery(sql).setResultTransformer(
Transformers.aliasToBean(Results.class)).list();
Utilisation d'un transformateur de résultat personnalisé
Une autre façon de résoudre le problème - en utilisant un transformateur de résultat qui ignore la casse des noms de méthodes (traitez getFirstName()
comme getFIRSTNAME()
). Vous pouvez écrire le vôtre ou utiliser FluentHibernateResultTransformer . Vous n’avez pas besoin d’utiliser des guillemets et des alias (si vous avez un nom de colonne égal à un nom DTO).
Il suffit de télécharger la bibliothèque à partir de la page du projet (elle n’a pas besoin de bocaux supplémentaires): fluent-hibernate .
String sql = "select firstName, lastName from Employee";
List<Results> employees = session.createSQLQuery(sql)
.setResultTransformer(new FluentHibernateResultTransformer(Results.class))
.list();
Ce transformateur peut également être utilisé pour les projections imbriquées: Comment transformer un ensemble de résultats plat avec Hibernate
Voir AliasToBeanResultTransformer :
Transformateur de résultat qui permet de transformer un résultat en une classe spécifiée par l'utilisateur qui sera renseignée via des méthodes de définition ou des champs correspondant aux noms d'alias.
List resultWithAliasedBean = s.createCriteria(Enrolment.class) .createAlias("student", "st") .createAlias("course", "co") .setProjection( Projections.projectionList() .add( Projections.property("co.description"), "courseDescription" ) ) .setResultTransformer( new AliasToBeanResultTransformer(StudentDTO.class) ) .list(); StudentDTO dto = (StudentDTO)resultWithAliasedBean.get(0);
Votre code modifié:
List resultWithAliasedBean = s.createCriteria(Employee.class, "e")
.setProjection(Projections.projectionList()
.add(Projections.property("e.firstName"), "firstName")
.add(Projections.property("e.lastName"), "lastName")
)
.setResultTransformer(new AliasToBeanResultTransformer(Results.class))
.list();
Results dto = (Results) resultWithAliasedBean.get(0);
Pour les requêtes SQL natives, voir Documentation Hibernate :
13.1.5. Renvoi des entités non gérées
Il est possible d'appliquer un ResultTransformer à des requêtes SQL natives, ce qui lui permet de renvoyer des entités non gérées.
sess.createSQLQuery("SELECT NAME, BIRTHDATE FROM CATS") .setResultTransformer(Transformers.aliasToBean(CatDTO.class))
Cette requête a spécifié:
- la chaîne de requête SQL
- un transformateur de résultat La requête ci-dessus renverra une liste de
CatDTO
qui a été instanciée et a injecté les valeurs de NAME et BIRTHNAME dans les propriétés ou champs correspondants.
Vous devez utiliser un constructeur et utiliser dans hql new. Je vous laisse l’exemple de code tiré de cette question: hibernate HQL createQuery () list () tapez le type à modeler directement
class Result {
private firstName;
private lastName;
public Result (String firstName, String lastName){
this.firstName = firstName;
this.lastName = lastName;
}
}
alors votre hql
select new com.yourpackage.Result(employee.firstName,employee.lastName)
from Employee
et votre Java (avec Hibernate)
List<Result> results = session.createQuery("select new com.yourpackage.Result(employee.firstName,employee.lastName) from Employee").list();
YMMV, mais j’ai constaté que le facteur clé est que vous devez vous assurer d’aliaser tous les champs de votre clause SELECT avec le mot-clé SQL "AS". Je n'ai jamais eu à utiliser de guillemets autour des noms d'alias. De plus, dans votre clause SELECT, utilisez la casse et la ponctuation des colonnes réelles de votre base de données et dans les alias, utilisez la casse des champs de votre POJO. Cela a fonctionné pour moi dans Hibernate 4 et 5.
@Autowired
private SessionFactory sessionFactory;
...
String sqlQuery = "SELECT firstName AS firstName," +
"lastName AS lastName from Employee";
List<Results> employeeList = sessionFactory
.getCurrentSession()
.createSQLQuery(sqlQuery)
.setResultTransformer(Transformers.aliasToBean(Results.class))
.list();
Si vous avez plusieurs tables, vous pouvez également utiliser des alias de table dans le code SQL. Cet exemple artificiel avec une table supplémentaire nommée "Department" utilise des minuscules plus traditionnelles et des traits de soulignement dans les noms de champ de base de données avec une casse de chameau dans les noms de champ POJO.
String sqlQuery = "SELECT e.first_name AS firstName, " +
"e.last_name AS lastName, d.name as departmentName" +
"from Employee e, Department d" +
"WHERE e.department_id - d.id";
List<Results> employeeList = sessionFactory
.getCurrentSession()
.createSQLQuery(sqlQuery)
.setResultTransformer(Transformers.aliasToBean(Results.class))
.list();
Écriture (existe ce type de défis travaillant avec hibernation)
Je ne dis pas que l'interface EntityRepository personnalisée étend JpaRepository sur SpringBoot et que vous pouvez écrire une requête personnalisée avec @Query -> vous ne pouvez pas écrire ici de requête avec les paramètres facultatifs. si param est null, ne l'ajoutez pas à la chaîne de requête. Et vous pouvez utiliser les critères api de hibernate mais cela n’est pas recommandé dans leur documentation en raison de problèmes de performances ...
Mais il existe des solutions simples et sujettes aux erreurs et des performances optimales ...
Ecrivez votre propre classe QueryService avec les méthodes suivantes: string (réponse pour les premier et second problèmes) sql et mappera le résultat à Classe personnalisée (troisième problème) avec n'importe quelle association @OneToMany, @ManyToOne ....
@Service
@Transactional
public class HibernateQueryService {
private final Logger log = LoggerFactory.getLogger(HibernateQueryService.class);
private JpaContext jpaContext;
public HibernateQueryService(JpaContext jpaContext) {
this.jpaContext = jpaContext;
}
public List executeJPANativeQuery(String sql, Class entity){
log.debug("JPANativeQuery executing: "+sql);
EntityManager entityManager = jpaContext.getEntityManagerByManagedType(Article.class);
return entityManager.createNativeQuery(sql, entity).getResultList();
}
/**
* as annotation @Query -> we can construct here hibernate dialect
* supported query and fetch any type of data
* with any association @OneToMany and @ManyToOne.....
*/
public List executeHibernateQuery(String sql, Class entity){
log.debug("HibernateNativeQuery executing: "+sql);
Session session = jpaContext.getEntityManagerByManagedType(Article.class).unwrap(Session.class);
return session.createQuery(sql, entity).getResultList();
}
public <T> List<T> executeGenericHibernateQuery(String sql, Class<T> entity){
log.debug("HibernateNativeQuery executing: "+sql);
Session session = jpaContext.getEntityManagerByManagedType(Article.class).unwrap(Session.class);
return session.createQuery(sql, entity).getResultList();
}
}
Cas d'utilisation - vous pouvez écrire n'importe quelle condition de type sur les paramètres de requête
@Transactional(readOnly = true)
public List<ArticleDTO> findWithHibernateWay(SearchFiltersVM filter){
Long[] stores = filter.getStores();
Long[] categories = filter.getCategories();
Long[] brands = filter.getBrands();
Long[] articles = filter.getArticles();
Long[] colors = filter.getColors();
String query = "select article from Article article " +
"left join fetch article.attributeOptions " +
"left join fetch article.brand " +
"left join fetch article.stocks stock " +
"left join fetch stock.color " +
"left join fetch stock.images ";
boolean isFirst = true;
if(!isArrayEmptyOrNull(stores)){
query += isFirst ? "where " : "and ";
query += "stock.store.id in ("+ Arrays.stream(stores).map(store -> store.toString()).collect(Collectors.joining(", "))+") ";
isFirst = false;
}
if(!isArrayEmptyOrNull(brands)){
query += isFirst ? "where " : "and ";
query += "article.brand.id in ("+ Arrays.stream(brands).map(store -> store.toString()).collect(Collectors.joining(", "))+") ";
isFirst = false;
}
if(!isArrayEmptyOrNull(articles)){
query += isFirst ? "where " : "and ";
query += "article.id in ("+ Arrays.stream(articles).map(store -> store.toString()).collect(Collectors.joining(", "))+") ";
isFirst = false;
}
if(!isArrayEmptyOrNull(colors)){
query += isFirst ? "where " : "and ";
query += "stock.color.id in ("+ Arrays.stream(colors).map(store -> store.toString()).collect(Collectors.joining(", "))+") ";
}
List<Article> articles = hibernateQueryService.executeHibernateQuery(query, Article.class);
/**
* MapStruct [http://mapstruct.org/][1]
*/
return articles.stream().map(articleMapper::toDto).collect(Collectors.toList());
}
Si vous avez une requête native, toutes les réponses ici utilisent des méthodes déconseillées pour les nouvelles versions d'Hibernate. Par conséquent, si vous utilisez la version 5.1+, voici la solution:
// Note this is a org.hibernate.query.NativeQuery NOT Query.
NativeQuery query = getCurrentSession()
.createNativeQuery(
"SELECT {y.*} , {x.*} from TableY y left join TableX x on x.id = y.id");
// This maps the results to entities.
query.addEntity("x", TableXEntity.class);
query.addEntity("y", TableYEntity.class);
query.list()
Ci-dessous un résultat de transformation qui ignore la casse:
package org.apec.abtc.dao.hibernate.transform;
import Java.lang.reflect.Field;
import Java.util.Arrays;
import Java.util.List;
import org.hibernate.HibernateException;
import org.hibernate.property.access.internal.PropertyAccessStrategyBasicImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyChainedImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyFieldImpl;
import org.hibernate.property.access.internal.PropertyAccessStrategyMapImpl;
import org.hibernate.property.access.spi.Setter;
import org.hibernate.transform.AliasedTupleSubsetResultTransformer;
/**
* IgnoreCaseAlias to BeanResult Transformer
*
* @author Stephen Gray
*/
public class IgnoreCaseAliasToBeanResultTransformer extends AliasedTupleSubsetResultTransformer
{
/** The serialVersionUID field. */
private static final long serialVersionUID = -3779317531110592988L;
/** The resultClass field. */
@SuppressWarnings("rawtypes")
private final Class resultClass;
/** The setters field. */
private Setter[] setters;
/** The fields field. */
private Field[] fields;
private String[] aliases;
/**
* @param resultClass
*/
@SuppressWarnings("rawtypes")
public IgnoreCaseAliasToBeanResultTransformer(final Class resultClass)
{
if (resultClass == null)
{
throw new IllegalArgumentException("resultClass cannot be null");
}
this.resultClass = resultClass;
this.fields = this.resultClass.getDeclaredFields();
}
@Override
public boolean isTransformedValueATupleElement(String[] aliases, int tupleLength) {
return false;
}
/**
* {@inheritDoc}
*/
@Override
public Object transformTuple(final Object[] Tuple, final String[] aliases)
{
Object result;
try
{
if (this.setters == null)
{
this.aliases = aliases;
setSetters(aliases);
}
result = this.resultClass.newInstance();
for (int i = 0; i < aliases.length; i++)
{
if (this.setters[i] != null)
{
this.setters[i].set(result, Tuple[i], null);
}
}
}
catch (final InstantiationException | IllegalAccessException e)
{
throw new HibernateException("Could not instantiate resultclass: " + this.resultClass.getName(), e);
}
return result;
}
private void setSetters(final String[] aliases)
{
PropertyAccessStrategyChainedImpl propertyAccessStrategy = new PropertyAccessStrategyChainedImpl(
PropertyAccessStrategyBasicImpl.INSTANCE,
PropertyAccessStrategyFieldImpl.INSTANCE,
PropertyAccessStrategyMapImpl.INSTANCE
);
this.setters = new Setter[aliases.length];
for (int i = 0; i < aliases.length; i++)
{
String alias = aliases[i];
if (alias != null)
{
for (final Field field : this.fields)
{
final String fieldName = field.getName();
if (fieldName.equalsIgnoreCase(alias))
{
alias = fieldName;
break;
}
}
setters[i] = propertyAccessStrategy.buildPropertyAccess( resultClass, alias ).getSetter();
}
}
}
/**
* {@inheritDoc}
*/
@Override
@SuppressWarnings("rawtypes")
public List transformList(final List collection)
{
return collection;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
IgnoreCaseAliasToBeanResultTransformer that = ( IgnoreCaseAliasToBeanResultTransformer ) o;
if ( ! resultClass.equals( that.resultClass ) ) {
return false;
}
if ( ! Arrays.equals( aliases, that.aliases ) ) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = resultClass.hashCode();
result = 31 * result + ( aliases != null ? Arrays.hashCode( aliases ) : 0 );
return result;
}
}
Java.lang.ClassCastException: "CustomClass" cannot be cast to Java.util.Map.
Ce problème survient lorsque les colonnes spécifiées dans SQL Query ne correspondent pas aux colonnes de la classe de mappage.
Cela peut être dû à:
Mise en majuscule du nom de la colonne ou
Les noms de colonne ne correspondent pas ou
la colonne existe dans la requête mais manque dans la classe.