projectlombok / lombok

Very spicy additions to the Java programming language.
https://projectlombok.org/
Other
12.86k stars 2.38k forks source link

[FEATURE] @JpaAccessors for bidirectional associations #3437

Open demkom58 opened 1 year ago

demkom58 commented 1 year ago

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 commented 5 months ago

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:

rukins commented 5 months ago

thank you, @Rawi01 ! i have started to implement it, so the pull request will be created pretty soon, depending on my free time

rukins commented 5 months ago

also i created the post on the Forum about this feature - https://groups.google.com/g/project-lombok/c/Z8LQEatEa-0