Проблемы с сохранением объекта (jpa), который является объединенной таблицей, с некоторыми дополнительными полями, в контексте пакета Spring

#java #spring-data-jpa #spring-batch

#java #spring-data-jpa #пакет spring-batch

Вопрос:

Пакет, который считывает две таблицы, изменяет их и записывает обратно во внешнюю базу данных (socio_db), является частью демонстрационного проекта микросервисов. Обновление базы данных пакета Spring выполняется по расписанию (каждые 24 часа), изменяет только два поля (boolean и Enum).

(Таблица, подлежащая изменению, принадлежит Facebook-подобному приложению, где socio регистрируются и добавляют другие socio в качестве associated-socio):

схема socio_db

Перед отображением кода и сообщения об ошибке:

  1. Я настроил базу данных метаданных Batch (socio_batch_meta_data_db) с ее собственным источником данных, поэтому она остается отдельно от socio_db, которая является целевой базой данных;
  2. Я не буду включать логику, касающуюся двух полей DB, поскольку это не относится к задаче потока чтения-обработки — записи класса SocioAssociatedSocio;
  3. Найдите только код трех соответствующих объектов (Socio, SocioAssociatedSocio, SocioAssociatedSocioId).

Сообщение об ошибке:

 2020-08-19 10:51:25,141 ERROR [restartedMain] org.springframework.batch.core.step.AbstractStep: Encountered an error executing step associatedsociodbsociowriteStep in job batchdbsociowriteJob
java.lang.IllegalArgumentException: Unable to invoke method: [public void com.artsgard.sociodbbatch.readers.AssociatedSocioReader.before(org.springframework.batch.core.StepExecution)] on object: [com.artsgard.sociodbbatch.readers.AssociatedSocioReader@9e233ce] with arguments: [[StepExecution: id=1178, version=1, name=associatedsociodbsociowriteStep, status=STARTED, exitStatus=EXECUTING, readCount=0, filterCount=0, writeCount=0 readSkipCount=0, writeSkipCount=0, processSkipCount=0, commitCount=0, rollbackCount=0, exitDescription=]]
    at org.springframework.batch.support.SimpleMethodInvoker.invokeMethod(SimpleMethodInvoker.java:112)
    at org.springframework.batch.core.listener.MethodInvokerMethodInterceptor.invoke(MethodInvokerMethodInterceptor.java:69)
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
    at com.sun.proxy.$Proxy111.beforeStep(Unknown Source)

Caused by: java.lang.reflect.InvocationTargetException: null
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.batch.support.SimpleMethodInvoker.invokeMethod(SimpleMethodInvoker.java:108)
    ... 39 common frames omitted
Caused by: org.springframework.dao.InvalidDataAccessResourceUsageException: could not extract ResultSet; SQL [n/a]; nested exception is org.hibernate.exception.SQLGrammarException: could not extract ResultSet
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.convertHibernateAccessException(HibernateJpaDialect.java:281)
    at org.springframework.orm.jpa.vendor.HibernateJpaDialect.translateExceptionIfPossible(HibernateJpaDialect.java:255)
    at org.springframework.orm.jpa.AbstractEntityManagerFactoryBean.translateExceptionIfPossible(AbstractEntityManagerFactoryBean.java:528)
    at org.springframework.dao.support.ChainedPersistenceExceptionTranslator.translateExceptionIfPossible(ChainedPersistenceExceptionTranslator.java:61)


Caused by: org.hibernate.exception.SQLGrammarException: could not extract ResultSet
    at org.hibernate.exception.internal.SQLStateConversionDelegate.convert(SQLStateConversionDelegate.java:103)
    at org.hibernate.exception.internal.StandardSQLExceptionConverter.convert(StandardSQLExceptionConverter.java:42)
    at org.hibernate.engine.jdbc.spi.SqlExceptionHelper.convert(SqlExceptionHelper.java:113)

Caused by: org.postgresql.util.PSQLException: ERROR: no existe la columna socioassoc0_.associatedsocioid
  Hint: Probablemente quiera hacer referencia a la columna «socioassoc0_.associated_socio_id».
  Position: 8
    at org.postgresql.core.v3.QueryExecutorImpl.receiveErrorResponse(QueryExecutorImpl.java:2497)
    at org.postgresql.core.v3.QueryExecutorImpl.processResults(QueryExecutorImpl.java:2233)
    at org.postgresql.core.v3.QueryExecutorImpl.execute(QueryExecutorImpl.java:310)
  

Файлы Pom и свойств:

 <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.2.RELEASE</version>
    </parent>
    
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.artsgard</groupId>
    <artifactId>sociodbbatch</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-batch</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency> 
            <groupId>org.springframework.boot</groupId> 
            <artifactId>spring-boot-starter-validation</artifactId> 
        </dependency>
         <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.batch</groupId>
            <artifactId>spring-batch-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

app.datasource.batch.url=jdbc:postgresql://localhost:5432/socio_batch_meta_data_db
app.datasource.batch.driverClassName=org.postgresql.Driver
app.datasource.batch.username=postgres
app.datasource.batch.password=Candita123

app.datasource.db.url=jdbc:postgresql://localhost:5432/socio_db
app.datasource.db.driverClassName=org.postgresql.Driver
app.datasource.db.username=postgres
app.datasource.db.password=Candita123

spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true

spring.batch.initialize-schema=always

#86400000
batch.delay= 5000

spring.jpa.hibernate.ddl-auto=none
spring.datasource.initialization-mode=never
spring.datasource.initialize=false
  

DB and JobRepositoy config:

 @Configuration
public class BatchDbMetaDataRepoConfig {
    
    @Primary
    @Bean(name = "batchDataSourceProperties")
    @ConfigurationProperties("app.datasource.batch")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Primary
    @Bean(name = "batchDataSource")
    @ConfigurationProperties("app.datasource.batch.hikari")
    public DataSource dataSource(@Qualifier("batchDataSourceProperties") DataSourceProperties dataSourceProperties) {
        return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }
}

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.artsgard.sociodbbatch.repository", "com.artsgard.sociodbbatch.config" },
                entityManagerFactoryRef = "dbEntityManagerFactory", 
                transactionManagerRef = "dbTransactionManager")

public class BatchDbRepoConfig {
    
    @Bean(name = "dbDataSourceProperties")
    @ConfigurationProperties("app.datasource.db")
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "dbDataSource")
    @ConfigurationProperties("app.datasource.db.hikari")
    public DataSource dataSource(@Qualifier("dbDataSourceProperties") DataSourceProperties dataSourceProperties) {
        return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class)
                .build();
    }
    
    @Bean(name = "dbEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            EntityManagerFactoryBuilder builder, @Qualifier("dbDataSource") DataSource dataSource) {
        return builder
                    .dataSource(dataSource)
                    .packages("com.artsgard.sociodbbatch.model")
                    .persistenceUnit("db")
                    .build();
    }

    @Bean(name = "dbTransactionManager")
    public PlatformTransactionManager transactionManager(
            @Qualifier("dbEntityManagerFactory") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

public class JobRepositoryConfig extends DefaultBatchConfigurer {
    
    @Autowired
    private PlatformTransactionManager transactionManager;
    
    @Autowired
    @Qualifier("batchDataSource")
    private DataSource datasource;

    
    @Override
    protected JobRepository createJobRepository() throws Exception {
        JobRepositoryFactoryBean factoryBean = new JobRepositoryFactoryBean();
        factoryBean.setDatabaseType(DatabaseType.POSTGRES.getProductName());
        factoryBean.setTablePrefix("BATCH_");
        factoryBean.setIsolationLevelForCreate("ISOLATION_READ_COMMITTED");
        factoryBean.setDataSource(datasource);
        factoryBean.setTransactionManager(transactionManager);
        factoryBean.afterPropertiesSet();
        return factoryBean.getObject();
    }
}
  

The Three Entities:

 @Data
@NoArgsConstructor
@Entity
@Table(name = "socio") //, catalog = "socio_db") , schema = "socio_db") , schema = "socio_db")
public class SocioModel implements Serializable { // UserDetails

    public SocioModel(Long id, String username, String password, String firstName, String lastName, String email, 
            Boolean active, List<LanguageModel> socioLanguages, List<AddressModel> socioAddresses) {
        this.id = id;
        this.password = password;
        this.username = username;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.active = active;
        this.socioLanguages = socioLanguages;
        this.socioAddresses = socioAddresses;
    }

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @NotNull
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    @NotNull
    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "first_name", nullable = true)
    private String firstName;

    @NotNull
    @Column(name = "last_name", nullable = true)
    private String lastName;

    @NotNull
    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @NotNull
    @Column(name = "register_date", nullable = false)
    private Timestamp registerDate;
    
    @NotNull
    @Column(name = "last_checkin_date", nullable = false)
    private Timestamp lastCheckinDate;

    @NotNull
    @Column(name = "active", nullable = false)
    private Boolean active;
 
    @JsonIgnore
    @OneToMany(targetEntity=SocioAssociatedSocio.class, mappedBy="socio")
    private List<SocioAssociatedSocio> socios;
    
    @JsonIgnore
    @OneToMany(targetEntity=SocioAssociatedSocio.class, mappedBy="socio") //associatedSocio
    private List<SocioAssociatedSocio> associatedSocios;
    
    @ManyToMany
    @JoinTable(name = "socio_role", joinColumns = @JoinColumn(name = "socio_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    private List<RoleModel> socioRoles;

    @NotNull
    @ManyToMany()
    @JoinTable(name = "socio_language", joinColumns = @JoinColumn(name = "socio_id"),
            inverseJoinColumns = @JoinColumn(name = "language_id"))
    private List<LanguageModel> socioLanguages;

    @OneToMany(mappedBy = "socio", cascade = CascadeType.REMOVE)
    private List<AddressModel> socioAddresses;
}

@Entity(name = "SocioAssociatedSocio")
@Table(name = "socio_associated_socio") //, catalog = "socio_db") , schema = "socio_db")
@IdClass(SocioAssociatedSocioId.class)
public class SocioAssociatedSocio implements Serializable {

    private SocioAssociatedSocio() { }

    public SocioAssociatedSocio(Long socioId, Long associatedSocioId, SocioModel socio, SocioModel associatedSocio, AssociatedSocioState associatedSocioState, Timestamp associatedSocioDate) {
        this.socioId = socioId;
        this.associatedSocioId = associatedSocioId;
        this.socio = socio;
        this.associatedSocio = associatedSocio;
        this.associatedSocioState = associatedSocioState;
        this.associatedSocioDate = associatedSocioDate;
    }
    
    @Id
    private Long socioId;
    
    @Id
    private Long associatedSocioId;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "socioId", updatable = false, insertable = false,
            referencedColumnName = "id")
    private SocioModel socio;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "associatedSocioId", updatable = false, insertable = false,
            referencedColumnName = "id")
    private SocioModel associatedSocio;
    
    public enum AssociatedSocioState {
        PENDING, EXPIRED, ACCEPTED, DENIED
    }

    @Column(name = "associated_socio_state", length = 100)
    @Enumerated(EnumType.STRING)
    private AssociatedSocioState associatedSocioState;
    
    @Column(name = "associated_socio_date", nullable = true)
    private Timestamp associatedSocioDate;

    public long getSocioId() {
        return socioId;
    }

    public void setSocioId(long socioId) {
        this.socioId = socioId;
    }

    public long getAssociatedSocioId() {
        return associatedSocioId;
    }

    public void setAssociatedSocioId(long associatedSocioId) {
        this.associatedSocioId = associatedSocioId;
    }

    public SocioModel getSocio() {
        return socio;
    }

    public void setSocio(SocioModel socio) {
        this.socio = socio;
    }

    public SocioModel getAssociatedSocio() {
        return associatedSocio;
    }

    public void setAssociatedSocio(SocioModel associatedSocio) {
        this.associatedSocio = associatedSocio;
    }

    public AssociatedSocioState getAssociatedSocioState() {
        return associatedSocioState;
    }

    public void setAssociatedSocioState(AssociatedSocioState associatedSocioState) {
        this.associatedSocioState = associatedSocioState;
    }

    public Timestamp getAssociatedSocioDate() {
        return associatedSocioDate;
    }

    public void setAssociatedSocioDate(Timestamp associatedSocioDate) {
        this.associatedSocioDate = associatedSocioDate;
    }
}

public class SocioAssociatedSocioId implements Serializable {
    private SocioAssociatedSocioId() { }
    private Long socioId;
    private Long associatedSocioId;

    @Override
    public int hashCode() {
        return (int) (socioId   associatedSocioId);
    }

    @Override
    public boolean equals(Object object) {
        if (object instanceof SocioAssociatedSocioId) {
            SocioAssociatedSocioId otherId = (SocioAssociatedSocioId) object;
            return (otherId.socioId == this.socioId)
                    amp;amp; (otherId.associatedSocioId == this.associatedSocioId);
        }
        return false;
    }
}
  

The Batch-Flow

 @Configuration
@EnableBatchProcessing
public class BatchFlowConfig {
    
    @Autowired
    @Qualifier("dbTransactionManager") 
    private PlatformTransactionManager transactionManager;
    
    @Autowired
    private JobBuilderFactory jobBuilders;

    @Autowired
    private StepBuilderFactory stepBuilders;
    
    @Autowired
    private JobRepository jobRepository;
    
    @Autowired
    private SocioProcessor socioProcessor;

    @Autowired
    private SocioReader socioReader;

    @Autowired
    private SocioWriter socioWriter;
    
    @Autowired
    private AssociatedSocioProcessor associatedProcessor;

    @Autowired
    private AssociatedSocioReader associatedReader;

    @Autowired
    private AssociatedSocioWriter associatedWriter;

    @Bean(name = "sociojob")
    public Job userDbJob() throws Exception {
        return jobBuilders.get("batchdbsociowriteJob")
                .repository(jobRepository)
                .start(socioStep())
                .next(associatedSocioStep())
                .build();
    }

    @Bean
    public Step socioStep() throws Exception {
        return stepBuilders.get("sociobatchdbsociowriteStep")
                .<SocioModel, SocioModel>chunk(20)
                .reader(socioReader)
                .processor(socioProcessor)
                .writer(socioWriter)
                .transactionManager(transactionManager)
                .build();
    }
    
    @Bean
    public Step associatedSocioStep() throws Exception {
        return stepBuilders.get("associatedsociodbsociowriteStep")
                .<SocioAssociatedSocio, SocioAssociatedSocio>chunk(20)
                .reader(associatedReader)
                .processor(associatedProcessor)
                .writer(associatedWriter)
                .transactionManager(transactionManager)
                .build();
    }
}
  

Reader:

 @Component
public class AssociatedSocioReader implements ItemReader<SocioAssociatedSocio> {
   
    @Autowired
    private AssociatedSocioRepository repo;

    private Iterator<SocioAssociatedSocio> associatedSocioIterator;

    @BeforeStep
    public void before(StepExecution stepExecution) {
        associatedSocioIterator = repo.findAll().iterator();
    }

    @Override
    public SocioAssociatedSocio read() {
        if (associatedSocioIterator != null amp;amp; associatedSocioIterator.hasNext()) {
            return associatedSocioIterator.next();
        } else {
            return null;
        }
    }
}
  

Процессор:

 @Component
public class AssociatedSocioProcessor implements ItemProcessor<SocioAssociatedSocio, SocioAssociatedSocio> {

    @Autowired
    private AssociatedSocioRepository repo;

    @Override
    public SocioAssociatedSocio process(SocioAssociatedSocio associated) throws Exception {
        associated.setAssociatedSocioState(SocioAssociatedSocio.AssociatedSocioState.EXPIRED);
        // logic not displayed
        return associated;
    }
}
  

Writer:

 @Component
public class AssociatedSocioWriter implements ItemWriter<SocioAssociatedSocio> {
    
    @Autowired
    private AssociatedSocioRepository repo;
    
    @Override
    public void write(List<? extends SocioAssociatedSocio> associated) throws Exception {
        List<SocioAssociatedSocio> list = new ArrayList();
        
        for (SocioAssociatedSocio scs: associated) {
            if(scs != null) {
                list.add(scs);
            }
        }
        repo.saveAll(list);
    }
}
  

Заключительный комментарий:
Работающий интерфейс Socio reader-processor-writer идентичен показанному выше. Проблема заключается в том факте, что SocioAssociatedSocio, который является объединенной таблицей (с дополнительными полями), отличается от Socio-entity (который является обычной сущностью).

Я полагаю, что есть некоторая проблема с управлением транзакциями и entitymanager (фабрики).

Любая помощь, комментарии по улучшению приветствуются.

спасибо, что изучили этот willem

Комментарии:

1. Кстати, при сохранении объекта SocioAssociatedSocio вне программы batch-writer (предоставляющей два необходимых идентификатора) все работает идеально.

2. Вы уверены, что TransactionManager, автоматически подключенный JobRepositoryConfig и используемый в createJobRepository , является менеджером транзакций JPA?

3. Я считаю, что нет. База данных и транзакции socio_batch_meta_data_db выполняются Spring-Batch (хранение метаданных, которые, как я вижу, происходят) с использованием PlatformTransactionManager, объявленного в JobRepositoryConfig. Я полагаю, что часть socio_db, объявленная в BatchDbRepoConfig, имеет свою единственную транзакцию и диспетчер объектов (BatchDbRepoConfig). Этот факт подтверждается SocioWriter, который фактически корректно сохраняет объект SocioModel. Как уже говорилось, AssociatedSocioWriter и предыдущий редактор и reader идентичны SocioReader / SocioProssesor / SocioWriter, которые действительно работают!

4. Хорошо, я решил проблему. Мне нужно было сделать две вещи в модели SocioAssociatedSocio. Сначала я добавил аннотацию имени столбца для двух идентификаторов следующим образом: @Column(name = «socio_id», nullable = true) @Column(name = «associated_socio_id», nullable = true), касающуюся двух идентификаторов socioId и associatedSocioId

5. Затем мне нужно было исправить ссылки @JoinColumn двух идентификаторов на подчеркнутые вместо Camelcase: