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 #3437] @JpaAssociationSync annotation for bidirectional entity associations #3692

Open
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

rukins
Copy link

@rukins rukins commented Jun 17, 2024

Overview

This pull request introduces a new Lombok annotation, @JpaAssociationSync, designed to facilitate the synchronization of JPA associations in the context of bidirectional relationships. The annotation can be applied at both the type and field level, generating helper methods to ensure that both sides of a bidirectional relationship remain consistent.
The methods is generated according to Vlad Mihalcea article

The idea is taken from #3437 and #2364.

Key Features

  • @JpaAssociationSync Annotation: Provides generation of association sync methods.
  • Nested @JpaAssociationSync.Extra Annotation: Provides additional configuration options.

Parameters

@JpaAssociationSync

@JpaAssociationSync.Extra

  • paramName: Specifies the name of the parameter for the JPA association sync methods. Defaults to the name of the field's type.
  • inverseSideFieldName: Specifies the name of the field on the inverse side of the relationship. Necessary only on the owning side of bidirectional @OneToOne or @ManyToMany associations.

Example

Source

Post.java

package com.example.demo.model;  

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.JpaAssociationSync;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@JpaAssociationSync
@Getter
@Setter
@Entity(name = "Post")
public class Post {
    @Id
    @GeneratedValue
    private Long id;
  
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<PostComment> comments = new ArrayList<>();

    @JpaAssociationSync.Extra(paramName = "details")
    @OneToOne(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private PostDetails details;

    @JpaAssociationSync.Extra(inverseSideFieldName = "posts")
    @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id"))
    private Set<Tag> tags = new HashSet<>();
}

PostComment.java

package com.example.demo.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity(name = "PostComment")
public class PostComment {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;
}

PostDetails.java

package com.example.demo.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity(name = "PostDetails")
public class PostDetails {
    @Id
    private Long id;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private Post post;
}

Tag.java

package com.example.demo.model;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;

import java.util.HashSet;
import java.util.Set;

@Getter
@Setter
@Entity(name = "Tag")
public class Tag {
    @Id
    @GeneratedValue
    private Long id;

    @ManyToMany(mappedBy = "tags")
    private Set<Post> posts = new HashSet<>();
}

Delomboked

Post.java

package com.example.demo.model;  

import jakarta.persistence.*;
import lombok.Generated;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

@Entity(name = "Post")
public class Post {
    @Id
    @GeneratedValue
    private Long id;
  
    @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<PostComment> comments = new ArrayList<>();

    @OneToOne(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    private PostDetails details;

    @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    @JoinTable(name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id"))
    private Set<Tag> tags = new HashSet<>();

    @Generated
    public void addPostComment(PostComment postComment) {
        this.comments.add(postComment);
        postComment.setPost(this);  
    }
    
    @Generated
    public void removePostComment(PostComment postComment) {
        this.comments.remove(postComment);
        postComment.setPost(null);
    }
    
    @Generated
    public void updateDetails(PostDetails details) {  
        if (details == null) {
	          if (this.details != null) {
	              this.details.setPost(null);
	          }
        } else {
	          details.setPost(this);
        }
        
        this.details = details;
    }
    
    @Generated
    public void addTag(Tag tag) {
        this.tags.add(tag);
        tag.getPosts().add(this);
    }
    
    @Generated
    public void removeTag(Tag tag) {
        this.tags.remove(tag);
        tag.getPosts().remove(this);
    }
    
    // generated getters and setters
}

PostComment.java, PostDetails.java and Tag.java remain as it was.

Notes

  • Setters for @OneToMany and @OneToOne and getters for @ManyToMany are necessary on the other side of association. Perhaps later it would be nice to implement an option with field access.
  • @OneToMany, @OneToOne and @ManyToMany annotations can be used on methods, so it will be implemented later.
  • If the @JpaAssociationSync handler can't find the mappedBy parameter in the association annotation or the inverseSideFieldName parameter in the@JpaAssociationSync.Extra annotation on field, nothing will be generated.
  • To make it work with ECJ, you should add the jakarta.persistence-api dependency to your maven-compiler-plugin plugin (in case of Maven) as described here.
  • I am ready to bring the implementation to the ideal, so any suggestions or corrections are highly welcomed.

@rzwitserloot
Copy link
Collaborator

We're going to look at this a bit later; one issue is that both roel and I thoroughly dislike JPA (not in the sense 'it's a bad tool', more in the sense: "The community (vastly) overuse it, and it tends to be 'missold' as a way to do SQL in java. It is not. It is a way to do object persistence in a crappy way in java, quite nice if you have no plans to ever actually treat the resulting persisted data as a database, let alone optimize the SQL behind it, because if thats on the horizon, just use JDBI, JOOQ, or some other abstraction that doesn't effectively yell out "SQL scares me!"'.

Hey, we know we're opinionated and these opinions can be a bit out there. JPA is used by a lot of people, and features that help using it have a place in Project Lombok. The point is more: We might need some extra help (and certainly some more time) to fully evaluate this PR.

Good job on including examples. We've had a quick look and we don't get it. Which, no doubt, is due to our lack of intricate understanding of the vagaries of JPA. We're going to try to find some time to evaluate this PR properly.

Thanks for the contribution, and apologies that we need some time to evaluate it :)

@rukins
Copy link
Author

rukins commented Jun 28, 2024

okay, no problem

@rukins
Copy link
Author

rukins commented Jun 28, 2024

it would be really great if @vladmihalcea could help us :)

@rzwitserloot
Copy link
Collaborator

@rukins any idea vlad is gonna chime in? I'm guessing no :(

@ZIRAKrezovic
Copy link

I'd like to add that a tooling for this problem already exists in Hibernate world

https://docs.jboss.org/hibernate/orm/5.6/topical/html_single/bytecode/BytecodeEnhancement.html

I'd be interested how this feature behaves when used together with the plugin linked above or if any effort should be made to prevent usage of both if there are any nasty side effects.

@rukins
Copy link
Author

rukins commented Sep 25, 2024

@ZIRAKrezovic thank you for the note. I will test it and be back with the result

@danielferber
Copy link

That would be an awesome feature! I understand that not everyone feels comfortable using JPA for SQL in Java. But this is the widespread reality in the Java EE/Spring software industry, and it has been a hassle to consistently handle one-to-one, one-to-many, and many-to-many relationships in a large project. Such annotations would add great value in making JPA at least a little better. It would prevent silly bugs and speed up development.

And it would be really nice to have a Hibernate (non JPA) variant.

@rukins
Copy link
Author

rukins commented Nov 9, 2024

@ZIRAKrezovic They seem to work fine with each other. But actually I have never used Bytecode Enhancement from Hibernate before, so I can't tell how it works in real world projects and how it affect performance and so on.

The only disadvantage I noticed is that Hibernate BE is embedded in setters and getters, and therefore bidirectional entity synchronization only works when creating new lists and setting it, not adding or removing elements.

Also there are the answer on Stackoverflow and the article from Vlad, where he said

The enableAssociationManagement cannot intercept changes happening in a collection of child entities, and, for this reason, you are better off using the addChild and removeChild methods.

(But it was 3 years ago)

It would be great if someone who has worked with Hibernate BE could help us with this issue.

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

Successfully merging this pull request may close these issues.

4 participants