eclipse-ee4j / eclipselink

Eclipselink project
https://eclipse.dev/eclipselink/
Other
199 stars 169 forks source link

Jakarta Data does not work #2209

Open hantsy opened 3 months ago

hantsy commented 3 months ago

Describe the bug I tried to port my Jakarta Data example project from Hibernate to Jakarta EE/EclipseLink. But it failed when testing with Glassfish v8 and EclipseLink.

To Reproduce Steps/resources to reproduce the behavior:

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "posts")
public class Post implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    UUID id;

    @Basic(optional = false)
    String title;

    @Basic(optional = false)
    String content;

    @Enumerated
    @Builder.Default
    Status status = Status.DRAFT;

    @OneToMany(mappedBy = "post", targetEntity = Comment.class, cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
    List<Comment> comments;

    LocalDateTime createdAt;
    LocalDateTime lastModifiedAt;

    @PrePersist
    public void prePersist() {
        createdAt = LocalDateTime.now();
        lastModifiedAt = createdAt;
    }

    @PreUpdate
    public void preUpdate() {
        lastModifiedAt = LocalDateTime.now();
    }

    @Override
    public String toString() {
        return "Post{" +
                "id=" + id +
                ", title='" + title + '\'' +
                ", content='" + content + '\'' +
                ", createdAt=" + createdAt +
                ", lastModifiedAt=" + lastModifiedAt +
                '}';
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Entity
@Table(name = "comments")
public class Comment implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    UUID id;

    @Basic(optional = false)
    private String content;

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

    private LocalDateTime createdAt;

    @PrePersist
    public void prePersist() {
        this.createdAt = LocalDateTime.now();
    }
}

And I created a free style @Repository interface like this.

@Repository
public interface Blogger {
//
//    StatelessSession session();

    @Query("""
            SELECT p.id, p.title, size(c) AS summary FROM Post p LEFT JOIN p.comments c
            WHERE p.title LIKE :title
                OR p.content LIKE :title
                OR c.content LIKE :title
            GROUP BY p
            ORDER BY p.createdAt DESC
            """)
    Page<PostSummary> allPosts(@Param("title") String title, PageRequest page);

    @Find
    @OrderBy("createdAt")
    List<Post> byStatus(Status status, Order<Post> order, Limit limit);

    @Find
    Optional<Post> byId(UUID id);

    @Insert
    Post insert(Post post);

    @Insert
    Comment insert(Comment comment);

    @Update
    Post update(Post post);

    @Delete
    void delete(Post post);

    // see: https://hibernate.zulipchat.com/#narrow/stream/132096-hibernate-user/topic/Jakarta.20Data.20cascade.20does.20not.20work.20in.20custom.20deletion.20Query/near/441874793
    @Query("delete from Post")
    @Transactional
    long deleteAllPosts();

//    default List<Comment> getCommentsOfPost(UUID postId) {
//        var post = this.byId(postId).orElseThrow(() -> new PostNotFoundException(postId));
//        session().fetch(post.getComments());
//        return post.getComments();
//    }
}

I have written a basic test to verify it, failed.

@ExtendWith(ArquillianExtension.class)
public class BloggerTest {

    private final static Logger LOGGER = Logger.getLogger(BloggerTest.class.getName());

    @Deployment
    public static WebArchive createDeployment() {
        WebArchive war = ShrinkWrap.create(WebArchive.class)
                .addPackage(Post.class.getPackage())
                .addAsResource("test-persistence.xml", "META-INF/persistence.xml")
                .addAsWebInfResource(EmptyAsset.INSTANCE, "beans.xml");
        LOGGER.log(Level.INFO, war.toString(true));
        return war;
    }

    @PersistenceContext
    private EntityManager em;

    @Inject
    private Blogger blogger;

    @Inject
    UserTransaction ux;

    private void startTx() throws Exception {
        ux.begin();
        em.joinTransaction();
    }

    @AfterEach
    public void after() throws Exception {
        endTx();
    }

    private void endTx() throws Exception {
        LOGGER.log(Level.INFO, "Transaction status: {0}", ux.getStatus());
        try {
            if (ux.getStatus() == Status.STATUS_ACTIVE) {
                ux.commit();
            }
        } catch (Exception e) {
            ux.rollback();
        }
    }

    @Test
    public void testBlogger() throws Exception {
        var post = new Post();
        post.setTitle("My Post");
        post.setContent("My Post Content");
        startTx();
        blogger.insert(post);
        endTx();
        assertNotNull(post.getId());

        var foundPost = blogger.byId(post.getId());
        assertTrue(foundPost.isPresent());
        Post savedPost = foundPost.get();
        assertEquals(post.getTitle(), savedPost.getTitle());
        assertEquals(post.getContent(), savedPost.getContent());

        var foundByStatus = blogger.byStatus(
                com.example.Status.PUBLISHED,
                Order.by(Sort.desc("createdAt")),
                Limit.of(10)
        );
        assertEquals(1, foundByStatus.size());
        assertEquals(post.getId(), foundByStatus.getFirst().getId());

        var allPosts = blogger.allPosts("%My%", PageRequest.ofPage(1, 10, true));
        assertEquals(1, allPosts.totalElements());
        assertEquals(post.getId(), allPosts.content().getFirst().id());

        savedPost.setTitle("New Title");
        startTx();
        blogger.update(savedPost);
        endTx();

        var updatedPost = blogger.byId(post.getId()).get();
        assertEquals("New Title", updatedPost.getTitle());

//        var comment = new Comment();
//        comment.setContent("My Comment");
//        comment.setPost(post);
//        startTx();
//        blogger.insert(comment);
//        endTx();
//        assertNotNull(comment.getId());
    }

}

It failed with the following exception in console.

 BloggerTest.testBlogger » ArquillianProxy 
org.jboss.arquillian.junit5.IdentifiedTestException :
 null [Proxied because : Original exception caused: 
class java.lang.ClassNotFoundException: 
org.jboss.weld.exceptions.IllegalArgumentException]

The complete Glassfish server logs, check here.

hantsy commented 3 months ago

I found this message in slack Jakarta EE/data channel. https://eclipsefoundationhq.slack.com/archives/C040AJ787PU/p1715951162858049?thread_ts=1715902886.424969&cid=C040AJ787PU

That means EclipseLink will not provide the Jakarta Data implementations?

OndroMih commented 2 months ago

Correct, @hantsy. I'm not a commiter on Eclipselink but I didn't see any signs that the Eclipselink project is going to implement Jakarta Data. We currently don't have a clear solution for GlassFish. I'm experimenting with integrating Eclipselink to the JNoSQL implementation of Jakarta Data, I have a good progress, but it's still not clear if it's a feasible way. For 2 main reasons - JNoSQL primarily focuses on Jakarta NoSQL, and even the NoSQL implementation of Jakarta Data is not complete yet. If we don't hit any showstopper with integrating Eclipselink to the JNoSQL solution, and if the JNoSQL project implements all Jakarta Data functionality, then we'll likely have a solution for GlassFish and even any app server. So far, my integration code does not depend on any Eclipselink-specific features and should support Hibernate or any other JPA implementation.

hantsy commented 2 months ago

So far, my integration code does not depend on any Eclipselink-specific features and should support Hibernate or any other JPA implementation.

This is great. I hope it will be integrated into the next Glassfish milestone.

And your work will be a new subproject under Eclipse EE4j?

hantsy commented 2 months ago

From other messages from Open Liberty, they have done the Jakarta Data implementation that is based on EclipseLink, why not port it back to EclipseLink project?

OndroMih commented 2 months ago

The OpenLiberty team say that their implementation is tightly coupled to OprnLiberty internals and thus it’s not easy to extract it.

My implementation should be in the JNoSQL project, which is already an Eclipse foundation project. I need some more time to prepare a PR to contribute what I have.