从数据库中获取实体及其集合包含空对象。这与 postgresql 查询形成对比,后者不返回空对象。
我尝试在处理器中进行 JPQL 查询
select pa from PreAlignment pa left join fetch pa.preAlignmentSources order by pa.id asc
,它发现空集合项。同时,如果我在控制台中运行相同的查询select * from pre_alignment_sources where pre_alignment_id = 4151;
,它只会返回两行没有任何空对象的好行。
这是我运行 Spring Batch Job 并在处理器中的第 36 行停止时在调试窗口中的样子。
这是在控制台中运行
select * from pre_alignment_sources where pre_alignment_id = 4151;
的结果。顺序栏被截掉了,但分别写着0和1。
package nathanlively.subalignercss.adapter.out.persistence;
import jakarta.persistence.*;
import lombok.*;
import nathanlively.subalignercss.Models.BaseEntity;
import org.hibernate.Hibernate;
import java.io.Serial;
import java.io.Serializable;
import java.util.*;
@Getter
@Setter
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "pre_alignments")
public class PreAlignment extends BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = 3564048730879366680L;
@Id
@GeneratedValue
private Long id;
@Builder.Default()
@OneToMany(mappedBy = "preAlignment", cascade = CascadeType.ALL, orphanRemoval = true)
@ToString.Exclude
@OrderColumn
private List<PreAlignmentSource> preAlignmentSources = new LinkedList<>();
@Builder.Default
@Column(name = "is_from_manufacturer")
private Boolean isFromManufacturer = false;
@Builder.Default
@Column(name = "is_from_user")
private Boolean isFromUser = false;
@Builder.Default
@Column(name = "is_public", nullable = false)
private Boolean isPublic = false;
@Column(name = "score")
private float score;
@Column(name = "notes")
private String notes;
public void addPreAlignmentSource(PreAlignmentSource preAlignmentSource) {
preAlignmentSources.add(preAlignmentSource);
preAlignmentSource.setPreAlignment(this);
}
public void removePreAlignmentSource(PreAlignmentSource preAlignmentSource) {
preAlignmentSources.remove(preAlignmentSource);
if(preAlignmentSource != null) {
preAlignmentSource.setPreAlignment(null);
}
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
PreAlignment that = (PreAlignment) o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
package nathanlively.subalignercss.adapter.out.persistence;
import jakarta.persistence.*;
import lombok.*;
import nathanlively.subalignercss.Models.BaseEntity;
import org.hibernate.Hibernate;
import java.io.Serial;
import java.io.Serializable;
import java.util.Objects;
@Getter
@Setter
@Entity
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "pre_alignment_sources")
public class PreAlignmentSource extends BaseEntity implements Serializable {
@Serial
private static final long serialVersionUID = -5343480867438042527L;
@Id
@GeneratedValue
@Column(nullable = false)
private Long id;
@ToString.Exclude
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "pre_alignment_id")
private PreAlignment preAlignment;
@ToString.Exclude
@ManyToOne(fetch = FetchType.LAZY, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
@JoinColumn(name = "speaker_id", nullable = false)
private Speaker speaker;
@Builder.Default
@Column(nullable = false)
private Boolean polarity = false; // false = 0 = normal; true = 1 = inverted;
@Builder.Default
@Column(nullable = false)
private Float delay = 0F;
@Builder.Default
@Column(nullable = false)
private Float gain = 0F;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || Hibernate.getClass(this) != Hibernate.getClass(o)) return false;
PreAlignmentSource that = (PreAlignmentSource) o;
return getId() != null && Objects.equals(getId(), that.getId());
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
@Bean
public Step removeNullSourcePreAlignments(JobRepository jobRepository, PlatformTransactionManager transactionManager,
JpaItemWriter<PreAlignment> preAlignmentWriter, NullSourceItemProcessor nullSourceItemProcessor) {
return new StepBuilder("removeNullSourcePreAlignments", jobRepository)
.<PreAlignment, PreAlignment>chunk(100, transactionManager)
.reader(preAlignmentJpaItemReader())
.processor(nullSourceItemProcessor)
.writer(preAlignmentWriter)
.build();
}
@Bean
public JpaCursorItemReader<PreAlignment> preAlignmentJpaItemReader() {
return new JpaCursorItemReaderBuilder<PreAlignment>()
.name("preAlignmentJpaItemReader")
.entityManagerFactory(entityManagerFactory)
.queryString("select pa from PreAlignment pa left join fetch pa.preAlignmentSources order by pa.id asc")
.build();
}
@Bean
public NullSourceItemProcessor nullSourceItemProcessor() {
return new NullSourceItemProcessor();
}
@Bean
public JpaItemWriter<PreAlignment> preAlignmentWriter() {
return new JpaItemWriterBuilder<PreAlignment>()
.entityManagerFactory(entityManagerFactory)
.build();
}
package nathanlively.subalignercss.BatchProcessing.CleanDatabase;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import nathanlively.subalignercss.adapter.out.persistence.PreAlignment;
import nathanlively.subalignercss.adapter.out.persistence.PreAlignmentSource;
import org.springframework.batch.item.ItemProcessor;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Objects;
@Log4j2
@RequiredArgsConstructor
public class NullSourceItemProcessor implements ItemProcessor<PreAlignment, PreAlignment> {
private static int counter = 0;
@Override
@Transactional
public PreAlignment process(@NotNull PreAlignment preAlignment) throws Exception {
counter++;
if (counter % 100 == 0) {
log.info("Searching for null sources. Processing hundredth PreAlignment with id: " + preAlignment.getId());
}
List<PreAlignmentSource> sourcesToRemove = preAlignment.getPreAlignmentSources().stream()
.filter(Objects::isNull)
.toList();
if (!sourcesToRemove.isEmpty()) {
log.info("Found null source on preAlignment " + preAlignment.getId() + ". Attempting to remove.");
for (PreAlignmentSource source : sourcesToRemove) {
preAlignment.removePreAlignmentSource(source);
}
return preAlignment;
} else {
return null;
}
}
}
这是设计使然。如果没有找到匹配,Left Join 应该返回 null 值。
这是 JPA 查询:
select pa from PreAlignment pa left join fetch pa.preAlignmentSources order by pa.id asc
它被翻译成原生 SQL,如下所示:
select pa.id, pas.pre_alignment_id from pre_alignments pa left join pre_alignment_sources pas on pa.id = pas.pre_alignment_id order by pa.id asc
如果 pa.id 没有与 pas.pre_alignment_id 对应的列,则仍返回 pa.id,但 pas.pre_alignment_id 将为 null。
例如,如果 pre_alignments 具有以下值:
id
1
2
并且 pre_alignment_sources 具有以下值:
id pre_alignment_id
1 1
2 1
那么查询的结果将是:
pa.id pas.pre_alignment_id
1 1
1 1
2 null
这就是结果中存在空元素的原因