Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEATURE] @JpaAccessors for bidirectional associations #3437

Open
demkom58 opened this issue Jun 18, 2023 · 1 comment
Open

[FEATURE] @JpaAccessors for bidirectional associations #3437

demkom58 opened this issue Jun 18, 2023 · 1 comment

Comments

@demkom58
Copy link

demkom58 commented Jun 18, 2023

Describe the feature

The basic idea is to automate the generation of mutators for bidirectional object associations to eliminate error-prone code that is responsible about synchronization of both sides.

Examples

I haven't tested the code to see if it works, but the idea should be clear.

OneToOne

Source

@JpaAccessors
@Entity
public class A {
     // ... //
    @OneToOne(mappedBy = "a")
    private B b;
}
@JpaAccessors
@Entity
public class B {
     // ... //
    @OneToOne
    @JoinColumn(name = "a_id", nullable = false)
    private A a;
}

Lomboked

@JpaAccessors
@Entity  
public class A {  
    // ... //  
    @OneToOne(mappedBy = "a")  
    private B b;  
      
    @ApiStatus.Internal  
    public synthetic void setB$lombok(B b) {  
        this.b = b;  
    }  
      
    @ApiStatus.Internal  
    public synthetic B getB$lombok() {  
        return this.b;  
    }  
      
    public void setB(B b) {  
        if (b != null) {  
            b.setA(this);  
        } else if (this.b != null) {  
            this.b.setA(null);  
        }  
    }  
}
@JpaAccessors
@Entity  
public class B {  
    // ... //  
    @OneToOne  
    @JoinColumn(name = "a_id", nullable = false)  
    private A a;  
      
    @ApiStatus.Internal  
    public synthetic void setA$lombok(A a) {  
        this.a = a;  
    }  
      
    @ApiStatus.Internal  
    public synthetic A getA$lombok() {  
        return this.a;  
    }  
      
    public void setA(A a) {  
        if (this.a != null && this.a.getB$lombok() != this) {  
            this.a.setB(null);  
        }  
          
        this.setA$lombok(a);  
        a.setB$lombok(this);  
    }  
}

OneToMany

Source

@JpaAccessors
@Entity
public class A {
     // ... //
    @OneToMany(mappedBy = "a")
    private Set<B> bs = new HashSet<>();
}
@JpaAccessors
@Entity
public class B {
     // ... //
    @ManyToOne
    @JoinColumn(name = "a_id", nullable = false)
    private A a;
}

Lomboked

@JpaAccessors
@Entity  
public class A {  
    // ... //  
    @OneToMany(mappedBy = "a")  
    private Set<B> bs = new HashSet<>();  
      
    @UnmodifiableView  
    public Set<B> getBs() {  
        return Collections.unmodifiableSet(bs);  
    }  
      
    @ApiStatus.Internal  
    public synthetic Set<B> getBsModifiable$lombok() {  
        return bs;  
    }  
      
    public boolean addB(B b) {  
        return bs.add(b) | b.setA(this);  
    }  
      
    public boolean removeB(B b) {  
        return bs.remove(b) | b.setA(null);  
    }  
      
    public boolean addAllBs(Set<B> bs) {  
        boolean added = this.bs.addAll(bs);  
        for (B b : bs) added |= b.setA(this);  
        return added;  
    }  
      
    public boolean removeAllBs(Set<B> bs) {  
        boolean removed = this.bs.removeAll(bs);  
        for (B b : bs) removed |= b.setA(null);  
        return removed;  
    }  
}
@JpaAccessors
@Entity  
public class B {  
    // ... //  
    @ManyToOne  
    @JoinColumn(name = "a_id", nullable = false)  
    private A a;  
      
    public boolean setA(A a) {  
        if (this.a.equals(a)) return false;  
        if (this.a != null) this.a.getBsModifiable$lombok().remove(this);  
        if (a != null) a.getBsModifiable$lombok().add(this);  
        this.a = a;  
        return true;  
    }  
      
    public A getA() {  
        return a;  
    }  
}

ManyToMany

Source

@JpaAccessors
@Entity
public class A {
     // ... //
    @ManyToMany
    @JoinTable(name = "a_b", joinColumns = @JoinColumn(name = "a_id"), inverseJoinColumns = @JoinColumn(name = "b_id")
    )
    private Set<@NotNull B> bs = new HashSet<>();
}
@JpaAccessors
@Entity
public class B {
     // ... //
    @ManyToMany(mappedBy = "bs")
    private Set<@NotNull A> as = new HashSet<>();
}

Lomboked

@JpaAccessors
@Entity  
public class A {  
    // ... //  
    @ManyToMany  
    @JoinTable(name = "a_b", joinColumns = @JoinColumn(name = "a_id"), inverseJoinColumns = @JoinColumn(name = "b_id"))  
    private Set<@NotNull B> bs = new HashSet<>();  
      
    public void addB(@NotNull B b) {  
        this.bs.add(b);  
        b.getAsModifiable$lombok().add(this);  
    }  
      
    public void removeB(@NotNull B b) {  
        this.bs.remove(b);  
        b.getAsModifiable$lombok().remove(this);  
    }  
      
    public void addAllBs(@NotNull Collection<B> bs) {  
        this.bs.addAll(bs);  
        bs.forEach(b -> b.getAsModifiable$lombok().add(this));  
    }  
      
    public void removeAllBs(@NotNull Collection<B> bs) {  
        this.bs.removeAll(bs);  
        bs.forEach(b -> b.getAsModifiable$lombok().remove(this));  
    }  
}
@JpaAccessors
@Entity  
public class B {  
    // ... //  
    @ManyToMany(mappedBy = "bs")  
    private Set<@NotNull A> as = new HashSet<>();  
      
    @UnmodifiableView  
    public Set<A> getAs() {  
        return Collections.unmodifiableSet(as);  
    }  
    
    @ApiStatus.Internal  
    public synthetic Set<A> getAsModifiable$lombok() {  
        return as;  
    }  
      
    public void addA(@NotNull A a) {  
        a.addB(this);  
    }  
      
    public void removeA(@NotNull A a) {  
        a.removeB(this);  
    }  
      
    public void addAllAs(@NotNull Collection<A> as) {  
        as.forEach(this::addA);  
    }  
      
    public void removeAllAs(@NotNull Collection<A> as) {  
        as.forEach(this::removeA);  
    }  
}

Describe the target audience

In almost any application that uses JPA are also used bidirectional associations, to synchronize both sides a lot of code should be written and maintained, this is the place where Lombok can help much and reduce count of error-prone code.

In the examples I have used Jetbrains Annotations for @ApiStatus.Internal and @UnmodifiableView. Also JPA annotations, which are located in jakarta.persistence.

Additional context

Reference: https://vladmihalcea.com/jpa-hibernate-synchronize-bidirectional-entity-associations/

@Rawi01
Copy link
Collaborator

Rawi01 commented Apr 28, 2024

In general I like the idea. It should be easy to implement and might reduce a bunch of boilerplate. Something similar was also requested in #2364.

Feedback:

  • I don't like is the generated code, especially the additional synthetic methods. Do you think it is possible to handle all cases without these methods?
  • External dependencies (JetBrains Annotations) should be avoided
  • The generated code should be compatible with Java 6 (no lambdas)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants