1

I want to assign a category to a recipe. If I assign a second category with the same name to the recipe it does another insert to the database that aborts (Abort due to constraint violation (UNIQUE constraint failed: category.name) - this is actually fine). I want to reuse this entry and attach it to the recipe. Is there a JPA way to do this "automatically" or do I have to handle this? Should I search for a category with the same name in the setCategory method and use this one if present? Is there a Pattern?

@Entity
public class Recipe {
    private Integer id;
    private Category category;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @ManyToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "category_id", referencedColumnName = "id")
    public Category getCategory() {
        return category;
    }

    public void setCategory(Category category) {
        this.category = category;
    }

} 

@Entity
public class Category {
    private Integer id;
    private String name;
    private List<Recipe> recipes;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Basic
    @Column(name = "name")
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @OneToMany(mappedBy = "category", cascade = {CascadeType.ALL})
    public List<Recipe> getRecipes() {
        return recipes;
    }

    public void setRecipes(List<Recipe> recipes) {
        this.recipes = recipes;
    }
} 

Example:

Category category = new Category();
category.setName("Test Category");
cookbookDAO.add(cookbook);

Recipe recipe = new Recipe();
recipe.setTitle("Test Recipe");
recipe.setCategory( category );
recipeDAO.add(recipe);

Executing this twice results in the UNIQUE constraint failed: category.name. This is fine since I don't want multiple categories with the same name. The database enforced this but I'm looking for the soltuion to enforce this on the java language level too. The DDL:

CREATE TABLE "recipe"
(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  category_id INTEGER,
  FOREIGN KEY (category_id) REFERENCES category(id)
);
CREATE TABLE "category"
(
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  `name` VARCHAR,
  UNIQUE(`name`) ON CONFLICT ABORT
);
No3x
  • 470
  • 1
  • 8
  • 28
  • What you need is a collection of category and map them using Hibernate's collection mapping rules. See [this](https://docs.jboss.org/hibernate/orm/3.3/reference/en/html/collections.html). In other words, some bits are needed to be done by yourself. – ha9u63a7 Jul 23 '16 at 16:54
  • and "the second category with the same name" comes from where? created from scratch ? (in which case is a different object), retrieved by find()/query() ? in which case represents what is already in the database. Unless you DEFINE your persistence nobody can know. FWIW A JPA "Unique Constraint" is enforced in the DB only ... in DDL –  Jul 23 '16 at 17:57

2 Answers2

0

Hello the behavior you are describing is a result of the mapping

@ManyToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "category_id", referencedColumnName = "id")
    public Category getCategory() {
        return category;
    }

If we translate this mapping is simple language. You are saying:

Everytime I attempt to save a Recipe the corresponding Category should be persisted as well.

The problem here comes from the fact that you already have an existing Category so the @Cascade.PERSIST here is not appropriate.

The semantics here is the opposite . A Recipie creates a Category only if a Category does not exist yet. This mean that the creation of the Category is more of an exception than a general rule.

This mean that you have two options here.

Option 1 is to remove Cascade.PERSIST.

Option 2 is to replace it with Cascade.MERGE.

Option 3 is to go the other way. Instead of annotating the @ManyToOne relationship in Recipe to Category with Cascade.PERSIST to annotate the @OneToMany relationship from the Category to Recipe.

The advantage of such approach is very simple. While you not always want to create a new category when adding a new recipe. It is 100% all the time you want to add a new Category you also want to add all the attached Recipies.

Also I will recommend you to favor Bidirectional relationships over unidirectional ones and read this article about merge and persist JPA EntityManager: Why use persist() over merge()?

Community
  • 1
  • 1
Alexander Petrov
  • 9,204
  • 31
  • 70
0

The problem is you are creating a new category with the following statement:

Category category = new Category();

Because this instance of the Category entity is new and accordingly does not have a persistent identity the persistence provider will try to create a new record in the database. As the name field of the category table is unique you get constraint violation exception from the database.

The solution is first fetching the category and assign it to recipe. So what you have to do is the following:

String queryString = "SELECT c FROM Category c WHERE c.name = :catName";
TypedQuery<Category> query = em.createQuery(queryString, Category.class);
em.setParameter("catName", "Test Category");
Category category = query.getSingleResult();

This fetched instance is a managed entity and the persistence provider will not try to save. Then assign this instance to the recipe as you already did:

recipe.setCategory( category );

In this case the cascading will just ignore saving the category instance when recipe is saved. This is stated in the JPA 2.0 specification in section 3.2.2 Persisting an Entity Instance as follows:

If X is a preexisting managed entity, it is ignored by the persist operation.

ujulu
  • 3,289
  • 2
  • 11
  • 14