【问题标题】:Spring Data JPA - While saving duplicate Composite Key doesn't get any errorSpring Data JPA - 保存重复的复合键时不会出现任何错误
【发布时间】:2026-01-13 12:20:02
【问题描述】:

我正在开发 使用 Postgres 的 Spring Boot JPA 组合键示例。在这个例子中,当我试图保存记录时,为什么我没有看到任何异常或违反约束异常?

SongId.java

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Embeddable
public class SongId implements Serializable {
    private static final long serialVersionUID = 1L;

    private String name;
    private String album;
    private String artist;
}

Song.java

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Entity
public class Song {
    @EmbeddedId 
    private SongId id;

    private int duration;
    private String genre;
    private LocalDateTime releaseDate;
    private int rating;
    private String downloadUrl;
}

SongsRepository.java

public interface SongsRepository extends JpaRepository<Song, Long>{

}

MainApp.java

@SpringBootApplication
public class CompositeApplication implements CommandLineRunner{

    public static void main(String[] args) {
        SpringApplication.run(CompositeApplication.class, args);
    }
    @Autowired
    private SongsRepository repo;


    @Override
    public void run(String... args) throws Exception {
        SongId songId1 = SongId.builder().name("John").album("AlbumA").artist("ArtistA").build();

        Song song = Song.builder().id(songId1).downloadUrl("http://www.gmail.com").duration(23)
                .genre("MyGene").rating(1).releaseDate(LocalDateTime.now()).build();
        Song song2 = Song.builder().id(songId1).downloadUrl("http://www.gmail.com").duration(23)
                .genre("MyGene").rating(1).releaseDate(LocalDateTime.now()).build();

        try {
            repo.saveAll(Arrays.asList(song, song2));
        } catch (Exception e) {
            System.out.println(e.getMessage());
        }
    }
}

Edit-1: 我将 SongsRepository.java 更改为

public interface SongsRepository extends CrudRepository<Song, SongId>{}

和主要方法代码。

@Override
    public void run(String... args) throws Exception {
        SongId songId1 = SongId.builder().name("John").album("AlbumA").artist("ArtistA").build();

        Song song = Song.builder().songId(songId1).downloadUrl("http://www.gmail.com").duration(23)
                .genre("MyGene").rating(4).releaseDate(LocalDateTime.now()).build();
        Song song2 = Song.builder().songId(songId1).downloadUrl("http://www.yahoo.com").duration(25)
                .genre("Sample Testung").rating(2).releaseDate(LocalDateTime.now()).build();

        repo.saveAll(Arrays.asList(song, song2));

    }

日志:

2019-07-03 19:56:31.901  INFO 5420 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 46ms. Found 1 repository interfaces.
2019-07-03 19:56:32.265  INFO 5420 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2019-07-03 19:56:32.378  INFO 5420 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2019-07-03 19:56:32.415  INFO 5420 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [
    name: default
    ...]
2019-07-03 19:56:32.466  INFO 5420 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate Core {5.3.10.Final}
2019-07-03 19:56:32.467  INFO 5420 --- [           main] org.hibernate.cfg.Environment            : HHH000206: hibernate.properties not found
2019-07-03 19:56:32.600  INFO 5420 --- [           main] o.hibernate.annotations.common.Version   : HCANN000001: Hibernate Commons Annotations {5.0.4.Final}
2019-07-03 19:56:32.747  INFO 5420 --- [           main] org.hibernate.dialect.Dialect            : HHH000400: Using dialect: org.hibernate.dialect.PostgreSQLDialect
2019-07-03 19:56:32.895  INFO 5420 --- [           main] o.h.e.j.e.i.LobCreatorBuilderImpl        : HHH000421: Disabling contextual LOB creation as hibernate.jdbc.lob.non_contextual_creation is true
2019-07-03 19:56:32.899  INFO 5420 --- [           main] org.hibernate.type.BasicTypeRegistry     : HHH000270: Type registration [java.util.UUID] overrides previous : org.hibernate.type.UUIDBinaryType@4ad3d266
Hibernate: 

    drop table if exists composite.song cascade
Hibernate: 

    create table composite.song (
       album varchar(255) not null,
        artist varchar(255) not null,
        name varchar(255) not null,
        download_url varchar(255),
        duration int4 not null,
        genre varchar(255),
        rating int4 not null,
        release_date timestamp,
        primary key (album, artist, name)
    )
2019-07-03 19:56:33.350  INFO 5420 --- [           main] o.h.t.schema.internal.SchemaCreatorImpl  : HHH000476: Executing import script 'org.hibernate.tool.schema.internal.exec.ScriptSourceInputNonExistentImpl@41ad373'
2019-07-03 19:56:33.352  INFO 5420 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2019-07-03 19:56:33.404 DEBUG 5420 --- [           main] .c.JpaMetamodelMappingContextFactoryBean : Initializing JpaMetamodelMappingContext…
2019-07-03 19:56:33.410 DEBUG 5420 --- [           main] .c.JpaMetamodelMappingContextFactoryBean : Finished initializing JpaMetamodelMappingContext!
2019-07-03 19:56:33.557 DEBUG 5420 --- [           main] o.s.d.r.c.s.RepositoryFactorySupport     : Initializing repository instance for com.example.repository.SongsRepository…
2019-07-03 19:56:33.603 DEBUG 5420 --- [           main] o.s.d.j.r.query.JpaQueryFactory          : Looking up query for method findBySongId_AlbumAndSongId_ArtistAndSongId_Name
2019-07-03 19:56:33.604 DEBUG 5420 --- [           main] o.s.d.jpa.repository.query.NamedQuery    : Looking up named query Song.findBySongId_AlbumAndSongId_ArtistAndSongId_Name
2019-07-03 19:56:33.606 DEBUG 5420 --- [           main] o.s.d.jpa.repository.query.NamedQuery    : Did not find named query Song.findBySongId_AlbumAndSongId_ArtistAndSongId_Name
2019-07-03 19:56:33.646 DEBUG 5420 --- [           main] o.s.d.r.c.s.RepositoryFactorySupport     : Finished creation of repository instance for com.example.repository.SongsRepository.
2019-07-03 19:56:33.713  INFO 5420 --- [           main] com.example.CompositeApplication         : Started CompositeApplication in 2.59 seconds (JVM running for 3.333)
2019-07-03 19:56:33.722 DEBUG 5420 --- [           main] stomAnnotationTransactionAttributeSource : Adding transactional method 'saveAll' with attribute: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Hibernate: 
    select
        song0_.album as album1_0_0_,
        song0_.artist as artist2_0_0_,
        song0_.name as name3_0_0_,
        song0_.download_url as download4_0_0_,
        song0_.duration as duration5_0_0_,
        song0_.genre as genre6_0_0_,
        song0_.rating as rating7_0_0_,
        song0_.release_date as release_8_0_0_ 
    from
        composite.song song0_ 
    where
        song0_.album=? 
        and song0_.artist=? 
        and song0_.name=?
Hibernate: 
    insert 
    into
        composite.song
        (download_url, duration, genre, rating, release_date, album, artist, name) 
    values
        (?, ?, ?, ?, ?, ?, ?, ?)
Hibernate: 
    update
        composite.song 
    set
        download_url=?,
        duration=?,
        genre=?,
        rating=?,
        release_date=? 
    where
        album=? 
        and artist=? 
        and name=?
2019-07-03 19:56:33.773  INFO 5420 --- [       Thread-4] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2019-07-03 19:56:33.776  INFO 5420 --- [       Thread-4] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2019-07-03 19:56:33.779  INFO 5420 --- [       Thread-4] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

【问题讨论】:

  • 您根据存储库接口将密钥定义为Long。相反,它应该是正确的类型。接下来,您将插入完全相同的内容,并且由于生成的等于/哈希码,休眠会将其检测为相同的实体并丢弃一个。更改实体 2 的一些属性(不是那些构成密钥的属性),这应该使它失败。
  • 这是因为最终会完成 merge。默认的isNew 检查检查 id 是否为null。在您的情况下,它不是,因此它将导致合并,从而更新第二个实体而不是新实体。
  • @M.Deinum - 你是对的。在检查isNew 后进行合并。你能指导我现在如何解决这个问题吗?
  • 能否分享一下日志的堆栈跟踪
  • @Mayur Jain - 添加日志

标签: java postgresql spring-data-jpa


【解决方案1】:

变化:

public interface SongsRepository extends JpaRepository<Song, Long>

public interface SongsRepository extends JpaRepository<Song, SongId>

【讨论】:

  • 这不起作用。你能试一试吗?这是错误吗?
  • 如果这没有帮助,很可能是您的数据库中存在问题。您确定您的namealbumartist 列是主键的一部分吗?对我来说,用作主键的组合看起来很奇怪。
  • 现在可以了,很抱歉我将JpaRepository更改为CrudRepository
  • 这很好,但是,您的song 表似乎是数据库规范化的一个非常糟糕的例子。只需创建带有 id 和名称的 artistalbum 表,并将它们作为外键引用到您的 song 表中,歌曲可通过 id 识别。举个简单的例子,假设您想准确地找到一首歌曲,您需要知道名称、作者是谁以及包含它的专辑。
  • 但是我必须把@PrePersist public void initIdentifier(SongId songId1) { if (songId == null) { this.songId = songId1; } }放在歌曲模型类,如果我不把数据保存起来的话。你知道为什么吗?
【解决方案2】:

Spring Data Jpa Repository 功能是通过包含以下 save(..) 方法的 SimpleJpaRepository 类实现的:

@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

因此 Spring Jpa Data Repository save(...) 方法合并了一个已经存在的实体。

与裸 EntityManager#persist() 相反,如果使用已经存在的实体调用,则会引发异常。

可以通过向 Spring Data Repository/ies 添加自定义行为来解决该问题。

或者:你可以关注this

解决此问题的最简单(且侵入性最小)的方法可能是确保仅在持久化之前设置 id。这可以在 @PrePersist 回调中实现:

@Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Entity
    public class Song {
        @EmbeddedId
        private SongId id;

        private int duration;
        private String genre;
        private LocalDateTime releaseDate;
        private int rating;
        private String downloadUrl;

    @PrePersist
    void initIdentifier() {
        SongId songId1 = SongId.builder().name("John").album("AlbumA").artist("ArtistA").build();
        if (id == null) {
            this.id = … songId1 // Create ID instance here.
        }
    }
    }

【讨论】:

  • 你可以用自定义的persist()方法扩展你的spring data jpa repository/ repositories,并通过EntityManager#persist()来实现,EntityManager可以注入到自定义方法的实现类中。跨度>
  • 仅在 initIdentifier() 中设置 SongId 对象。不要在构建歌曲时设置 id。
  • 我不确定如何在 initIdentifier() 中实际设置 SongId 对象。你能显示一些代码吗?这真的很有帮助。谢谢!
  • 其实在// Create ID instance here.上不清楚,怎么从main方法中设置这个
最近更新 更多