java spring hibernate

java - mapear los resultados de la consulta de Hibernate a la clase personalizada?



spring (8)

A continuación se muestra un transformador de resultados que ignora el caso:

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

Después de una pregunta que publiqué ayer: ¿Cómo llenar la clase POJO desde una consulta personalizada de Hibernate?

¿Alguien puede mostrarme un ejemplo de cómo codificar el siguiente SQL en Hibernate y obtener los resultados correctamente?

SQL:

select firstName, lastName from Employee

Lo que me gustaría hacer, si es posible en Hibernate, es poner los resultados en su propia clase base:

class Results { private firstName; private lastName; // getters and setters }

Creo que es posible en JPA (usando EntityManager ), pero no he descubierto cómo hacerlo en Hibernate (usando SessionFactory y Session ).

Estoy tratando de aprender Hibernate mejor, e incluso esta consulta "simple" está resultando confusa para saber qué forma Hibernate devuelve los resultados y cómo asignar los resultados en mi propia clase (base). Entonces, al final de la rutina DAO, haría:

List<Results> list = query.list();

devolviendo una List de Results (mi clase base).


En caso de que tenga una consulta nativa, todas las respuestas aquí usan métodos obsoletos para las versiones más nuevas de Hibernate, por lo que si está utilizando 5.1+, este es el camino a seguir:

// 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()


Escritura (existen este tipo de desafíos al trabajar con hibernación)

  1. Consultas personalizadas
  2. Consultas personalizadas con parámetros opcionales
  3. Asignación de resultados de consultas personalizadas de Hibernate a la clase personalizada.

No estoy diciendo sobre la interfaz EntityRepository personalizada que extiende JpaRepository en SpringBoot, que puede escribir consultas personalizadas con @Query -> aquí no puede escribir consultas con parámetros opcionales, por ejemplo, si param es nulo, no lo agregue en la cadena de consulta. Y puede usar Criterios api de hibernación, pero no se recomienda en su documentación debido a un problema de rendimiento ...

Pero existe una manera simple y propensa a errores y buen rendimiento ...

Escriba su propia clase QueryService, que son métodos que obtendrán sql (respuesta para el primer y segundo problema) sql y asignarán el resultado a la clase personalizada (tercer problema) con cualquier asociación @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(); } }

Caso de uso: puede escribir cualquier condición de tipo sobre parámetros de consulta

@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()); }


Necesita usar un constructor y en hql usar new. Te dejo el ejemplo de código tomado de esta pregunta: hibernate HQL createQuery () list () type cast to model directamente

class Result { private firstName; private lastName; public Result (String firstName, String lastName){ this.firstName = firstName; this.lastName = lastName; } }

entonces tu hql

select new com.yourpackage.Result(employee.firstName,employee.lastName) from Employee

y tu java (usando Hibernate)

List<Result> results = session.createQuery("select new com.yourpackage.Result(employee.firstName,employee.lastName) from Employee").list();


Ver AliasToBeanResultTransformer :

Transformador de resultados que permite transformar un resultado en una clase especificada por el usuario que se completará mediante métodos de establecimiento o campos que coincidan con los nombres de 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);

Tu código modificado:

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

Para consultas SQL nativas, consulte la documentación de Hibernate :

13.1.5. Entidades no gestionadas que regresan

Es posible aplicar un ResultTransformer a consultas SQL nativas, lo que le permite devolver entidades no administradas.

sess.createSQLQuery("SELECT NAME, BIRTHDATE FROM CATS") .setResultTransformer(Transformers.aliasToBean(CatDTO.class))

Esta consulta especificaba:

  • la cadena de consulta SQL
  • un transformador de resultados La consulta anterior devolverá una lista de CatDTO que se ha instanciado e inyectado los valores de NAME y BIRTHNAME en sus propiedades o campos correspondientes.

YMMV, pero he descubierto que el factor clave es que debe asegurarse de alias cada campo en su cláusula SELECT con la palabra clave "AS" de SQL. Nunca he tenido que usar comillas alrededor de los nombres de alias. Además, en su cláusula SELECT use el caso y la puntuación de las columnas reales en su base de datos y en los alias use el caso de los campos en su POJO. Esto me ha funcionado en Hibernate 4 y 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 tiene varias tablas, también puede usar alias de tabla en el SQL. Este ejemplo artificial con una tabla adicional llamada "Departamento" utiliza minúsculas y guiones bajos más tradicionales en los nombres de campo de la base de datos con mayúsculas en los nombres de campo 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();


java.lang.ClassCastException: "CustomClass" cannot be cast to java.util.Map.

Este problema aparece cuando las columnas especificadas en la consulta SQL no coinciden con las columnas de la clase de mapeo.

Puede deberse a:

  • Carcasa no coincidente del nombre de la columna o

  • Los nombres de las columnas no coinciden o

  • la columna existe en la consulta pero falta en la clase.


select firstName, lastName from Employee query.setResultTransformer(Transformers.aliasToBean(MyResults.class));

No puede usar el código anterior con Hibernate 5 e Hibernate 4 (al menos Hibernate 4.3.6.Final), debido a una excepción

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)

El problema es que Hibernate convierte los alias de los nombres de columna en mayúsculas: firstName convierte en FIRSTNAME . E intenta encontrar un getter con el nombre getFIRSTNAME() , y setter setFIRSTNAME() en el DTO usando tales estrategias

PropertyAccessStrategyChainedImpl propertyAccessStrategy = new PropertyAccessStrategyChainedImpl( PropertyAccessStrategyBasicImpl.INSTANCE, PropertyAccessStrategyFieldImpl.INSTANCE, PropertyAccessStrategyMapImpl.INSTANCE );

Solo PropertyAccessStrategyMapImpl.INSTANCE adapta, en opinión de Hibernate, bien. Entonces, después de eso, intenta hacer la conversión (Map)MyResults .

public void set(Object target, Object value, SessionFactoryImplementor factory) { ( (Map) target ).put( propertyName, value ); }

No sé, es un error o característica.

Cómo resolver

Usando alias con comillas

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();

Usando un transformador de resultado personalizado

Otra forma de resolver el problema: usar un transformador de resultados que ignore el caso de los nombres de métodos (trate getFirstName() como getFIRSTNAME() ). Puede escribir el suyo o usar FluentHibernateResultTransformer . No necesitará usar comillas y alias (si tiene nombres de columna iguales a los nombres DTO).

Simplemente descargue la biblioteca desde la página del proyecto (no necesita frascos adicionales): fluent-hibernate .

String sql = "select firstName, lastName from Employee"; List<Results> employees = session.createSQLQuery(sql) .setResultTransformer(new FluentHibernateResultTransformer(Results.class)) .list();

Este transformador también se puede utilizar para proyecciones anidadas: cómo transformar un conjunto de resultados plano utilizando Hibernate