3

Schema definition

CREATE TABLE person (
  id bigint(20) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (id)
) ENGINE=InnoDB

CREATE TABLE address (
    person_id bigint(20) not null,
    postcode varchar(255) not null,
    state int(11),
    constraint `PRIMARY` primary key (address_id, postcode),
    constraint FK_dq8j32idfdnwny42fiovenqwo foreign key (person_id) references person (id)
) ENGINE=InnoDB

Ie a person should be able to have multiple addresses as long as the postcode is different.

Classes

@Entity
@Getter
@Setter
class Person implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name="id", unique = true, nullable = false)
    private Long id;

    @OneToMany(cascade= CascadeType.ALL)
    private Set<Address> Addresses = new HashSet<>();

    protected Person(){}
}

@Entity
@Getter
@Setter
class Address implements Serializable {
    @EmbeddedId
    private AddressId addressId;
    private State state;
    protected Address(){}
}

@Embeddable
@Getter
@Setter
public class AddressId implements Serializable {
    @Column(name = "person_id")
    private Long personId;
    @Column(name = "postcode")
    private String postcode;
}

This works in the sense that it allows adding more than one address to the same person as long as the two addresses have different postcodes.

Person person = personRepo.findOne(personId);

AddressId addressId = new AddressId();
addressId.setPersonId(personId);
addressId.setPostcode("4000");
Address address = new Address();
address.setAddressId(addressId);
address.setState(State.QLD);
person.getAddresses().add(address);

AddressId addressId2 = new AddressId();
addressId2.setPersonId(personId);
addressId2.setPostcode("4001");
Address address2 = new Address();
address2.setAddressId(addressId2);
address.setState(State.VIC);
person.getAddresses().add(address2);

person = personRepo.save(person);

But when trying to update one of them (eg changing the state)

Person person = personRepo.findOne(personId);

AddressId addressId = new AddressId();
addressId.setPersonId(personId);
addressId.setPostcode("4000"); //person already has an address with this postcode
Address address = new Address();
address.setAddressId(addressId);
address.setState(State.TAS); //but I want to change the state from QLD to TAS
person.getAddresses().add(address);
person = personRepo.save(person);

the below is generated. I think the below basically means "Hey, you're trying to add an address with a person_id and postcode that already exists, I don't care if the state is different, you can't do that". How can it be made to work?

Caused by: java.lang.IllegalStateException: Multiple representations of the same entity [Address#AddressId@5047ce78] are being merged. Managed: [Address@5c220478]; Detached: [Address@79804699] at org.hibernate.event.internal.EntityCopyNotAllowedObserver.entityCopyDetected(EntityCopyNotAllowedObserver.java:51) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.event.internal.MergeContext.put(MergeContext.java:262) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.event.internal.DefaultMergeEventListener.entityIsPersistent(DefaultMergeEventListener.java:216) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:192) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:886) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:868) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.spi.CascadingActions$6.cascade(CascadingActions.java:277) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:350) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:293) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascadeCollectionElements(Cascade.java:379) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascadeCollection(Cascade.java:319) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:296) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:161) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:118) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.event.internal.DefaultMergeEventListener.cascadeOnMerge(DefaultMergeEventListener.java:474) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.event.internal.DefaultMergeEventListener.entityIsPersistent(DefaultMergeEventListener.java:218) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:192) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.event.internal.DefaultMergeEventListener.onMerge(DefaultMergeEventListener.java:85) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.internal.SessionImpl.fireMerge(SessionImpl.java:876) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:858) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.internal.SessionImpl.merge(SessionImpl.java:863) ~[hibernate-core-4.3.11.Final.jar:4.3.11.Final] at org.hibernate.jpa.spi.AbstractEntityManagerImpl.merge(AbstractEntityManagerImpl.java:1196) ~[hibernate-entitymanager-4.3.11.Final.jar:4.3.11.Final] at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_102] at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_102] at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_102] at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_102]

However, if I do

Person person = personRepo.findOne(personId);
new ArrayList<>(person.getAddresses()).get(0).setState(State.QLD); //change state of existing address row
person = personRepo.save(person);

I don't get the exception. I think Hibernate detects that in that case, an existing row is being updated, whereas if trying to ADD another Address with the same personId and postcode as one that already exists, it tries to do an INSERT and fails. Is there no way to make Hibernate figure out to do an update in date case as well?

fred
  • 1,812
  • 3
  • 37
  • 57
  • 1
    Where is the code that adds the addresses to the set? Did you implement equals and hashCode properly? – aviad Oct 17 '16 at 05:50
  • @aviad added code that generates the exception. As for equals and hashcode, they have not been overridden for any of the classes, and I'm not sure what constitutes "properly" since there are so many strongly differing opinions on that. – fred Oct 17 '16 at 05:59

3 Answers3

3

Address can be made an @Embeddable rather than an @Entity. In this scenario an Address has no persistent identity of its own which reflects your database: an address cannot exist without an associated person.

The simplified mappings then look like:

@Entity
@Getter
@Setter
class Person implements Serializable {
    @Id
    @GeneratedValue(strategy=GenerationType.AUTO)
    @Column(name="id", unique = true, nullable = false)
    private Long id;

    @ElementCollection
    @CollectionTable(name = "address", joinColumns = @JoinColumn(name = "person_id"))
    private Set<Address> Addresses = new HashSet<>();

    protected Person(){}
}

@Embeddable
@Getter
@Setter
class Address implements Serializable {

    private State state;
    private String postcode;

    protected Address(){}

    // override equals and hascode based on post code only
    // this will give you the desired behaviour
}

If you override equals() and hashcode() based on post code then everything should work as expected.

https://en.wikibooks.org/wiki/Java_Persistence/ElementCollection

See also:

Hibernate - @ElementCollection - Strange delete/insert behavior

Community
  • 1
  • 1
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • This is promising but when I add to a persons addresses a second address with has the same personId and postcode as an already added one, but eg a different state, nothing happens. (because the equals method only looks at the postcode, so the new state isn't picked up) – fred Oct 17 '16 at 22:11
  • 1
    Ok. Got it now. You want to add an item to a collection and the jvm will guess if it's an update or a new item. Is this correct? You can't have it both ways. – Alan Hay Oct 17 '16 at 22:22
  • Of course the JVM can't do magic. Your answer is much appreciated. – fred Oct 17 '16 at 22:24
  • If Person has an addAddress method which first removes the argument address in the persons addresses set, and then adds it, it works fine. – fred Oct 18 '16 at 03:14
2

You have to go through each of the person's addresses to check if it exists and update if it does, create if it doesn't.

Person person = personRepo.findOne(personId);

AddressId addressId = new AddressId();
addressId.setPersonId(personId);
addressId.setPostcode("4000"); //person already has an address with this postcode
Address address = null;
for (Address a : person.getAddresses()) {
    if (a.getAddressId().equals(addressId)) {
        address = a;
        break;
    }
}
if (address == null) {
    address = new Address();
    address.setAddressId(addressId);
    address.setState(State.TAS); //but I want to change the state from QLD to TAS
    person.getAddresses().add(address);
}
else {
    address.setState(State.TAS);
}
person = personRepo.save(person);

This code sample assumes you've overridden equals on the Embeddable composite key, as you're required.

coladict
  • 4,799
  • 1
  • 16
  • 27
  • I see how that will work, but is there no more idiomatic, elegant way of achieving the end goal? (including possibly changing the schema definition etc?) – fred Oct 17 '16 at 07:37
  • 1
    If you're using Postgres you can create a native query that uses the `INSERT ... ON CONFLICT UPDATE ... WHERE` syntax, but that's about it. There are other syntaxes for other databases listed here https://wiki.postgresql.org/wiki/UPSERT – coladict Oct 17 '16 at 07:39
  • I wish marking more than one answer as accepted was possible. – fred Oct 18 '16 at 03:15
1

You are creating a new Address object with the same AddressId key.

Since equals and hashCode are not overriden, the new duplicated address is inserted to the set instead of replacing the old Address object.

This creates a duplication of Addresses with the same AddressId.

So, either implement equals in Address in order for the Set to replace duplication, or just extract from the Set the proper address and change its state directly.

The equals and hashCode of Address should call the equals and hashCode of AddressId. The addressId equald and hashCode should check the personId and postCode.

Using modern IDEAs such as intelliJ or eclipse will allow you to auto generate those methods with respect to postCode and personId

aviad
  • 1,553
  • 12
  • 15
  • If I implement the equals and hashcode as described, when adding the "edited" Address to the Person (ie an Address with the same personId and postcode as an existing one, but with a different state), the size of the set doesn't increase (good) but the entries in the set aren't changed. (not the desired behaviour) – fred Oct 17 '16 at 06:25
  • 1
    do a remove before – aviad Oct 17 '16 at 06:29
  • I see how that will work, but is there no more idiomatic, elegant way of achieving the end goal? (including possibly changing the schema definition etc?) – fred Oct 17 '16 at 07:37
  • 1
    those duplications are entered on the java level tier, i dont see how manipulating the db would be more elegant. – aviad Oct 17 '16 at 07:44
  • I wish marking more than one answer as accepted was possible. – fred Oct 18 '16 at 03:15