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.
Data Engineering Design Patterns
Looking for a book that defines and solves most common data engineering problems? I wrote
one on that topic! You can read it online
on the O'Reilly platform,
or get a print copy on Amazon.
I also help solve your data engineering problems 👉 contact@waitingforcode.com 📩
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.
Consulting
With nearly 16 years of experience, including 8 as data engineer, I offer expert consulting to design and optimize scalable data solutions.
As an O’Reilly author, Data+AI Summit speaker, and blogger, I bring cutting-edge insights to modernize infrastructure, build robust pipelines, and
drive data-driven decision-making. Let's transform your data challenges into opportunities—reach out to elevate your data engineering game today!
👉 contact@waitingforcode.com
đź”— past projects

