Search code examples
hibernatekotlinthymeleafmany-to-oneconstraintviolationexception

Kotlin Hibernate @ManyToOne MERGE violates foreign key constraint (Postgres)


I implemented a @ModelToOne Relationship and would like to use a Dropdown List via Thymeleaf. I use a separate formular to persist PlaceEntities and a formular for WorkEntities where a appropriate Place can be selected from the mentioned dropdown list. The current implementation forces me to add a Place, otherwise a WorkEntity will not be persisted. When I try to save the form without having selected a Place from the dropdown list the following Error appears:

ERROR: insert or update on table "work" violates foreign key constraint "fklyi06ir16ujsbylbtf5k33rmv" Detail: Key (place_of_premiere_id)=() is not present in table "place".

I already tried to add the "optional = true" condition (which is true by default in any case) or @JoinColumn(nullable = true) to the @ManyToOne Side but none of these worked.

Saving the form with a selected Place works like a charm but how can I do this if I want to persist a WorkEntity without a PlaceEntity (which means that the foreignkey column must be null)?

EDIT: I could solve this problem by myself. The error came from the th:value="" in the first option tag of my template. Instead of null it creates a zero args Object of Place which cannot be persisted. I put an if statement in my controller which might be not the most elegant approach but I do not know how to make a th:value= nullable in Thymeleaf.

My fixed Controller:

@PostMapping(value = ["/addWork"])
fun addWork(@Validated work: Work?, bindingResult: BindingResult): String {
    if (bindingResult.hasErrors()) {
        print(bindingResult.allErrors)
    }
    
    work?.apply {

        if (placeOfPremiere?.id == "") {
            this.placeOfPremiere = null
        }
    }

    workService.create(work!!)
    return "editor/addWork"
}

Abstract class for all Entities:

@MappedSuperclass
abstract class Entity(
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name = "system-uuid", strategy = "uuid")
    open var id: String?,
    open var title: String?,
    open var created: OffsetDateTime,
    open var modified: OffsetDateTime

Child:

     @Entity
     @Table(name = "work")
     class WorkEntity(
             id: String? = null,
             title: String? = null,
             created: OffsetDateTime = OffsetDateTime.now(),
             modified: OffsetDateTime = OffsetDateTime.now(),
             var opus: String? = null,
             var dateOfCreation: String? = null,
             var dateOfPremiere: String? = null,

      @JsonManagedReference
      @ManyToOne(
            fetch = FetchType.LAZY,
            cascade = [CascadeType.MERGE]
       ) 
      var placeOfPremiere: PlaceEntity? = null,

      ...Other Properties...equals...hashcode...

Parent:

      @Entity
      @Table(name = "place")
      class PlaceEntity(
              id: String? = null,
              title: String? = null,
              created: OffsetDateTime = OffsetDateTime.now(),
              modified: OffsetDateTime = OffsetDateTime.now(),
              var name: String? = null,
              var locality: String? = null,
              var country: String? = null,

              @Embedded
              @AttributeOverrides(
                  AttributeOverride(name = "latitude", column = Column(name = "latitude")),
                  AttributeOverride(name = "longitude", column = Column(name = "longitude"))
              )
              var coordinates: CoordinatesEntity? = CoordinatesEntity(),

              @JsonBackReference
              @OneToMany(mappedBy = "placeOfPremiere")
              var relatedWorks: MutableSet<WorkEntity>? = mutableSetOf()              
              ...Other Properties...equals...hashcode...

The Dropdown Menu of my ThymeleafTemplate:

                <div class="form-group">
                    <label for="places">Places</label>
                    <select th:field="*{placeOfPremiere.id}" class="form-control" id="places"
                            name="Place of Premiere">
                        <option th:value="">Select Places</option>
                        <option
                                th:each="place : ${places}"
                                th:value="${place.id}"
                                th:text="${place.title}">
                        </option>
                    </select>
               <span th:if="${#fields.hasErrors('placeOfPremiere')}" th:errors {placeOfPremiere}"></span>
                 </div>

Solution

  • I suppose the problem is that the Work entitie refers to a Place that doesn't exists yet. Either you mark the foreign key as deferrable initially deferred or you make sure that the order in which you call persist or merge is correct.