#java #spring-data-jpa #spring-batch
#java #spring-data-jpa #пакет spring-batch
Вопрос:
Пакет, который считывает две таблицы, изменяет их и записывает обратно во внешнюю базу данных (socio_db), является частью демонстрационного проекта микросервисов. Обновление базы данных пакета Spring выполняется по расписанию (каждые 24 часа), изменяет только два поля (boolean и Enum).
(Таблица, подлежащая изменению, принадлежит Facebook-подобному приложению, где socio регистрируются и добавляют другие socio в качестве associated-socio):
Перед отображением кода и сообщения об ошибке:
- Я настроил базу данных метаданных Batch (socio_batch_meta_data_db) с ее собственным источником данных, поэтому она остается отдельно от socio_db, которая является целевой базой данных;
- Я не буду включать логику, касающуюся двух полей DB, поскольку это не относится к задаче потока чтения-обработки — записи класса SocioAssociatedSocio;
- Найдите только код трех соответствующих объектов (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: