Posted by: Ivko | October 27, 2009

Каскади

Тези дни имах един малък проблем с open source програмата, която пиша. Този проблем произтичаше от грешната ми идея, че Entity Manager-ът в едно JPA приложение се грижи даже за неща, които изрично му е казано да не пипа. Ама и аз съм един готованец! За какво става въпрос:

Приложението се занимава най-общо с класифициране на продукти по категории и сравняване на техните цени. В базата има една таблица за категориите и друга таблица за продуктите. Релацията е 1:n – в една категоря може да има много продукти, но един продукт може да бъде само в една категория. И сега идва времето за изискването за каскадност: когато се изтрие дадена категория, продуктите, които са закачени към него, не бива да бъдат трити автоматично. Това на езика на JPA е описано така (изпускам допълнителните детайли):

1) Category entity-то:

@Entity
@Table(name = "CATEGORY")
public class Category implements Serializable {
...
    @OneToMany(mappedBy = "category", cascade = { CascadeType.PERSIST,
        CascadeType.REFRESH })
    private Collection products;
...
}

2) Product entity-то:

@Entity
@Table(name = "PRODUCT")
public class Product implements Serializable {
...
    @ManyToOne
    @JoinColumn(name = "CATEGORY", referencedColumnName = "ID")
    private Category category;
...
}

Най-общо казано, в @OneToMany анотацията в Category класа казвам на Entity Manager-а, че искам да се грижи за cascade на persist и refresh операциите, а останалите две (delete и merge) не са негова работа.

И сега: пиша си един DAO клас, който покрива CRUD операциите на Category. Ето една от тези операции:

public class JpaCategoriesDao {
....
    public void deleteCategory(Category cat) throws ComparatorDaoException {
        entityManager.getTransaction().begin();
        Category catx = entityManager.find(Category.class, cat.getId());
        if (catx != null) {
            entityManager.remove(catx);
            entityManager.getTransaction().commit();
        } else {
            log.debug("Category " + cat + " does not exist.");
            entityManager.getTransaction().rollback();
        }
    }
}

И при това положение, ако се опитам да изтрия категория, която съдържа продукт, entity manager-ът гърми с един много интересен exception:

org.hibernate.exception.GenericJDBCException: Could not execute JDBC batch update
  at org.hibernate.exception.SQLStateConverter.handledNonSpecificException(SQLStateConverter.java:126)
  at org.hibernate.exception.SQLStateConverter.convert(SQLStateConverter.java:114)
  at org.hibernate.exception.JDBCExceptionHelper.convert(JDBCExceptionHelper.java:66)
  at org.hibernate.jdbc.AbstractBatcher.executeBatch(AbstractBatcher.java:275)
  at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:266)
  at org.hibernate.engine.ActionQueue.executeActions(ActionQueue.java:172)

След доста чуденки, консулатция с форуми, с колеги, които разбират от JPA и т.н., стигнах до великия извод: от къде на къде entity manager-ът трябва да се грижи за продуктите при триене, при положение, че не знае какво да ги прави. Очевидно не трябва да ги трие (описал съм го в cascade атрибута). Тогава какво: да ги null-ира ли? Или да им дава default-на стойност?

Ето защо, след няколко дни борба, стигнах до това не особено елегантно (но работещо) решение:

public void deleteCategory(Category cat) throws ComparatorDaoException {
    entityManager.getTransaction().begin();
    Category catx = entityManager.find(Category.class, cat.getId());
    if (catx != null) {
        if (catx.hasProducts()) {
            for (Product product : catx.getProducts()) {
                product.setCategory(null);
            }
        }
        entityManager.remove(catx);
        entityManager.getTransaction().commit();
    } else {
        log.debug("Category " + cat + " does not exist.");
        entityManager.getTransaction().rollback();
    }
}

Сега вътре в самата транзакция (това е много важно!), която трие категорията, се проверява дали въпросната категория има продукти и преди да кажем delete, set-ваме category полето на всички въпросни продукти на null.

При commit на транзакцията, entity manager-ът ще направи първо refresh операция на Product entity-то (която е разрешена в @OneToMany анотацията) и чак след тогава ще направи delete.

С две думи, когато искаме контролът за дадена операция да е при нас, ние трябва да се държим мъжки и да правим нещата докрай!🙂


Responses

  1. Ванка,
    Много интересен казус, на пръв поглед тривиален, на втори и трети не толкова …

    Интересно ми е какво ще стане ако:

    1. catx.getProducts().clear();
    2. entityManager.remove(catx);

    Предполагам отново Exception, но все пак при манипулиране на елементите на products, имаме манипулиране на връзката и може би ще product.category-тата ще се null-ират от framework-а … но все пак Продуктите са си собственици на връзката и решението ти ми изглежда съвсем логично

    Оп са


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Categories

%d bloggers like this: