CodeOnce
CodeOnce

Reputation: 79

Spring Data JPA - Multi Bidirectional @ManyToOne Propagation Data

Diagram

    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    @ToString 
    public class Lawyer extends ID{
        @EqualsAndHashCode.Exclude
        @ToString.Exclude
        @JsonIgnore
        @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "lawyer")
        private Set<Appointment> appointments = new HashSet<>();

public void addAppointment(Client client, LocalDateTime data) {
            Appointment app = new Appointment (client,this,data);
            this.consultas.add(app);
            app.getClient().getAppointments().add(app);
        }

       }

        @Entity
        @Getter
        @Setter
        @NoArgsConstructor
        @ToString
        public class Appointment extends ID{

            @EqualsAndHashCode.Exclude
            @ToString.Exclude
            @ManyToOne
            private Client client;

            @EqualsAndHashCode.Exclude
            @ToString.Exclude
            @ManyToOne
            private Lawyer lawyer;
        }



        @Entity
        @Getter
        @Setter
        @NoArgsConstructor
        @ToString
        public class Client extends ID{

            @EqualsAndHashCode.Exclude
            @ToString.Exclude
            @JsonIgnore
            @OneToMany
            private Set<Appointment> appointments = new HashSet<>();
        }

        @MappedSuperclass
        @Getter
        @Setter
        @NoArgsConstructor
        public class ID{
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            private Long id;
        }

BootStrap Class

@Component
public class Bootstrap implements ApplicationListener<ContextRefreshedEvent> {
    private LaywerRepoI LaywerService;

    public Bootstrap(LaywerRepoI LaywerService) {
        this.LaywerService = LaywerService;
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        Client c1 = new Client("Lukz", LocalDate.of(1971, 11, 26));
        Client c2 = new Client("Adrian", LocalDate.of(1956, 01, 28));
        Client c3 = new Client("Danny", LocalDate.of(1936, 1, 11));

        Laywer l1 = new Laywer("Morgan", LocalDate.of(1941, 1, 1));
        Laywer l2 = new Laywer("Ana", LocalDate.of(1931, 10, 1));

        l1.addAppointment(c1,LocalDateTime.of(2018, 11, 22,18, 25));
        l1.addAppointment(c1,LocalDateTime.of(2018, 11, 22, 10, 15));

        LawyerService.save(l1);
        LawyerService.save(l2);
    }
}

When I'm making a new Appointment on my class Lawyer I'm trying to propagate de data from Lawyer to Client, but I can only reach it to Appointment. From Appointment to Client, I can't propagate it.... I get this error:

Caused by: java.lang.IllegalStateException: org.hibernate.TransientPropertyValueException: object references an unsaved transient instance - save the transient instance before flushing

How do I propagate from Appointment to Client ? I already read some articles about this types of cases, but still I havent understood them.

Upvotes: 0

Views: 578

Answers (2)

K.Nicholas
K.Nicholas

Reputation: 11551

The spring-data-jpa is a layer on top of JPA. Each entity has its own repository and you have to deal with that.

@Entity
public class Lawyer  {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "client", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Appointment> appointments;

@Entity
public class Client {
    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "lawyer", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Appointment> appointments;

@Entity
public class Appointment {
    @EmbeddedId
    private AppointmentId id = new AppointmentId();

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("lawyerId")
    private Lawyer lawyer;

    @ManyToOne(fetch = FetchType.LAZY)
    @MapsId("clientId")
    private Client client;

    public Appointment() {}
    public Appointment(Lawyer lawyer, Client client) {
        this.lawyer = lawyer;
        this.client = client;
    }

@SuppressWarnings("serial")
@Embeddable
public class AppointmentId implements Serializable {
    private Long lawyerId;
    private Long clientId;

And to use it, as show above:

@Transactional
private void update() {
    System.out.println("Step 1");
    Client client1 = new Client();
    Lawyer lawyer1 = new Lawyer();
    Appointment apt1 = new Appointment(lawyer1, client1);
    clientRepo.save(client1);
    lawyerRepo.save(lawyer1);
    appointmentRepo.save(apt1);

    System.out.println("Step 2");
    Client client2 = new Client();
    Lawyer lawyer2 = new Lawyer();
    Appointment apt2 = new Appointment(lawyer2, client2);
    lawyerRepo.save(lawyer2);
    clientRepo.save(client2);
    appointmentRepo.save(apt2);

    System.out.println("Step 3");
    client2 = clientRepo.getOneWithLawyers(2L);
    client2.getAppointments().add(new Appointment(lawyer1, client2));
    clientRepo.save(client2);

    System.out.println("Step 4 -- better");
    Appointment apt3 = new Appointment(lawyer2, client1);
    appointmentRepo.save(apt3);
}

Note I don't explicitly set the AppointmentId id's. These are handled by the persistence layer (hibernate in this case).

Note also that you can update Appointment entries either explicity with its own repo or by adding and removing them from the list since CascadeType.ALL is set, as shown. The problem with using the CascadeType.ALL for spring-data-jpa is that even though you prefetch the join table entities spring-data-jpa will do it again anyway. Trying to update the relationship through the CascadeType.ALL for new entities is problematic.

Without the CascadeType neither the lawyers or Clients lists (which should be Sets) are the owners of the relationship so adding to them wouldn't accomplish anything in terms of persistence and would be for query results only.

When reading the Appointment relationships you need to specifically fetch them since you don't have FetchType.EAGER. The problem with FetchType.EAGER is the overhead if you don't want the joins and also if you put it on both Client and Lawyer then you will create a recursive fetch that gets all Clients and lawyers for any query.

@Query("select c from Client c left outer join fetch c.lawyers ls left outer join fetch ls.lawyer where t.id = :id")
Client getOneWithLawyers(@Param("id") Long id);

Finally, always check the logs. Creating an association requires spring-data-jpa (and I think JPA) to read the existing table to see if the relationship is new or updated. This happens whether you create and save a Appointment yourself or even if you prefetched the list. JPA has a separate merge and I think you can use that more efficiently.

create table appointment (client_id bigint not null, lawyer_id bigint not null, primary key (client_id, lawyer_id))
create table client (id bigint generated by default as identity, primary key (id))
alter table appointment add constraint FK3gbqcfd3mnwwcit63lybpqcf8 foreign key (client_id) references client
create table lawyer (id bigint generated by default as identity, primary key (id))
alter table appointment add constraint FKc8o8ake38y74iqk2jqpc2sfid foreign key (lawyer_id) references lawyer

insert into client (id) values (null)
insert into lawyer (id) values (null)
select appointmen0_.client_id as client_i1_0_0_, appointmen0_.lawyer_id as lawyer_i2_0_0_ from appointment appointmen0_ where appointmen0_.client_id=? and appointmen0_.lawyer_id=?
insert into appointment (client_id, lawyer_id) values (?, ?)

insert into lawyer (id) values (null)
insert into client (id) values (null)
select appointmen0_.client_id as client_i1_0_0_, appointmen0_.lawyer_id as lawyer_i2_0_0_ from appointment appointmen0_ where appointmen0_.client_id=? and appointmen0_.lawyer_id=?
insert into appointment (client_id, lawyer_id) values (?, ?)

select client0_.id as id1_1_0_, appointmen1_.client_id as client_i1_0_1_, appointmen1_.lawyer_id as lawyer_i2_0_1_, lawyer2_.id as id1_2_2_, appointmen1_.lawyer_id as lawyer_i2_0_0__, appointmen1_.client_id as client_i1_0_0__ from client client0_ left outer join appointment appointmen1_ on client0_.id=appointmen1_.lawyer_id left outer join lawyer lawyer2_ on appointmen1_.lawyer_id=lawyer2_.id where client0_.id=?
select client0_.id as id1_1_1_, appointmen1_.lawyer_id as lawyer_i2_0_3_, appointmen1_.client_id as client_i1_0_3_, appointmen1_.client_id as client_i1_0_0_, appointmen1_.lawyer_id as lawyer_i2_0_0_ from client client0_ left outer join appointment appointmen1_ on client0_.id=appointmen1_.lawyer_id where client0_.id=?
select appointmen0_.client_id as client_i1_0_0_, appointmen0_.lawyer_id as lawyer_i2_0_0_ from appointment appointmen0_ where appointmen0_.client_id=? and appointmen0_.lawyer_id=?
insert into appointment (client_id, lawyer_id) values (?, ?)

select appointmen0_.client_id as client_i1_0_0_, appointmen0_.lawyer_id as lawyer_i2_0_0_ from appointment appointmen0_ where appointmen0_.client_id=? and appointmen0_.lawyer_id=?
insert into appointment (client_id, lawyer_id) values (?, ?)

Upvotes: 1

user10639668
user10639668

Reputation:

You save the Lowyer, therefore you need to cascade the Lowyer -> Appointment and the Appointment -> Client relations.

Therefore you have to cascade the relation Appointment -> Client also.

        @Entity
        @Getter
        @Setter
        @NoArgsConstructor
        @ToString
        public class Appointment extends ID{

            @EqualsAndHashCode.Exclude
            @ToString.Exclude
            @ManyToOne(cascade = CascadeType.ALL)
            private Client client;

            @EqualsAndHashCode.Exclude
            @ToString.Exclude
            @ManyToOne
            private Lawyer lawyer;
        }

Upvotes: 1

Related Questions