0

In my application, I have a tender entity.

    @Entity
    @Table(name = "tender")
    public class Tender {

        @Id
        @Column(name = "tender_id", unique = true, nullable = false)
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;

        @Column(name = "name")
        private String name;

        @Column(name = "description")
        private String description;

        @OneToMany(mappedBy="tender", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
        @JsonIgnoreProperties("tender")
        private List<TenderAddress> addresses;

        // constructors and getters and setters
        // I am showing the setter for Address since it is customized for my requirement

        public void setAddresses(List<TenderAddress> addresses) {
          this.addresses = addresses;
          for (TenderAddress tenderAddress : addresses) {
            tenderAddress.setTender(this);
          }
        }

    }

My TenderAddress entity looks like this.

@Entity
@Table(name = "tender_address")
public class TenderAddress {

    @Id
    @Column(name = "id", unique = true, nullable = false)
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name="tender_id")
    @JsonIgnoreProperties("addresses")
    private Tender tender;

    @Column(name = "address", nullable = false)
    private String address;

    // constructors
    //getters and setters
}

Everything worked fine until I started testing the update operation.

This is my update method.

@Transactional
    public Tender updateTender(Tender tender) {
        Tender t = entityManager.find(Tender.class, tender.getId());

        t.setName(tender.getName());
        t.setDescription(tender.getDescription());

        t.setAddresses(tender.getAddresses());

        entityManager.merge(t);
        entityManager.flush();
        return findById(t.getId());
    }

name and description are updated fine. What I thought how the addresses would get updated is, it will edit the current record in the relevant database table. But what happened was, it inserted a new record into the table with out editing the already existing record. In fact, I thought JPA looks after that it self.

So, my question now is, do I have to merge the address table explicitly when merging the tender table? If so, how it should be done?

vigamage
  • 1,975
  • 7
  • 48
  • 74
  • When you call `updateTender`, where do the entries in the `tender.addresses` list come from? Do they have their original ids set? If not, JPA has no way of knowing they should be treated as existing entities – crizzis Feb 12 '17 at 18:13
  • @crizzis those addresses are set at the controller class. This is in DAO. I do not set ids for address entities. But the tender id for them is set using the setter method when i call `t.setAddresses(tender.getAddresses)` . Should I set ids for the Address entities ? – vigamage Feb 13 '17 at 04:28
  • You should simply never lose track of the ids of existing addresses, even when you're presenting them on the view in the detached state. I don't know what specific view technology you use, but if its a web app, you could for example keep their ids in hidden input fields. Also, I'd strongly recommend adding `orphanRemoval=true` to the `@OneToMany` annotation – crizzis Feb 13 '17 at 08:32
  • okay, thanks.. so what I should do is follow a method which has been proposed in the answer below by @naros. ? – vigamage Feb 13 '17 at 09:33
  • Not necessarily. As i said in the previous comment, you need to make sure that when your controller receives the `Tender` back from the view for update, all `TenderAddress` entries that existed in its `address` list at the time you presented them on your view **still** have their original id set (along with all the other properties). `EntityManager.merge` should be able to handle the rest, provided that you add `orphanRemoval = true`. – crizzis Feb 13 '17 at 10:20
  • BTW you could (and probably should) replace the entire body of `updateTender` with `return entityManager.merge(tender);`. `EntityManager.merge` does for you exactly what you're doing manually in `updateTender` now. The current call to `EntityManager.merge` in your code has no effect, you are already editing a managed entity. – crizzis Feb 13 '17 at 10:30
  • make sense. I will give it a try and will let you know the results – vigamage Feb 13 '17 at 12:58
  • Thanks @crizzis. It is working perfectly. – vigamage Feb 15 '17 at 05:32

1 Answers1

1

One question to ask is whether anything else will ever have an association to a TenderAddress?

If the answer to your question is yes, then you'll have to do an iterative update of the list based on some natural key and not the database identifier. This is because your incoming list could be a combination of new and updated entries and should be merged in that particular way.

  • Iterate the new list
    • For any new list item that isn't found in the old list, add it to the old list.
    • For any new list item that is found in the old list, update its attributes.
  • Iterate the old list
    • For any old list item that isn't found in the new list, remove it.

If the answer to your question is no, then you could rework the annotations of the mapping and accomplish what you thought would work. Rather than treat TenderAddress as an entity type, lets change it to be considered an @Embeddable and place it inside an @ElementCollection as shown below:

@Entity
public class Tender {
  @ElementCollection
  private Set<TenderAddress> addresses;
}

@Embeddable
public class TenderAddress {
}

In this case, you can now safely replace the entire collection of TenderAddress and Hibernate will basically perform a bulk delete from the collection table based on the Tender primary key and then will reinsert all the new rows from the new list, which will work with the code which you've presented in your question.

Naros
  • 19,928
  • 3
  • 41
  • 71
  • thanks for the answer. The answer for your question is NO. It does not have any associations with any other entities. However, instead of going for the way you have mentioned with `@Embeddable`, what would be an alternative way to overcome my issue while keeping the `TenderAddress` entity as it is? Is it the first way you have suggested ? – vigamage Feb 13 '17 at 04:33
  • Yes, if you want to keep it as an `@Entity`, you'll have to manage the merge of the two lists yourself. – Naros Feb 13 '17 at 06:31