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'm currently writing
one on that topic and the first chapters are already available in 👉
Early Release on the O'Reilly platform
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