mapping Hibernate query results to custom class?

前端 未结 8 726
时光说笑
时光说笑 2020-12-01 11:02

Following up on a question I posted yesterday: How to populate POJO class from custom Hibernate query?

Can someone show me an example of how to code the following SQ

相关标签:
8条回答
  • 2020-12-01 11:27

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

    This issue appears when the columns specified in SQL Query doesn't match with the columns of the mapping class.

    It may be due to:

    • Non-matching casing of column name or

    • The column names are not matching or

    • column exist in query but missing in class.

    0 讨论(0)
  • 2020-12-01 11:27

    Writing (exist this type of Challenges working with hibernate)

    1. Custom Queries
    2. Custom Queries with Optional Parameters
    3. Mapping Hibernate Custom query results to Custom class.

    I am not saying about custom EntityRepository interface which extends JpaRepository on SpringBoot which you can write custom Query with @Query -> here you can't write query with optional params e.g. if param is null don't append it in query string. And you can use Criteria api of hibernate but it not recommended in their documentation because of performance issue...

    But exist simple and error prone and performance good way...

    Write your own QueryService class which are methods will get string(answer for first and second problem) sql and will map result to Custom class (third problem) with it's any 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();
    }
    
    
    }
    

    Use case - you can write any type condition about query params

     @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());
    
        }
    
    0 讨论(0)
  • 2020-12-01 11:34

    See AliasToBeanResultTransformer:

    Result transformer that allows to transform a result to a user specified class which will be populated via setter methods or fields matching the alias names.

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

    Your modified code:

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

    For native SQL queries see Hibernate documentation:

    13.1.5. Returning non-managed entities

    It is possible to apply a ResultTransformer to native SQL queries, allowing it to return non-managed entities.

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

    This query specified:

    • the SQL query string
    • a result transformer The above query will return a list of CatDTO which has been instantiated and injected the values of NAME and BIRTHNAME into its corresponding properties or fields.
    0 讨论(0)
  • 2020-12-01 11:38

    YMMV but I've found that the key factor is you must make sure to alias every field in your SELECT clause with the SQL "AS" keyword. I've never had to use quotes around the alias names. Also, in your SELECT clause use the case and punctuation of the actual columns in your database and in the aliases use the case of the fields in your POJO. This has worked for me in Hibernate 4 and 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();
    

    If you have multiple tables you can use table aliases in the SQL as well. This contrived example with an additional table named "Department" uses more traditional lower case and underscores in database field names with camel case in the POJO field names.

    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();
    
    0 讨论(0)
  • 2020-12-01 11:39
    select firstName, lastName from Employee
    
    query.setResultTransformer(Transformers.aliasToBean(MyResults.class));
    

    You can't use above code with Hibernate 5 and Hibernate 4 (at least Hibernate 4.3.6.Final), because of an 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)
    

    The problem is that Hibernate converts aliases for column names to upper case — firstName becomes FIRSTNAME. And it try to find a getter with name getFIRSTNAME(), and setter setFIRSTNAME() in the DTO using such strategies

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

    Only PropertyAccessStrategyMapImpl.INSTANCE suits, in opinion of Hibernate, well. So after that it tries to do conversion (Map)MyResults.

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

    Don't know, it is a bug or feature.

    How to solve

    Using aliases with quotes

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

    Using a custom result transformer

    Another way to solve the problem — using a result transformer that ignores method names case (treat getFirstName() as getFIRSTNAME()). You can write your own or use FluentHibernateResultTransformer. You will not need to use quotes and aliases (if you have column names equal to DTO names).

    Just download the library from the project page (it doesn't need additional jars): fluent-hibernate.

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

    This transformer can be used for nested projections too: How to transform a flat result set using Hibernate

    0 讨论(0)
  • 2020-12-01 11:39

    You need to use a constructor and in the hql use new. I let you the code example taken from this question: hibernate HQL createQuery() list() type cast to model directly

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

    then your hql

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

    and your java (using Hibernate)

    List<Result> results = session.createQuery("select new com.yourpackage.Result(employee.firstName,employee.lastName) from Employee").list();
    
    0 讨论(0)
提交回复
热议问题