Understanding Embedded Types in MongoDB with Java and Helidon
Rate this tutorial
MongoDB provides a flexible schema design, allowing data modeling patterns that differ significantly from traditional relational databases. With MongoDB, you model data specifically for your use cases rather than using a generic schema that attempts to cover all cases.
1 git clone git@github.com:soujava/mongodb-embedding-sample.git
For this tutorial, you’ll need:
- Java 21.
- Maven.
- A MongoDB cluster.
- MongoDB Atlas (Option 1)
- Docker (Option 2)
- A Helidon project with MongoDB integrated:
You can use the following Docker command to start a standalone MongoDB instance:
1 docker run --rm -d --name mongodb-instance -p 27017:27017 mongo
MongoDB people make a more efficient way of modeling data than what relational database users are used to. For one, it allows you to set your data as embedded types, subdocuments, and arrays with a more dynamic and flexible schema. This feature allows you to design a schema that best fits your application's requirements.
Subdocuments group related fields, reducing retrieval time and enhancing query accuracy. Arrays combine data from various tables, simplifying access.
Moreover, MongoDB's flexible schema allows for altering the design without distressing the existing ones. This feature is handy due to unforeseen progress and changes in the application scope, making MongoDB suitable for current-era data management.
While MongoDB has many strengths, it shares a common weakness with other database systems regarding data structuring. Optimizing a schema reduces the need for joins, improves performance, and simplifies development by minimizing relationships. Additionally, a well-structured schema can lower costs for storage, data retrieval, and command execution.
In this tutorial, you'll learn to use MongoDB with Helidon and Eclipse JNoSQL to manage products through a robust API. We'll create a Product entity that includes embedded types like Manufacturer and Category, as well as array fields for tags, all within a paginated RESTful API for efficient product management.
Begin by following the Helidon with MongoDB tutorial for the setup. After completing the setup, ensure you update the database name in the microprofile-config.properties file to reflect our example accurately.
1 # Define the database name 2 jnosql.document.database=products
With this foundation, we'll move on to implementing the Product entity and explore how MongoDB's embedded types can simplify data modeling for your application.
To start, we’ll define the core of our application: the
Product
entity. This class represents the product data structure and contains fields such as id
, name
, manufacturer
, tags
, and categories
. The manufacturer
field is modeled as a subdocument, while the tags
field demonstrates how to use arrays in MongoDB. These fields showcase the flexibility of MongoDB’s schema design, allowing us to group related information and co-locate it efficiently.In the src/main/java directory, create a Product class:
1 import jakarta.json.bind.annotation.JsonbVisibility; 2 import jakarta.nosql.Column; 3 import jakarta.nosql.Convert; 4 import jakarta.nosql.Entity; 5 import jakarta.nosql.Id; 6 import org.eclipse.jnosql.databases.mongodb.mapping.ObjectIdConverter; 7 import org.soujava.samples.mongodb.products.infra.FieldVisibilityStrategy; 8 import java.util.List; 9 import java.util.Objects; 10 import java.util.Set; 11 12 13 14 public class Product { 15 16 17 private String id; 18 19 private String name; 20 21 private Manufacturer manufacturer; 22 23 private List<String> tags; 24 25 private Set<Category> categories; 26 }
- @Entity: Marks the Product class as a database entity for management by Jakarta NoSQL.
- @JsonbVisibility (FieldVisibilityStrategy.class): Defines a visibility strategy for JSON binding, serializing by field instead of method, so getters and setters are unnecessary.
- @Id: Indicates the primary identifier for the entity, uniquely distinguishing each document in the MongoDB collection.
- @Convert (ObjectIdConverter.class): Converts the id field between Java strings and MongoDB's ObjectId format for compatibility.
- @Column: Maps fields (name, manufacturer, tags, categories) for reading from or writing to MongoDB.
Next, we’ll define the
Manufacturer
subdocument. These embedded type groups detail the product’s manufacturer, such as name
, address
, and contactNumber
. Using a subdocument, we can store this related information within the product, eliminating the need for a separate collection. For simplicity and immutability, we’ll implement this as a Java record.Create a Manufacturer record in src/main/java:
1 import jakarta.json.bind.annotation.JsonbVisibility; 2 import jakarta.nosql.Column; 3 import jakarta.nosql.Embeddable; 4 import org.soujava.samples.mongodb.products.infra.FieldVisibilityStrategy; 5 6 7 8 public record Manufacturer( 9 @Column String name, 10 @Column String address, 11 @Column String contactNumber 12 ) {}
After the manufacturer, we’ll create another embedded type:
Category
. This subdocument holds information about product categories, including their names
and descriptions
. Like the Manufacturer
, Category
is implemented as a Java record, keeping our model clean and immutable while enabling better query scope for related fields.Create a Category record:
1 import jakarta.nosql.Column; 2 import jakarta.nosql.Embeddable; 3 4 5 public record Category( 6 @Column String name, 7 @Column String description 8 ) {}
- @Embeddable: Specifies that the class is an embeddable type, meaning it is not stored as a separate collection but embedded within another document. The EmbeddableType.GROUPING option groups the annotated fields as a subdocument while the FLAT option works, as we know from the Jakarta Persistence, former JPA, thus, the fields will be appended as the Product’s field.
With the entity and subdocuments in place, we need a way to interact with the database. Here, we define the
ProductRepository
interface, leveraging Jakarta Data for seamless CRUD operations. This repository bridges our code with the MongoDB database, simplifying data access and storage.Define a ProductRepository to interact with MongoDB:
1 import jakarta.data.repository.BasicRepository; 2 import jakarta.data.repository.Repository; 3 4 5 public interface ProductRepository extends BasicRepository<Product, String> { 6 }
We’ll create the ProductResource class to expose our data through a RESTful API. This resource handles HTTP requests, enabling clients to create, retrieve (with pagination), and delete products. Integrating the
ProductRepository
ensures efficient interaction with the MongoDB database while adhering to modern web standards.Create a ProductResource class to handle HTTP requests:
1 import jakarta.data.Order; 2 import jakarta.data.Sort; 3 import jakarta.data.page.PageRequest; 4 import jakarta.enterprise.context.ApplicationScoped; 5 import jakarta.inject.Inject; 6 import jakarta.ws.rs.DELETE; 7 import jakarta.ws.rs.DefaultValue; 8 import jakarta.ws.rs.GET; 9 import jakarta.ws.rs.POST; 10 import jakarta.ws.rs.Path; 11 import jakarta.ws.rs.PathParam; 12 import jakarta.ws.rs.QueryParam; 13 import jakarta.ws.rs.WebApplicationException; 14 import jakarta.ws.rs.core.Response; 15 import java.util.List; 16 import java.util.logging.Logger; 17 18 19 20 public class ProductResource { 21 private static final Order<Product> ORDER = Order.by(Sort.asc("name")); 22 private static final Logger LOGGER = Logger.getLogger(ProductResource.class.getName()); 23 private final ProductRepository repository; 24 25 26 public ProductResource(ProductRepository repository) { 27 this.repository = repository; 28 } 29 30 31 public ProductResource() { 32 this(null); 33 } 34 35 36 public List<Product> get( 37 int page, 38 int size) { 39 LOGGER.info("Fetching page: " + page + ", size: " + size); 40 var request = PageRequest.ofPage(page).size(size); 41 return repository.findAll(request, ORDER).content(); 42 } 43 44 45 public Product insert(Product product) { 46 LOGGER.info("Saving product: " + product); 47 return repository.save(product); 48 } 49 50 51 52 public void delete( String id) { 53 repository.deleteById(id); 54 } 55 56 57 58 public Product findById( String id) { 59 return repository.findById(id) 60 .orElseThrow(() -> new WebApplicationException("Product not found with ID: " + id, Response.Status.NOT_FOUND)); 61 } 62 }
Finally, it’s time to integrate everything and run the application. After packaging the project with Maven, start the application. Ensure MongoDB is running, either locally or via MongoDB Atlas. Once the application is up and running, you can test the API endpoints to interact with the
Product
data.Make sure MongoDB is running (locally or on MongoDB Atlas). Then, build and run the application:
1 mvn package 2 java -jar target/embedded-mongodb.jar
1 curl -X POST -H "Content-Type: application/json" -d '{ 2 "name": "Smartphone", 3 "manufacturer": { 4 "name": "Tech Co", 5 "address": "1234 Tech Street", 6 "contactNumber": "+123456789" 7 }, 8 "tags": ["mobile", "smart"], 9 "categories": [ 10 {"name": "Electronics", "description": "Devices and gadgets." }, 11 {"name": "Gadgets", "description": "Small useful tools and devices."} 12 ] 13 }' http://localhost:8080/products
1 curl -X GET http://localhost:8080/products?page=1&size=10
1 curl -X GET http://localhost:8080/products/{id}
1 curl -X DELETE http://localhost:8080/products/{id}
MongoDB's embedded types simplify relationships compared to relational databases. You can reduce joins and improve performance by grouping related data as subdocuments and arrays. Using Helidon, Jakarta Data, and Eclipse JNoSQL, you can easily create modern, scalable Java applications with MongoDB.
Access the source code used in this tutorial.
References:
Top Comments in Forums
There are no comments on this article yet.