1

I am developing Spring Boot JPA Composite key example using Postgres. In this example, when I'm trying to save record, why I dont see any exceptions or constraint violation exception?

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: I changed SongsRepository.java to

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

and main Method code.

@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));

    }

Logs:

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.
PAA
  • 1
  • 46
  • 174
  • 282
  • You defined your key as a `Long` according to your repository interface. Instead it should be the proper type. Next to that you are inserting the exact same and due to the generated equals/hashcode hibernate will detect this as the same entity and discard one. Change some of the properties of entity 2 (not those that make up the key) which shouls make it fail. – M. Deinum Jul 03 '19 at 12:49
  • Which is because eventually a `merge` is being done. The default `isNew` check checks if the id is `null`. In your case it isn't, hence it will lead to a merge, and thus an update for the second entity instead of a new one. – M. Deinum Jul 03 '19 at 13:31
  • @M.Deinum - You're right. Merge is happening after checking `isNew`. Can you guide me how to fixed this issue now ? – PAA Jul 03 '19 at 13:35
  • Can you please share the stacktrace for logs – Mayur Jain Jul 03 '19 at 14:22
  • @Mayur Jain - Added logs – PAA Jul 03 '19 at 14:27
  • *Can you guide me how to fixed this issue now?* What do you perceive the issue to be? – Alan Hay Jul 04 '19 at 08:23
  • I also not getting the error in the same scenario I am also using composite keys – Rajat Agrawal Apr 01 '21 at 10:10

2 Answers2

2

Change:

public interface SongsRepository extends JpaRepository<Song, Long>

to

public interface SongsRepository extends JpaRepository<Song, SongId>
t4dohx
  • 675
  • 4
  • 24
  • This doesn't work. Could you please give a try ? Is this bug ? – PAA Jul 03 '19 at 13:46
  • If this did not help, there is the most probably problem in your database. Are you sure your `name`, `album` and `artist` columns are part of the primary key? Looks like a strange combination to use as a primary key to me. – t4dohx Jul 03 '19 at 13:54
  • This works now, I am so sorry that I had change `JpaRepository` to `CrudRepository` – PAA Jul 03 '19 at 14:02
  • That's good, but however, your `song` table seems like a very bad example of database normalization. Just create and `artist` and `album` tables with id and name and reference them as foreign keys in your `song` table with the song identifiable by and id. Just a simple example, consider you want to find exactly one song, you need to know the name, who the author is and which album contains it. – t4dohx Jul 03 '19 at 14:03
  • But I have to put `@PrePersist public void initIdentifier(SongId songId1) { if (songId == null) { this.songId = songId1; } }` at songs model class, if I dont put that data gets saved . Do you know why ? – PAA Jul 03 '19 at 14:05
  • I have no idea why. But seems like not a good approach? You set the id and then it gets overwritten by entity mapping (when using select). – t4dohx Jul 03 '19 at 14:08
  • I don't get why you need to do that, but seems like an another question to ask. – t4dohx Jul 03 '19 at 14:10
  • Like I said, make sure you follow relational database principes and it will work just fine. Normalize your database columns and try that. – t4dohx Jul 04 '19 at 07:02
0

Spring Data Jpa Repository functionality is implemented via the SimpleJpaRepository class containing following save(..) method:

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

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

Thus the Spring Jpa Data Repository save(...) method merges an already existing entity.

Opposed to that the naked EntityManager#persist() throws an exception if invoked with already existing entity.

The problem might be solved by adding custom behavior to Spring Data Repository/ies.

Alternatively : you can follow this

The easiest (and least invasive) way to work around this is probably by making sure the id only gets set right before the persist. This can be achieved in a @PrePersist callback:

@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.
        }
    }
    }
Avinash Gupta
  • 208
  • 5
  • 18
  • You might extend your spring data jpa repository/ repositories with custom persist() method and implement it via EntityManager#persist(), the EntityManager can be injected into the implementation class of the custom method. – Avinash Gupta Jul 03 '19 at 13:19
  • https://stackoverflow.com/questions/27151125/spring-data-jpa-composite-key-duplicate-key-record-insertion-resulting-in-update – Avinash Gupta Jul 03 '19 at 13:25
  • Set the SongId object inside initIdentifier() only. Don't set the id at the time of build the song. – Avinash Gupta Jul 03 '19 at 13:40
  • I am not sure how to actually set the SongId object inside initIdentifier(). Can you please show some code? That will really help. Thanks! – PAA Jul 03 '19 at 13:44
  • Actually not clear on ``// Create ID instance here.``, how to set this from main method – PAA Jul 03 '19 at 13:46
  • I dont think we can keep creating instance like this where we have so many create request. How everytime we will pass SongId instance to ``void initIdentifier()``, do you think this is good approach ? – PAA Jul 03 '19 at 13:54
  • `@PrePersist` is a method which is executed before persisting the entity and should be definitely not used for that purpose. Rather a constructor argument of type `SongId` should be made which will be assigned to an `id` attribute. – t4dohx Jul 03 '19 at 14:18