Collections in JPA

JPA 2.0 brought some improvements for handling object collections. Before this release, to associate two objects, we were forced to define them as entities and use one from (One|Many)To(One|Many) mapping annotations. With 2.0 version came another possibility - @ElementCollection.

Through this article we'll see how to use @ElementCollection. At the begin we'll discover some basic concepts for this feature. After that we'll show a sample test case illustrating the using of @ElementCollection.

What is @ElementCollection ?

In very simple definition, @ElementCollection is an annotation used to define the collections of @Embeddable objects. They're defined as a field of @Entity object, implementing one of Java's collection class, exactly as classical @OneToMany relationship. Usually objects referenced by @ElementCollection are stored in separate database table. They can be simple data types (as String) or more complex ones, composed by multiple columns.

The querying of @ElementCollection items it's not made through some JOIN clause. Instead of using it, JPA provider will execute separated query and take the rows by join columns defined in @CollectionTable property. This property represents foreign key used in database to mark relationship between entity and given collection of embeddable objects.

Unlike classical @OneToMany relationship, @ElementCollection is used to manage embeddable objects, so the objects without identity. This fact (no @Entity objects) brings also another point - embeddable collections don't have their own lifecycle. They can exist only through parent entity encompassing them, for example: when this entity is deleted, they're deleted too.

Example of @ElementCollection

To see @ElementCollection in action, we need to begin by define the mapping:

// GiftPackItem.java
@Embeddable
public class GiftPackItem {

  private Product product;
  private double price;

  @ManyToOne
  @JoinColumn(name = "product_id")
  public Product getProduct() {
    return this.product;
  }

  @Column(name = "price", length = 7, precision = 2)
  public double getPrice() {
    return this.price;
  }

  public void setProduct(Product product) {
    this.product = product;
  }

  public void setPrice(double price) {
    this.price = price;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
      .add("product", this.product.getName()).add("price", this.price).toString();
  }

}

// GiftPack.java
@Entity
@Table(name = "gift_pack")
public class GiftPack {

  private Long id;
  private String name;
  private List<GiftPackItem> items;

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

  @Column(name = "name", length = 30)
  public String getName() {
    return this.name;
  }

  @ElementCollection
  @CollectionTable(
    name="gift_pack_item",
    joinColumns={@JoinColumn(name="gift_pack_id")}
  )
  public List<GiftPackItem> getItems() {
    return this.items;
  }
  // ...
}

And below you can find JUnit case to demonstrate @ElementCollection in real use (all important information is written as comments):

/**
 * Tests for {@link javax.persistence.ElementCollection} annotation.
 *
 * Data expected before the test :
 * <pre>
 * mysql> select * from gift_pack;
 * +----+----------------+
 * | id | name           |
 * +----+----------------+
 * |  1 | Breakfast pack |
 * |  2 | Sport pack     |
 * +----+----------------+
 * 2 rows in set (0.00 sec)
 *
 * mysql> select * from gift_pack_item;
 * +--------------+------------+-------+
 * | gift_pack_id | product_id | price |
 * +--------------+------------+-------+
 * |            1 |          1 |  1.00 |
 * |            1 |          4 |  0.50 |
 * |            2 |          1 |  1.00 |
 * |            2 |          3 |  0.33 |
 * |            2 |          5 |  1.19 |
 * +--------------+------------+-------+
 * 5 rows in set (0.00 sec)
 *
 * mysql> select id, name from product where id in (1, 3, 4, 5);
 * +----+--------------+
 * | id | name         |
 * +----+--------------+
 * |  1 | cereal       |
 * |  3 | water        |
 * |  4 | milk         |
 * |  5 | orange juice |
 * +----+--------------+
 * 4 rows in set (0.00 sec)
 * </pre>
 *
 * @author Bartosz Konieczny
 */
public class ElementCollectionTest extends AbstractJpaTester {


  @Test
  public void testRead() {
    Query query = entityManager.createQuery("SELECT gp FROM GiftPack gp");
    List<GiftPack> packs = query.getResultList();
    assertEquals("The 1st pack should contain 2 products", 2, 
      packs.get(0).getItems().size());
    String[] products1 = new String[]{"cereal", "milk"};
    equalsFromOne(products1, packs.get(0).getItems().get(0).getProduct().getName(), 
      packs.get(0).getName());
    equalsFromOne(products1, packs.get(0).getItems().get(1).getProduct().getName(), 
      packs.get(0).getName());

    assertEquals("The 2nd pack should contain 2 products", 3, packs.get(1).getItems().size());
    String[] products2 = new String[]{"cereal", "orange juice", "water"};
    equalsFromOne(products2, packs.get(1).getItems().get(0).getProduct().getName(), 
      packs.get(1).getName());
    equalsFromOne(products2, packs.get(1).getItems().get(1).getProduct().getName(), 
      packs.get(1).getName());
    equalsFromOne(products2, packs.get(1).getItems().get(2).getProduct().getName(), 
      packs.get(1).getName());

    // If you inspect MySQL query logs, you'll see that 2 queries were created to get both packs - even if
    // only one createQuery("") method is invoked :
    // - 1st one
    // 106 Query     select items0_.gift_pack_id as gift_pac1_5_0_, items0_.price as price2_6_0_, items0_.product_id as
    // product_3_6_0_, product1_.id as id1_10_1_, product1_.category_id as category7_10_1_, product1_.name as name2_10_1_,
    // product1_.price as price3_10_1_, product1_.meta_description as meta_des4_10_1_, product1_.meta_keywords as
    // meta_key5_10_1_, product1_.meta_title as meta_tit6_10_1_, category2_.id as id1_4_2_, category2_.name as name2_4_2_,
    // category2_.seo_desc as seo_desc3_4_2_, category2_.seo_keywords as seo_keyw4_4_2_, category2_.seo_title as
    // seo_titl5_4_2_ from gift_pack_item items0_ left outer join product product1_ on items0_.product_id=product1_.id
    // left outer join category category2_ on product1_.category_id=category2_.id where items0_.gift_pack_id=1
    //
    // - 2nd one
    // 106 Query     select items0_.gift_pack_id as gift_pac1_5_0_, items0_.price as price2_6_0_,
    // items0_.product_id as product_3_6_0_, product1_.id as id1_10_1_, product1_.category_id as category7_10_1_,
    // product1_.name as name2_10_1_, product1_.price as price3_10_1_, product1_.meta_description as meta_des4_10_1_,
    // product1_.meta_keywords as meta_key5_10_1_, product1_.meta_title as meta_tit6_10_1_, category2_.id as id1_4_2_,
    // category2_.name as name2_4_2_, category2_.seo_desc as seo_desc3_4_2_, category2_.seo_keywords as seo_keyw4_4_2_,
    // category2_.seo_title as seo_titl5_4_2_ from gift_pack_item items0_ left outer join product product1_ on
    // items0_.product_id=product1_.id left outer join category category2_ on product1_.category_id=category2_.id
    // where items0_.gift_pack_id=2
  }

  @Test
  public void testInsert() {
    EntityTransaction transaction = entityManager.getTransaction();
    try {
      transaction.begin();

      Seo seo = new Seo();
      seo.setDescription("desc");
      seo.setKeywords("key");
      seo.setTitle("tit");

      Category category = new Category();
      category.setName("tea time");
      category.setSeo(seo);
      entityManager.persist(category);

      Product coffee = new Product();
      coffee.setName("coffee");
      coffee.setSeo(seo);
      coffee.setCategory(category);
      entityManager.persist(coffee);

      Product tea = new Product();
      tea.setName("tea");
      tea.setSeo(seo);
      tea.setCategory(category);
      entityManager.persist(tea);

      GiftPack pack = new GiftPack();
      pack.setName("tea time pack");

      GiftPackItem item1 = new GiftPackItem();
      item1.setPrice(29.99d);
      item1.setProduct(coffee);

      GiftPackItem item2 = new GiftPackItem();
      item2.setPrice(0.01d);
      item2.setProduct(tea);

      pack.setItems(new ArrayList<GiftPackItem>(Arrays.asList(item1, item2)));
      entityManager.persist(pack);

      Query query = entityManager.createQuery("SELECT gp FROM GiftPack gp WHERE gp.id = :id");
      query.setParameter("id", pack.getId());

      GiftPack dbPack = (GiftPack) query.getSingleResult();
      assertEquals("The inserted pack should contain 2 products", 2, dbPack.getItems().size());
      assertEquals("Pack's name should be 'tea time pack'", "tea time pack", dbPack.getName());
      String[] products1 = new String[]{"coffee", "tea"};
      equalsFromOne(products1, dbPack.getItems().get(0).getProduct().getName(), dbPack.getName());
      equalsFromOne(products1, dbPack.getItems().get(1).getProduct().getName(), dbPack.getName());

      // Queries used to insert new pack and products were :
      // insert into category (name, seo_desc, seo_keywords, seo_title) values ('tea time', 'desc', 'key', 'tit')
      // insert into product (category_id, name, price, meta_description, meta_keywords, meta_title) values
      // (5, 'coffee', 0.0, 'desc', 'key', 'tit')
      // insert into product (category_id, name, price, meta_description, meta_keywords, meta_title)
      // values (5, 'tea', 0.0, 'desc', 'key', 'tit')
      // insert into gift_pack (name) values ('tea time pack')
    } finally {
      transaction.rollback();
    }
  }

  private void equalsFromOne(String[] names, String name, String packName) {
    boolean wasFound = false;
    int i = 0;
    while (!wasFound && i < names.length) {
      String itemName = names[i];
      wasFound = name.equals(itemName);
      i++;
    }
    assertTrue("Product " + name + " doesn't exist for pack " + packName, wasFound);
  }

}

This article shows JPA feature to implement collections of not-entity objects. It can be made with @ElementCollection annotation, accompanied by @CollectionTable one. The last annotation represents database relation between embeddable collection and its entity.


If you liked it, you should read:

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!