我有一个问题,如果不使用非常低效的代码,我似乎无法弄清楚。情况是我有一个使用 Hibernate 进行数据库访问和实体映射的 Quarkus Web 服务。在里面,有一个叫做
SanctionedEntity
的java实体,像这样:
@Entity
@Table(name = "sanctioned_entities")
public class SanctionedEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, unique = true)
private String name;
@ManyToMany
@JoinTable(
name = "sanctioned_entity_country_map",
joinColumns = { @JoinColumn(name = "sanctioned_entity_id") },
inverseJoinColumns = { @JoinColumn(name = "country_id") }
)
private Set<Country> sanctioningCountries = new HashSet<>();
//... get/set
}
主要实体具有以下底层 SQL 模式:
CREATE TABLE sanctioned_entities (
id bigint NOT NULL,
name character varying(1024) NOT NULL
);
以及每个
Country
实体到多个SanctionedEntity
实体的映射:
CREATE TABLE sanctioned_entity_country_map (
id bigint NOT NULL,
sanctioned_entity_id bigint NOT NULL,
country_id bigint NOT NULL
);
问题是,我必须为新的
SanctionedEntity
实体及其相应的Country
实体映射执行大数据集的插入。通常这通过 Hibernate 映射没有问题,但它不应该为 name
实体的 SanctionedEntity
字段重复抛出异常,而只是简单地忽略它们,但是如果一个新的 Country
映射还没有存储,这应该是保存在映射表中。
我的第一个想法是我需要用“upsert”语句来解决这个问题,用“ON CONFLICT (name) DO NOTHING”忽略重复项然后在映射表中单独执行另一个“upsert”语句以获得潜在的新
Country
映射。所以这将是两个单独的“upsert”语句。但是 Hibernate 不支持“upsert”语句,所以我不得不求助于本机 SQL 查询来进行插入。
下一个问题是,尚未为
SanctionedEntity
对象创建ID,如果它们是新的并且将被添加(而不是如果它们已经存在则被忽略),这是映射表中潜在新映射所必需的。但是我可以使用“RETURNING id”从第一个查询中获取它们。由于数据集可能很大,我尝试避免对数据集进行不必要的重复,但我最终得到了一个 Java 代码解决方案,其中我的复杂度为 O(n) + O(n * m),因为我首先需要插入实体以获取创建的 ID,这样我就可以使用它们遍历所有国家映射并插入它们:
@Transactional
public void persistAllWithMultipleCountries(List<SanctionedEntity> sanctionedEntities) {
StringBuilder entityValuesBuilder = new StringBuilder();
StringBuilder countryMapValuesBuilder = new StringBuilder();
for (SanctionedEntity entity : sanctionedEntities) {
addUpsertSanctionedEntitySqlFor(entity, entityValuesBuilder);
}
List<Long> ids = runSanctionedEntityUpsertFor(entityValuesBuilder);
for (int i = 0; i < sanctionedEntities.size(); i++) {
SanctionedEntity entity = sanctionedEntities.get(i);
entity.setId(ids.get(i));
addUpsertCountryMapsSqlFor(entity, countryMapValuesBuilder);
}
runEntityCountryMapsUpsertFor(countryMapValuesBuilder);
}
private void addUpsertSanctionedEntitySqlFor(SanctionedEntity entity, StringBuilder builder) {
String currentTimestamp = new Date().toString();
builder.append("(")
.append("'").append(entity.getName()).append("'")
.append("),");
}
private void addUpsertCountryMapsSqlFor(SanctionedEntity entity, StringBuilder builder) {
entity.getSanctioningCountries().forEach(country -> {
builder.append("(")
.append(entity.getId()).append(",")
.append(country.getId())
.append("),");
});
}
@SuppressWarnings("unchecked")
private List<Long> runSanctionedEntityUpsertFor(StringBuilder builder) {
String values = getChainedValuesWithoutLastRedundantComma(builder);
String upsertSql = "INSERT INTO "
+ "sanctioned_entities (name) "
+ "VALUES " + values + " "
+ "ON CONFLICT DO NOTHING "
+ "RETURNING id";
return entityManager.createNativeQuery(upsertSql).getResultList();
}
private void runEntityCountryMapsUpsertFor(StringBuilder builder) {
String values = getChainedValuesWithoutLastRedundantComma(builder);
String upsertSql = "INSERT INTO "
+ "sanctioned_entity_country_map (sanctioned_entity_id, country_id) "
+ "VALUES " + values + " "
+ "ON CONFLICT DO NOTHING";
entityManager.createNativeQuery(upsertSql).executeUpdate();
}
private String getChainedValuesWithoutLastRedundantComma(StringBuilder builder) {
return builder.deleteCharAt(builder.length() - 1).toString();
}
我认为这里的 Java 站点上没有真正更好的解决方案,所以我想知道我是否需要使用 PostgreSQL 服务器上的存储过程来解决这个问题并调用它。但我不确定如何将所有数据传递给程序
任何人都可以指出我更好的解决方案或解释如何使用存储过程解决这个问题吗?
编辑:我也刚刚意识到我的代码解决方案到目前为止还不能完全工作,因为创建的 ID 集的长度可能与我之前的
SanctionedEntity
列表不同,因为我只获得了创建的那些行的 ID(而不是那些已经存在的被忽略的)。所以我必须先合并它们,也返回它们 name
字段,并将 java 代码中返回的 ID 映射到具有正确名称的实体。这太复杂了,必须有更好的解决方案。