java - Spring/Hibernate - 在 Java 中不使用 @Entity 从存储过程中获取结果

标签 java spring hibernate jpa stored-procedures

我们在 MS SQL 中有一个存储过程,它返回一些与任何数据库表都不匹配的结果集。我们希望在不使用 @Entity 注释的情况下将结果映射到普通的 java POJO。但是如果我们打电话

entityManager.createStoredProcedureQuery("schema.myProcedure",
                        PlainPojo.class);

它将失败 org.hibernate.MappingException: Unknown entity异常(exception)

我知道有 @SqlResultSetMapping 注释,但它需要放在 Entity 类上?这不起作用
@Repository
@SqlResultSetMapping(name = "Mapping")
public class MyRepositoryImpl implements MyRepository

只有这个
@Enity
@SqlResultSetMapping(name = "Mapping")
public class MyEntity

但我们不想要实体类...所以把它放在另一个实体上?? (不完全是)
  • 可以把它放在其他地方吗?
  • 或者有可能在代码中做到吗?

    另一种解决方案?
  • 是否可以制作类似 transient 实体的东西? -> 使用@Entity 注释而不创建数据库表等
  • 最佳答案

    我没有在 stackoverflow 上找到复杂的解决方案并重新发明轮子,也许这对需要在项目中多次调用存储过程的人有所帮助。

    主要思想是编写类似于@StoredProcedure的自定义注解,但独立于@Entity

    @Target(TYPE)
    @Retention(RUNTIME)
    public @interface CustomNamedStoredProcedureQueries {
         /**
         * Array of CustomNamedStoredProcedureQuery annotations.
         */
         CustomNamedStoredProcedureQuery[] value();
    }
    @Target(TYPE)
    @Retention(RUNTIME)
    public @interface CustomNamedStoredProcedureQuery {
        /*
        * The name of the stored procedure for call.
        */
        String name();
        /*
        * The name of the stored procedure in the database.
        */
        String procedureName();
        /*
        * The scheme name of the database.
        */
        String schemeName();
        /*
        * The name of the package that contains stored procedure in the database.
        */
        String packageName();
        /*
        * Information about all parameters of the stored procedure.
        */
        CustomStoredProcedureParameter[] parameters() default {};
        /*
        * The names of one or more result set mappings, as defined in metadata, when you need custom mapping.
        */
        ProcedureRowMapper[] resultSetRowMappers() default {};
        /*
        * The names of one or more result set mappings, as defined in metadata, when you need 1 to 1 mapping.
        */
        ProcedureResultClass[] resultSetMappers() default {};
    }
    @Target({})
    @Retention(RUNTIME)
    public @interface CustomStoredProcedureParameter {
        /** Name of strored procedure parameter.*/
        String name() default "";
        /** JDBC type of the parameter.*/
        int type();
        /** Parameter mode.*/
        ParameterMode mode() default ParameterMode.IN;
    }
    @Target({})
    @Retention(RUNTIME)
    public @interface ProcedureResultClass {
        /* Name of procedure parameter*/
        String name();
        /* Class of object using for mapping */
        Class<?> resultClass();
    }
    @Target({})
    @Retention(RUNTIME)
    public @interface ProcedureRowMapper {
        /* Name of cursor field for use mapper*/
        String name();
        /* Class of object using for mapping, must extend BeanPropertyRowMapper */
        Class<? extends BeanPropertyRowMapper> mapper();
    }
    

    如前所述,当您需要将 resultSet 1 映射到 1 时,请使用 ProcedureResultClass,字段必须具有相同的名称。

    当你需要一些自定义映射时使用 ProcedureRowMapper,你可以@Override mapRow() 方法,并手动映射它,或者您可以@Override initBeanWrapper() 如果您只需要以特定方式转换某些字段。

    所以,接下来我们需要一个让它工作的方法。把它放在你的应用程序的 init 方法中,比如 main 方法的 @PostConstruct 。
    ProjectUtils {
        private JdbcTemplate jdbcTemplate;
    
        @Autowired
        public ProjectUtils(DataSource dataSource) {
            jdbcTemplate = new JdbcTemplate(DataSource);
        }
    
        private static Map<String, SimpleJdbcCall> storedProcedureContainer = new HashMap<>();
    
    
        public void initStoredProcedure() {
            Reflections reflections = new Reflections("my.package");
            Set<Class<?>> classList = reflections.getTypesAnnotatedWith(CustomNamedStoredProcedureQueries.class);
    
            for (Class<?> clazz : classList) {
                CustomNamedStoredProcedureQueries queriesAnnotation = clazz.getAnnotation(CustomNamedStoredProcedureQueries.class);
    
                for (CustomNamedStoredProcedureQuery queryAnnotation : queriesAnnotation.value()) {
                    SimpleJdbcCall jdbcCall = new SimpleJdbcCall(jdbcTemplate)
                            .withSchemaName(queryAnnotation.schemeName())
                            .withCatalogName(queryAnnotation.packageName())
                            .withProcedureName(queryAnnotation.procedureName());
                    Set<SqlParameter> parameters = this.getDeclaredParameters(queryAnnotation.parameters());
                    jdbcCall.declareParameters(parameters.toArray(new SqlParameter[0]));
    
                    Map<String, RowMapper<?>> rowMappers = getDeclaredRowMappers(queryAnnotation.resultSetRowMappers(), queryAnnotation.resultSetMappers());
                     rowMappers.forEach((parameterName, rowMapper) -> jdbcCall.addDeclaredRowMapper(parameterName, rowMapper));
    
                    jdbcCall.compile();
    
                    storedProcedureContainer.put(queryAnnotation.name(), jdbcCall);
                }
            }
        }
    
        private Set<SqlParameter> getDeclaredParameters(CustomStoredProcedureParameter[] procedureParameters) {
            Set<SqlParameter> parameters = new HashSet<>();
            for (CustomStoredProcedureParameter procedureParameter : procedureParameters) {
                if (procedureParameter.mode().equals(ParameterMode.IN)) {
                    parameters.add(new SqlParameter(procedureParameter.name(), procedureParameter.type()));
                }
                if (procedureParameter.mode().equals(ParameterMode.OUT)) {
                    parameters.add(new SqlOutParameter(procedureParameter.name(), procedureParameter.type()));
                }
            }
            return parameters;
        }
    
        private Map<String, RowMapper<?>> getDeclaredRowMappers(ProcedureRowMapper[] resultSetMappers, ProcedureResultClass[] simpleResultSetMappers) {
            Map<String, RowMapper<?>> mappers = new HashMap<>();
            if (!ArrayUtils.isEmpty(simpleResultSetMappers)) {
                for (ProcedureResultClass procedureResultClass : simpleResultSetMappers) {
                    mappers.put(procedureResultClass.name(), BeanPropertyRowMapper.newInstance(procedureResultClass.resultClass()));
                }
                return mappers;
            }
            try {
                for (ProcedureRowMapper resultSetMapper : resultSetMappers) {
                    mappers.put(resultSetMapper.name(), resultSetMapper.mapper().newInstance());
                }
            } catch (InstantiationException | IllegalAccessException e) {
                e.printStackTrace();
            }
            return mappers;
        }
    
        public SimpleJdbcCall getProcedure(String name) {
            return storedProcedureContainer.get(name);
        }
    }
    
  • 假设必须声明@CustomNamedStoredProcedureQueries,即使存储过程只有一个,您也可以根据需要自行更改。
  • 您可以使用ProcedureResultClass 或ProcedureRowMapper,不能同时使用两者,它可以更改为。

  • 示例 (我使用 oracledb,但稍作改动,它可以用于另一个数据库)
    @CustomNamedStoredProcedureQueries({
            @CustomNamedStoredProcedureQuery(
                    name = "getFoo",
                    schemeName = "FooScheme",
                    packageName = "FooPackage",
                    procedureName = "get_Foo",
                    resultSetMappers = @ProcedureResultClass(name = "foo_cursor", resultClass = Foo.class),
                    parameters = {
                            @CustomStoredProcedureParameter(name = "foo_id", type = OracleTypes.NUMBER, mode = ParameterMode.IN),
                            @CustomStoredProcedureParameter(name = "error_code", type = OracleTypes.NUMBER, mode = ParameterMode.OUT),
                            @CustomStoredProcedureParameter(name = "foo_cursor", type = OracleTypes.CURSOR, mode = ParameterMode.OUT),
                    }
            ),
            @CustomNamedStoredProcedureQuery(
                    name = "deleteFoo",
                    schemeName = "FooScheme",
                    packageName = "FooPackage",
                    procedureName = "delete_Foo",
                    parameters = {
                            @CustomStoredProcedureParameter(name = "foo_id", type = OracleTypes.NUMBER, mode = ParameterMode.IN),
                            @CustomStoredProcedureParameter(name = "error_code", type = OracleTypes.NUMBER, mode = ParameterMode.OUT),
                    }
            )
    })
    public class Foo {
         long id;
         String name;
         int age;
    
         public void setId(long id) { 
           this.id= id;
         }
         public long getId() { 
           return this.id;
         }
    
         public void setName(String name) { 
           this.name = name;
         }
         public String getName() { 
           return this.name;
         }
    
         public void setAge(int age) { 
           this.age = age;
         }
         public int getAge() { 
           return this.age;
         }
    }
    

    叫它:
    public Foo getFoo(Long fooId) {
            SqlParameterSource sqlParams = new MapSqlParameterSource()
                    .addValue("foo_id", fooId);
            SimpleJdbcCall procedure = projectUtils.getProcedure("getFoo");
            Map<String, Object> result = procedure.execute(sqlParams);
            return ((List<Foo>) result.get("foo_cursor")).get(0);
        }
    

    提示 - 它可以与自定义类型(存储在 bd 中的类型)一起使用,例如 Array,因为您需要创建扩展的 AbstractSqlTypeValue 的自定义类型并覆盖 createTypeValue() ,然后像其他参数一样传递它( 仅在 oracle 上经过仔细测试)。

    CustomType 必须在方案级别和更高级别上声明,在包级别它不起作用,方案和类型必须以大写形式调用。
        @Override
        protected Object createTypeValue(Connection connection, int sqlType, String typeName) throws SQLException {
            if (connection.isWrapperFor(OracleConnection.class)) {
                OracleConnection oracleConnection = connection.unwrap(OracleConnection.class);
                return oracleConnection.createARRAY("FOOSCHEME.NUMBERARRAY", this.array); // array pass when customType class init
            }
            return connection.createArrayOf("FOOSCHEME.NUMBERARRAY", this.array);
        }
    

    关于java - Spring/Hibernate - 在 Java 中不使用 @Entity 从存储过程中获取结果,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48687462/

    相关文章:

    java - 如何在 hibernate 中使用数据库作为备份/故障转移?

    java regex - 无法从第一个方括号中检索内容

    java - add(element) 方法将数组中的每个元素更改为元素?

    java - Spring 集成;在复制完成之前复制服务拾取的文件

    hibernate - Grails/GORM/Hibernate 应用程序不是 SQL 转义输入数据

    java - 事务不会回滚@Hibernate

    Java时间,无法从纪元秒解析年份

    java - Androidannotations EBean注释类应该有一个带有一个Context类型参数的构造函数

    java - 如何从 Spring Web 应用程序返回一个字符串作为有效的 JSON?

    java - Tomcat在Spring中上传文件与允许的最大大小相关