title: Spring Data save doesn't DELETE
body: <p><code>removeRecipe</code> in Cookbook removes a <code>Recipe</code> from a <code>Cookbook</code>. After setting the references to null and removing the entity from the collections the entity is not deleted. </p>
  
  <pre><code>@Entity
  public class Cookbook implements Identifiable&lt;Cookbook&gt; {
      private static final Logger LOG = LoggerFactory.getLogger(Cookbook.class);
      private Long id;
      private String title;
      private List&lt;CookbookRecipe&gt; cookbookRecipes = new ArrayList&lt;&gt;();
  
      public Cookbook() {}
  
      public Cookbook(String title) {
          this.title = title;
      }
  
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = "id")
      public Long getId() {
          return id;
      }
  
      public void setId(Long id) {
          this.id = id;
      }
  
      @Basic
      @Column(name = "title")
      public String getTitle() {
          return title;
      }
      public void setTitle(String title) {
          this.title = title;
      }
  
      @OneToMany(fetch = FetchType.EAGER, mappedBy = "cookbook", cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REMOVE}, orphanRemoval = true)
      public List&lt;CookbookRecipe&gt; getCookbookRecipes() {
          return cookbookRecipes;
      }
  
      /**
       * The setter is called by hibernate.
       * @param cookbookRecipes maybe null, maybe the collection is not even ready for read access.
       *                    Don't do anything with the collection here!
       */
      public void setCookbookRecipes(List&lt;CookbookRecipe&gt; cookbookRecipes) {
          this.cookbookRecipes = cookbookRecipes;
      }
  
      /**
       * Returns a List that must remain unchanged.
       */
      @Transient
      public List&lt;Recipe&gt; getRecipes() {
          return Collections.unmodifiableList(getCookbookRecipes().stream().map(CookbookRecipe::getRecipe).collect(Collectors.toList()));
      }
  
      public void addRecipe(Recipe recipe, String createdBy, Date createdDate) {
          final CookbookRecipe cookbookRecipe = new CookbookRecipe(this, recipe);
          cookbookRecipe.setCreatedBy(createdBy);
          cookbookRecipe.setCreatedDate(createdDate);
          if( !cookbookRecipes.contains(cookbookRecipe) &amp;&amp; !recipe.getCookbookRecipes().contains(cookbookRecipe)) {
              if( !cookbookRecipes.add(cookbookRecipe) ) {
                  LOG.error("Failed to add cookbookRecipe " + cookbookRecipe + " to collection cookbookRecipes " + cookbookRecipes);
              }
              if( !recipe.getCookbookRecipes().add( cookbookRecipe ) ) {
                  LOG.error("Failed to add cookbookRecipe " + cookbookRecipe + " to collection recipe.getCookbookRecipes " + recipe.getCookbookRecipes());
              }
          }
      }
  
      public void removeRecipe(Recipe recipe) {
  
          for (Iterator&lt;CookbookRecipe&gt; iterator = cookbookRecipes.iterator();
               iterator.hasNext(); ) {
              CookbookRecipe cookbookRecipe = iterator.next();
  
              if (cookbookRecipe.getCookbook().equals(this) &amp;&amp;
                      cookbookRecipe.getRecipe().equals(recipe)) {
                  iterator.remove();
                  recipe.getCookbookRecipes().remove(cookbookRecipe);
                  cookbookRecipe.setCookbook(null);
                  cookbookRecipe.setRecipe(null);
              }
          }
      }
  
      @Override
      public String toString() {
          return title;
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
          Cookbook that = (Cookbook) o;
          return getId() != null &amp;&amp; Objects.equals(getId(), that.getId());
      }
  
      @Override
      public int hashCode() {
          return 31;
      }
  
      @Override
      public boolean equalsByBusinessKey(Cookbook other) {
          if (this == other) return true;
          if (other == null || getClass() != other.getClass()) return false;
          return Objects.equals(getTitle(), other.getTitle());
      }
  }
  
  @Entity
  @Table(name = "cookbook_recipe")
  public class CookbookRecipe implements Serializable {
      @EmbeddedId
      private CookbookRecipePk pk;
  
      @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
      @MapsId("cookbookId")
      private Cookbook cookbook;
  
      @ManyToOne(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
      @MapsId("recipeId")
      private Recipe recipe;
  
      private Date createdDate;
      private String createdBy;
  
      public CookbookRecipe() {
      }
  
      public CookbookRecipe(Cookbook cookbook, Recipe recipe) {
          this.cookbook = cookbook;
          this.recipe = recipe;
          this.pk = new CookbookRecipePk(cookbook.getId(), recipe.getId());
      }
  
      public CookbookRecipePk getPk() {
          return pk;
      }
  
      public void setPk(CookbookRecipePk pk) {
          this.pk = pk;
      }
  
      @Transient
      public Cookbook getCookbook() {
          return cookbook;
      }
  
      public void setCookbook(Cookbook cookbook) {
          this.cookbook = cookbook;
      }
  
      @Transient
      public Recipe getRecipe() {
          return recipe;
      }
  
      public void setRecipe(Recipe recipe) {
          this.recipe = recipe;
      }
  
      @Temporal(TemporalType.DATE)
      @Column(name = "CREATED_DATE", nullable = false, length = 10)
      public Date getCreatedDate() {
          return this.createdDate;
      }
  
      public void setCreatedDate(Date createdDate) {
          this.createdDate = createdDate;
      }
  
      @Column(name = "CREATED_BY", nullable = false, length = 10)
      public String getCreatedBy() {
          return this.createdBy;
      }
  
      public void setCreatedBy(String createdBy) {
          this.createdBy = createdBy;
      }
  
      public boolean equals(Object o) {
          if (this == o)
              return true;
          if (o == null || getClass() != o.getClass())
              return false;
  
          CookbookRecipe that = (CookbookRecipe) o;
  
          return Objects.equals(getPk(), that.getPk());
      }
  
      public int hashCode() {
          return 31;
      }
  
      @Override
      public String toString() {
          final StringBuilder sb = new StringBuilder("CookbookRecipe{");
          sb.append("pk=")
            .append(pk);
          sb.append('}');
          return sb.toString();
      }
  }
  
  @Embeddable
  public class CookbookRecipePk implements java.io.Serializable {
  
      @Column(name = "cookbook_id")
      private Long cookbookId;
  
      @Column(name = "recipe_id")
      private Long recipeId;
  
      public CookbookRecipePk() {}
  
      public CookbookRecipePk(Long cookbookId, Long recipeId) {
          this.cookbookId = cookbookId;
          this.recipeId = recipeId;
      }
  
      public Long getCookbookId() {
          return cookbookId;
      }
  
      public void setCookbookId(Long cookbookId) {
          this.cookbookId = cookbookId;
      }
  
      public Long getRecipeId() {
          return recipeId;
      }
  
      public void setRecipeId(Long recipeId) {
          this.recipeId = recipeId;
      }
  
  
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
  
          CookbookRecipePk that = (CookbookRecipePk) o;
  
          return null != cookbookId &amp;&amp; null != recipeId &amp;&amp;
                  Objects.equals(cookbookId, that.cookbookId) &amp;&amp;
                  Objects.equals(recipeId, that.recipeId);
      }
  
      public int hashCode() {
          return 31;
      }
  
      @Override
      public String toString() {
          final StringBuilder sb = new StringBuilder("CookbookRecipePk{");
          sb.append("cookbookId=")
            .append(cookbookId);
          sb.append(", recipeId=")
            .append(recipeId);
          sb.append('}');
          return sb.toString();
      }
  }
  
  @Entity
  public class Recipe implements Serializable, Identifiable&lt;Recipe&gt; {
      private Long id;
      private String title;
      private Category category;
      private List&lt;CookbookRecipe&gt; cookbookRecipes = new ArrayList&lt;&gt;();
  
      public Recipe(String title) {
          this.title = title;
      }
  
      public Recipe() {}
  
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      @Column(name = "id")
      public Long getId() {
          return id;
      }
  
      public void setId(Long id) {
          this.id = id;
      }
  
      @Basic
      @Column(name = "title")
      public String getTitle() {
          return title;
      }
      public void setTitle(String title) {
          this.title = title;
      }
  
      @ManyToOne(cascade = CascadeType.ALL)
      @JoinColumn(name = "category_id", referencedColumnName = "id")
      public Category getCategory() {
          return category;
      }
  
      public void setCategory(Category category) {
          this.category = category;
      }
  
      @OneToMany(fetch = FetchType.EAGER, mappedBy = "recipe", cascade = {CascadeType.PERSIST, CascadeType.MERGE}, orphanRemoval = true)
      public List&lt;CookbookRecipe&gt; getCookbookRecipes() {
          return cookbookRecipes;
      }
  
      public void setCookbookRecipes(List&lt;CookbookRecipe&gt; cookbookRecipes) {
          this.cookbookRecipes = cookbookRecipes;
      }
  
      @Override
      public boolean equals(Object o) {
          if (this == o) return true;
          if (o == null || getClass() != o.getClass()) return false;
          Recipe that = (Recipe) o;
          return getId() != null &amp;&amp; Objects.equals(getId(), that.getId());
      }
  
      @Override
      public int hashCode() {
          return 31;
      }
  
      @Override
      public String toString() {
          final StringBuilder sb = new StringBuilder("Recipe{");
          sb.append("id=")
            .append(id);
          sb.append(", title='")
            .append(title)
            .append('\'');
          sb.append(", cookbookRecipes=")
            .append(cookbookRecipes);
          sb.append('}');
          return sb.toString();
      }
  
      @Override
      public boolean equalsByBusinessKey(Recipe other) {
          if (this == other) return true;
          if (other == null || getClass() != other.getClass()) return false;
          return Objects.equals(getTitle(), other.getTitle());
      }
  }
  </code></pre>
  
  <p>Test</p>
  
  <pre><code>@RunWith(SpringRunner.class)
  @DataJpaTest
  public class CookbookRepositoryIntegrationTest {
  
      @Autowired
      RecipeRepository recipeRepository;
      @Autowired
      CookbookRepository cookbookRepository;
      @Autowired
      CookbookRecipeRepository cookbookRecipeRepository;
  
      @Test
      public void WhenAddingSameAssociationAgain_ThenNoException() {
          Recipe recipe = new Recipe();
          recipe.setTitle("A Recipe");
          recipe = recipeRepository.save(recipe);
  
          Cookbook cookbook = new Cookbook();
          cookbook.setTitle("A Cookbook");
          cookbook = cookbookRepository.save(cookbook);
  
          cookbook.addRecipe(recipe, "integrationtest", new Date());
          cookbook = cookbookRepository.save(cookbook);
  
          cookbook.removeRecipe(recipe);
          cookbook = cookbookRepository.save(cookbook);
          assertThat(cookbookRecipeRepository.findAll().size(), is(0));
      }
  
  }
  </code></pre>
  
  <p>The assertion fails. I don't understand why.</p>
  
  <pre><code>java.lang.AssertionError: 
  Expected: is &lt;0&gt;
       but: was &lt;1&gt;
  </code></pre>
  
  <p>I expect JPA to generate a DELETE statement, because orphanRemoval is set to true. Instead the CookbookRecipe.recipeId and CookbookRecipe.cookbookId are set to null in the database but they are not removed.</p>