春季数据jdbc。无法为枚举添加自定义转换器

gst*_*low 2 java spring spring-boot spring-data-jdbc

我想将枚举作为我的实体的字段。

我的应用程序看起来像:

春季启动版本

plugins {
        id 'org.springframework.boot' version '2.6.2' apply false
Run Code Online (Sandbox Code Playgroud)

存储库:

@Repository
public interface MyEntityRepository extends PagingAndSortingRepository<MyEntity, UUID> {
 ...
Run Code Online (Sandbox Code Playgroud)

实体:

@Table("my_entity")
public class MyEntity{
   ...
   private FileType fileType;
  // get + set
}
Run Code Online (Sandbox Code Playgroud)

枚举声明:

public enum FileType {
    TYPE_1(1),
    TYPE_2(2);

    int databaseId;

    public static FileType byDatabaseId(Integer databaseId){
        return Arrays.stream(values()).findFirst().orElse(null);
    }
    FileType(int databaseId) {
        this.databaseId = databaseId;
    }

    public int getDatabaseId() {
        return databaseId;
    }
}
Run Code Online (Sandbox Code Playgroud)

我的尝试:

我找到了以下答案并尝试遵循它:/sf/answers/3730733961/

所以我添加了豆子

@Bean
public JdbcCustomConversions jdbcCustomConversions() {
    return new JdbcCustomConversions(asList(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter()));
}
Run Code Online (Sandbox Code Playgroud)

转换器:

@WritingConverter
public class FileTypeToDatabaseIdConverter implements Converter<FileType, Integer> {
    @Override
    public Integer convert(FileType source) {
        return source.getDatabaseId();
    }
}

@ReadingConverter
public class DatabaseIdToFileTypeConverter implements Converter<Integer, FileType> {
    @Override
    public FileType convert(Integer databaseId) {
        return FileType.byDatabaseId(databaseId);
    }
}
Run Code Online (Sandbox Code Playgroud)

但我看到错误:

无法注册类路径资源 [org/springframework/boot/autoconfigure/data/jdbc/JdbcRepositoriesAutoConfiguration$SpringBootJdbcConfiguration.class] 中定义的 bean“jdbcCustomConversions”。具有该名称的 bean 已在 my.pack.Main 中定义,并且覆盖已禁用。

我尝试将方法重命名jdbcCustomConversions()myJdbcCustomConversions(). 它有助于避免上述错误,但在实体持久性期间不会调用转换器,并且我看到另一个错误,即应用程序尝试保存 String 但数据库类型是 bigint。

20:39:10.689  DEBUG  [main] o.s.jdbc.core.StatementCreatorUtils: JDBC getParameterType call failed - using fallback method instead: org.postgresql.util.PSQLException: ERROR: column "file_type" is of type bigint but expression is of type character varying
  Hint: You will need to rewrite or cast the expression.
  Position: 174 
Run Code Online (Sandbox Code Playgroud)

我还尝试使用最新(当前)版本的 Spring Boot:

id 'org.springframework.boot' version '2.6.2' apply false
Run Code Online (Sandbox Code Playgroud)

但这没有帮助。

我错过了什么?如何正确地将枚举映射到整数列?

聚苯乙烯

我使用以下代码进行测试:

@SpringBootApplication
@EnableJdbcAuditing
@EnableScheduling
public class Main {
    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
        MyEntityRepositoryrepository = applicationContext.getBean(MyEntityRepository.class);
        MyEntity entity =  new MyEntity();
        ...
        entity.setFileType(FileType.TYPE_2);
        repository.save(entity);
    }

    @Bean
    public ModelMapper modelMapper() {
        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STRICT)
                .setFieldMatchingEnabled(true)
                .setSkipNullEnabled(true)
                .setFieldAccessLevel(PRIVATE);
        return mapper;
    }


    @Bean
    public AbstractJdbcConfiguration jdbcConfiguration() {
        return new MySpringBootJdbcConfiguration();
    }
    @Configuration
    static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
        @Override
        protected List<?> userConverters() {
            return asList(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

更新

我的代码是:

@SpringBootApplication
@EnableJdbcAuditing
@EnableScheduling
public class Main {

    public static void main(String[] args) {
        ConfigurableApplicationContext applicationContext = SpringApplication.run(Main.class, args);
        MyEntityRepositoryrepository = applicationContext.getBean(MyEntityRepository.class);
        MyEntity entity =  new MyEntity();
        ...
        entity.setFileType(FileType.TYPE_2);
        repository.save(entity);
    }

    @Bean
    public ModelMapper modelMapper() {
        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration()
                .setMatchingStrategy(MatchingStrategies.STRICT)
                .setFieldMatchingEnabled(true)
                .setSkipNullEnabled(true)
                .setFieldAccessLevel(PRIVATE);
        return mapper;
    }

    @Bean
    public AbstractJdbcConfiguration jdbcConfiguration() {
        return new MySpringBootJdbcConfiguration();
    }

    @Configuration
    static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
        @Override
        protected List<?> userConverters() {
            return asList(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
        }

        @Bean
        public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext,
                                           NamedParameterJdbcOperations operations,
                                           @Lazy RelationResolver relationResolver,
                                           JdbcCustomConversions conversions,
                                           Dialect dialect) {

            JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect ? ((JdbcDialect) dialect).getArraySupport()
                    : JdbcArrayColumns.DefaultSupport.INSTANCE;
            DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations(),
                    arrayColumns);

            return new MyJdbcConverter(
                    mappingContext,
                    relationResolver,
                    conversions,
                    jdbcTypeFactory,
                    dialect.getIdentifierProcessing()
            );
        }
    }

    static class MyJdbcConverter extends BasicJdbcConverter {
        MyJdbcConverter(
                MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
                RelationResolver relationResolver,
                CustomConversions conversions,
                JdbcTypeFactory typeFactory,
                IdentifierProcessing identifierProcessing) {
            super(context, relationResolver, conversions, typeFactory, identifierProcessing);
        }

        @Override
        public int getSqlType(RelationalPersistentProperty property) {
            if (FileType.class.equals(property.getActualType())) {
                return Types.BIGINT;
            } else {
                return super.getSqlType(property);
            }
        }

        @Override
        public Class<?> getColumnType(RelationalPersistentProperty property) {
            if (FileType.class.equals(property.getActualType())) {
                return Long.class;
            } else {
                return super.getColumnType(property);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

但我遇到错误:

Caused by: org.postgresql.util.PSQLException: Cannot convert an instance of java.lang.String to type long
    at org.postgresql.jdbc.PgPreparedStatement.cannotCastException(PgPreparedStatement.java:925)
    at org.postgresql.jdbc.PgPreparedStatement.castToLong(PgPreparedStatement.java:810)
    at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:561)
    at org.postgresql.jdbc.PgPreparedStatement.setObject(PgPreparedStatement.java:931)
    at com.zaxxer.hikari.pool.HikariProxyPreparedStatement.setObject(HikariProxyPreparedStatement.java)
    at org.springframework.jdbc.core.StatementCreatorUtils.setValue(StatementCreatorUtils.java:414)
    at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValueInternal(StatementCreatorUtils.java:231)
    at org.springframework.jdbc.core.StatementCreatorUtils.setParameterValue(StatementCreatorUtils.java:146)
    at org.springframework.jdbc.core.PreparedStatementCreatorFactory$PreparedStatementCreatorImpl.setValues(PreparedStatementCreatorFactory.java:283)
    at org.springframework.jdbc.core.PreparedStatementCreatorFactory$PreparedStatementCreatorImpl.createPreparedStatement(PreparedStatementCreatorFactory.java:241)
    at org.springframework.jdbc.core.JdbcTemplate.execute(JdbcTemplate.java:649)
    ... 50 more
Caused by: java.lang.NumberFormatException: For input string: "TYPE_2"
    at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    at java.lang.Long.parseLong(Long.java:589)
    at java.lang.Long.parseLong(Long.java:631)
    at org.postgresql.jdbc.PgPreparedStatement.castToLong(PgPreparedStatement.java:792)
    ... 59 more
Run Code Online (Sandbox Code Playgroud)

Les*_*iak 6

请尝试以下方法:

@Bean
public AbstractJdbcConfiguration jdbcConfiguration() {
    return new MySpringBootJdbcConfiguration();
}

@Configuration
static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
    @Override
    protected List<?> userConverters() {
        return List.of(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
    }
}
Run Code Online (Sandbox Code Playgroud)

解释:

Spring 抱怨JdbcCustomConversions在自动配置类中已经定义了(由您的 bean)并且您没有启用 bean 覆盖。

JdbcRepositoriesAutoConfiguration已经改变了几次,在 Spring 2.6.2 中它有:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(AbstractJdbcConfiguration.class)
static class SpringBootJdbcConfiguration extends AbstractJdbcConfiguration {

}
Run Code Online (Sandbox Code Playgroud)

反过来,AbstractJdbcConfiguration有:

@Bean
public JdbcCustomConversions jdbcCustomConversions() {

    try {

        Dialect dialect = applicationContext.getBean(Dialect.class);
        SimpleTypeHolder simpleTypeHolder = dialect.simpleTypes().isEmpty() ? JdbcSimpleTypes.HOLDER
                : new SimpleTypeHolder(dialect.simpleTypes(), JdbcSimpleTypes.HOLDER);

        return new JdbcCustomConversions(
                CustomConversions.StoreConversions.of(simpleTypeHolder, storeConverters(dialect)), userConverters());

    } catch (NoSuchBeanDefinitionException exception) {

        LOG.warn("No dialect found. CustomConversions will be configured without dialect specific conversions.");

        return new JdbcCustomConversions();
    }
}
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,JdbcCustomConversions它没有任何条件,因此定义您自己的会引起冲突。幸运的是,它提供了一个扩展点userConverters(),可以覆盖该扩展点以提供您自己的转换器。

更新

正如评论中所讨论的:

  • FileType.byDatabaseId已损坏 - 它忽略其输入参数

  • 由于 db 中的列类型是 BIGINT,您的转换器必须从 Long 转换,而不是从 Integer 转换,这解决了读取查询的问题

  • 对于写入,存在一个未解决的错误https://github.com/spring-projects/spring-data-jdbc/issues/629有一个硬编码的假设,即枚举被转换为字符串,并且仅检查枚举 -> 字符串转换器。由于我们想要转换为 Long,因此我们需要BasicJdbcConverter通过对其进行子类化并将子类转换器注册为@Bean.

您需要重写两个方法

  • public int getSqlType(RelationalPersistentProperty property)
  • public Class<?> getColumnType(RelationalPersistentProperty property)

我对 Enum 类型和相应的列类型进行了硬编码,但您可能想要更喜欢它。

@Bean
public AbstractJdbcConfiguration jdbcConfiguration() {
    return new MySpringBootJdbcConfiguration();
}

@Configuration
static class MySpringBootJdbcConfiguration extends AbstractJdbcConfiguration {
    @Override
    protected List<?> userConverters() {
        return List.of(new DatabaseIdToFileTypeConverter(), new FileTypeToDatabaseIdConverter());
    }

    @Bean
    public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext,
                                       NamedParameterJdbcOperations operations,
                                       @Lazy RelationResolver relationResolver,
                                       JdbcCustomConversions conversions,
                                       Dialect dialect) {

        JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect ? ((JdbcDialect) dialect).getArraySupport()
                : JdbcArrayColumns.DefaultSupport.INSTANCE;
        DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations(),
                arrayColumns);

        return new MyJdbcConverter(
                mappingContext,
                relationResolver,
                conversions,
                jdbcTypeFactory,
                dialect.getIdentifierProcessing()
        );
    }
}

static class MyJdbcConverter extends BasicJdbcConverter {
    MyJdbcConverter(
            MappingContext<? extends RelationalPersistentEntity<?>, ? extends RelationalPersistentProperty> context,
            RelationResolver relationResolver,
            CustomConversions conversions,
            JdbcTypeFactory typeFactory,
            IdentifierProcessing identifierProcessing) {
        super(context, relationResolver, conversions, typeFactory, identifierProcessing);
    }

    @Override
    public int getSqlType(RelationalPersistentProperty property) {
        if (FileType.class.equals(property.getActualType())) {
            return Types.BIGINT;
        } else {
            return super.getSqlType(property);
        }
    }

    @Override
    public Class<?> getColumnType(RelationalPersistentProperty property) {
        if (FileType.class.equals(property.getActualType())) {
            return Long.class;
        } else {
            return super.getColumnType(property);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)