0

Having unidirectional ManyToMany mapping in an entity called A to entity called B on field called files:

@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = JOIN_TABLE_NAME)
private List<B> files = new ArrayList<>();

, which creates a join table with ids of each entity as composite primary key.

Automatically, when an item is removed from files list and A entity is saved, a row is also removed from join table.

I would like to implement this behavior:

Whenever a row is removed from this join table, check if there are no more rows with B-id as removed row B-id, if so, remove rows from B table matching that B-id.

There might be several ways to implement this. Some of which i have tought, but are unsatisfactory or do not work:

Create join table as separate entity and add a post remove method. However this method is not called:

@Entity
@PrimaryKeyJoinColumns(
    @PrimaryKeyJoinColumn(name = "a_id"),
    @PrimaryKeyJoinColumn(name = "b_id")
)
@Table(name = JOIN_TABLE_NAME)
public class ABjoinTable {
    
    @EmbeddedId
    private ABId abid;
    
    @Embeddable
    public static final class ABId {
    
        @Column(name = "a_id);
        public long aid;
    
        @Column(name = "b_id);
        public long bid;
    }
    
    @PostRemove
    public void removeDanglingFiles() {
            // not called
    }
}

Create a remove trigger on join table, however B entity has PostRemove method already implemented, which in this solution will not be called (trigger is on db level).

Use PostUpdate method on A entity and remove dangling rows manually, however rows are not yet removed from join table in post update context:

@PostLoad
public void createFilesSnapshot() {
    filesSnapshot = new ArrayList<>(this.files);
}

@PostUpdate
public void removeDanglingFiles() {
    // if there are no more references to other Bids, delete that B
    val removed = filesSnapshot.stream()
            .filter(b -> !this.files.contains(b))
            .map(PersistentObject::getId)
            .collect(Collectors.toSet());
    try (val em = JPAutil.getEntityManager()) {
        val tx = em.getTransaction();
        tx.begin();
        for (val rId : removed) {
            val q = em.createQuery("SELECT a" +
                    " FROM A a JOIN a.files b" +
                    " WHERE b.id = :bId", B.class);
            q.setParameter("bId", rId);
            val result = q.getResultList();
            if (result.size() == 0) { // at this point in post load, join table row is not yet removed, so size is >= 1
                em.remove(...);
            }
        }
        tx.commit();
    } catch (final Exception e) {
    }
}

Using EclipseLink jpa provider and MySql db.

1 Answer 1

1

You can solve this by doing the cleanup after the transaction is flushed, i.e. using a @TransactionalEventListener or a SessionEventListener (if using EclipseLink-specific APIs).
Alternatively, you can hook into your service layer logic, which is the most straightforward and JPA-compliant solution.

Option 1) Handle it in your service layer (recommended)

Instead of relying on entity callbacks, handle the cleanup explicitly after saving A:

@Transactional
public void saveA(A a) {
    // Snapshot before save (to know what was removed)
    List<B> oldFiles = repository.findById(a.getId())
                                 .map(A::getFiles)
                                 .orElse(Collections.emptyList());

    repository.save(a); // triggers join table updates

    // Flush to ensure join table rows are removed
    entityManager.flush();

    // Check which Bs were removed
    Set<Long> removedIds = oldFiles.stream()
            .filter(b -> !a.getFiles().contains(b))
            .map(B::getId)
            .collect(Collectors.toSet());

    // Delete Bs no longer referenced
    for (Long bId : removedIds) {
        Long count = entityManager.createQuery(
            "SELECT COUNT(a) FROM A a JOIN a.files b WHERE b.id = :bId", Long.class)
            .setParameter("bId", bId)
            .getSingleResult();

        if (count == 0) {
            B b = entityManager.find(B.class, bId);
            entityManager.remove(b);
        }
    }
}

This approach:

  • Guarantees the join table is already synchronized (flush()).

  • Keeps the logic in your domain/service layer instead of relying on low-level callbacks.

  • Works with any JPA provider (EclipseLink, Hibernate, etc.).


Option 2) Use EclipseLink session events (provider-specific)

EclipseLink exposes a lower-level SessionEventAdapter you can register to catch post-commit or post-delete events at the session level:

public class CleanupListener extends SessionEventAdapter {
    @Override
    public void postCommitUnitOfWork(SessionEvent event) {
        EntityManager em = (EntityManager) event.getResult();
        // Perform the same orphan check logic here
    }
}

You’d register this via persistence.xml or SessionCustomizer.
This gives you “after commit” behavior without polluting your entity model.

Sign up to request clarification or add additional context in comments.

1 Comment

I tried option 1. and it works! I have a follow up question - having debug breakpoint at that count query and looking in database - join table row is still there. Does flush() method just, in this case, remove that row in some abstract way inside jpa logic, not physically removing it from database yet, since it is inside same transaction?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.