MongoDB Integration in Jmix

This guide demonstrates how to integrate MongoDB as an additional database within a Jmix application. MongoDB, a popular NoSQL database, allows for flexible and scalable data storage. In this guide, we’ll store VisitLog entries associated with Jmix Visit entities. We’ll leverage Spring Data MongoDB within Jmix to handle CRUD operations for VisitLog entries, creating a logbook structure while keeping the data model and UI logic separate.

Requirements

If you want to implement this guide step by step, you will need the following:

  1. Setup Jmix Studio

  2. Download the sample project. You can download the completed sample project, which includes all the examples used in this guide. This allows you to explore the finished implementation and experiment with the functionality right away.

Alternatively, you can start with the base Petclinic project and follow the step-by-step instructions in this guide to implement the features yourself: Jmix Petclinic.

What We are Going to Build

In this guide, we enhance the Jmix Petclinic by integrating MongoDB to store VisitLog entries that detail specific interactions within the application. This MongoDB setup allows us to manage visit logs independently of the Jmix persistence context, ensuring a separation between core Jmix entities and log data.

The final application includes:

  • MongoDB Running and Connected: MongoDB is set up via Docker and configured to connect with the Jmix application.

  • Data Access Layer: A Spring Data repository and entity representation that facilitates interaction with VisitLog entries in the MongoDB database.

  • Service and UI Integration: MongoDB is integrated with the Jmix UI through a service layer, where Jmix’s UI delegation mechanisms are used to bridge data operations via VisitLogService.

Why Add a Non-Relational Database?

In Jmix applications, the relational database is required as the primary data store. However, there are situations where integrating a secondary, non-relational database like MongoDB is beneficial. This might be due to existing data storage requirements, such as an external MongoDB database managed by another application, or because specific use cases call for flexible data handling outside the constraints of a structured schema.

For instance, storing high-volume, log-like data — such as visit logs — can be more efficient in a NoSQL database. MongoDB’s document-based structure provides a natural fit for this type of data, allowing for quick storage and retrieval without the overhead of complex relational mappings.

Integration Approaches

In Jmix applications, MongoDB can be integrated as an additional database using one of two main approaches:

UI Data Components With Custom Delegates

In this approach, MongoDB access is managed independently through standard UI Data Components configured with custom load delegate and save delegate methods. These delegates override the default behavior by bypassing the Data Manager API, instead pointing CRUD operations to custom service calls that handle data retrieval and persistence directly.

This setup is also the default recommendation in Jmix Studio when working with DTO entities and is generally preferred for cases that require a distinct data access layer. However, since this approach functions independently of the default storage and retrieval APIs of Jmix, some built-in features, such as security constraints, may require custom implementation.

For other features, such as the genericFilter 通用过滤器 component, full support may be challenging because it requires translation of filter conditions to the form applicable to MongoDB.

Providing a Custom DataStore for MongoDB

Jmix allows you to provide a custom implementation of the DataStore interface, enabling the DataManager to work seamlessly with both relational and non-relational data sources.

This approach supports high-level Jmix features, including the security system and the genericFilter component. However, to fully leverage these features, the custom data store implementation must be built to handle specific conditions and constraints. For example, filter conditions need to be mapped to MongoDB-compatible queries, and security constraints must be respected within the MongoDB queries. While this approach allows for tight integration with Jmix’s built-in features, it requires significant implementation effort to ensure compatibility with these higher-level Jmix functionalities, which makes it potentially very time-consuming. Therefore, this approach is beyond the scope of this guide.

If you want to learn more about the DataStore API, see Data Model: Data Stores and in particular the section on Custom Data Store implementations.

In this guide, we focus on using the first approach with UI Data Components with custom delegates and handling the MongoDB interaction on a custom service layer.

MongoDB Setup

Before configuring the Jmix application, we need to set up MongoDB first. In this example, we use Docker Compose to simplify the setup, which is quite common for local development environments and allow you to not have to install Mongo manually on your computer. Besides the database itself we include Mongo Express as an optional, web-based interface for a visual interface to MongoDB data.

Here is the docker-compose.yml file defining the two containers:

docker-compose.yml
services:
  # MongoDB database
  mongo:
    image: mongo
    ports:
      - "27017:27017"
    environment:
      MONGO_INITDB_ROOT_USERNAME: root
      MONGO_INITDB_ROOT_PASSWORD: petclinic


  # Mongo Express Service - Web-based MongoDB admin interface
  mongo-express:
    image: mongo-express
    ports:
      - "8081:8081"
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: root
      ME_CONFIG_MONGODB_ADMINPASSWORD: petclinic
      ME_CONFIG_MONGODB_URL: mongodb://root:petclinic@mongo:27017/
      ME_CONFIG_BASICAUTH: false  # Disables basic authentication for Mongo Express

    # Note: The ME_CONFIG_MONGODB_URL uses the internal Docker network to connect to the `mongo` service by name.
    # The Mongo Express interface is accessible at http://localhost:8081 on the host machine.

To start up the containers, you can use the following docker command:

$ docker compose up
Mongo Express can be accessed at http://localhost:8081, where you can view and manage MongoDB collections and data directly. This is meant as an optional mechanism to introspect what the Jmix application does when it interacts with the database.

With MongoDB running, we can proceed to configure the Jmix application to connect to the database.

Adding Dependencies

To integrate MongoDB with Jmix, add the spring-boot-starter-data-mongodb dependency to your build.gradle file:

build.gradle
dependencies {

    // ...

    // MongoDB Starter for Spring Data
    implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'

}

The spring-boot-starter-data-mongodb is part of the Spring Data project, managed and supported directly by the Spring team. It is the standard solution for integrating MongoDB within Spring applications, providing Spring Data repositories, MongoDB-specific annotations, and utilities. This starter simplifies interactions with MongoDB and is fully compatible with Spring Boot.

The version of spring-boot-starter-data-mongodb aligns automatically with the compatible Spring Boot version, as defined by the Jmix BOM (Bill of Materials). This setup ensures compatibility and minimizes manual dependency management.

MongoDB Configuration

In your Jmix application, configure the MongoDB connection URI in application.properties. Use the credentials defined in the Docker Compose setup:

application.properties
spring.data.mongodb.uri=mongodb://root:petclinic@localhost:27017/visitlogs?authSource=admin
spring.data.mongodb.auto-index-creation=true

This configuration directs Jmix to connect to MongoDB for managing VisitLog entities or other collections. The URI specifies the username and password (root:petclinic) to connect to the database as well as the database where the visit log entries should be stored: visitlogs.

In MongoDB connection URIs, the authSource parameter specifies the database against which MongoDB should authenticate the provided credentials. Setting authSource=admin means that MongoDB will validate the user credentials (like root:petclinic) against the admin database, instead of the specific application database (petclinic in this case). For further information, you can refer to the official MongoDB documentation on authentication sources and user management.

We will now examine each layer in detail, from database access and the service layer to the UI views.

Creating a VisitLog Entity

In this application, the persistence and UI representation of a visit log are separated by using two distinct classes:

  • VisitLog: A Jmix DTO entity used in the UI for managing and displaying visit log data.

  • VisitLogDocument: A dedicated persistence entity mapped to MongoDB for storing and retrieving visit logs.

This separation enhances the application structure by isolating persistence details from the UI layer, ensuring a clear division of responsibilities.

When using MongoDB to persist data, avoid serializing Jmix DTO entities directly to MongoDB, as this may cause serialization issues. Jmix uses entity enhancement for JPA and DTO entities resulting in additional system fields that Jmix uses internally. MongoDB attempts to serialize those system fields, leading to errors or unexpected behavior.

By defining a dedicated Java class annotated with @Document, without the interference of Jmix entity enhancement you can ensure that MongoDB serialization functions as expected.

DTO Entity

The VisitLog entity is used in the Jmix UI for displaying and managing visit log data. As a DTO entity, it does not interact directly with MongoDB but serves as a data model tailored for UI components.

In this UI representation, VisitLog includes a direct reference to the Visit entity from the relational database. This is possible because VisitLog is only used in the UI layer. When data is converted to its persistent form for MongoDB, we store only the UUID of the associated Visit. Since a true foreign key relationship cannot exist between multiple databases, we store the identifier as a simple reference.

VisitLog.java
import io.jmix.core.entity.annotation.JmixId;
import io.jmix.core.metamodel.annotation.JmixEntity;
import io.jmix.core.metamodel.annotation.JmixProperty;

@JmixEntity(name = "petclinic_VisitLog")
public class VisitLog {

    @JmixId
    private String id;

    @JmixProperty(mandatory = true)
    @NotNull
    private Visit visit;

    private String title;

    @InstanceName
    private String description;

    // ...


}

MongoDB Document

The VisitLogDocument class is annotated with @Document, mapping it to a MongoDB collection. This class is the persistent representation of VisitLog, and it is used exclusively in the service layer to interact with MongoDB.

VisitLogDocument.java
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.HashIndexed;
import org.springframework.data.mongodb.core.mapping.Document;

@Document (1)
public class VisitLogDocument {

    @Id (2)
    private String id;

    @HashIndexed (4)
    private String visitId; (3)
    private String title;
    private String description;

    // ...


}
1 @Document - Marks this class as a MongoDB document, treating VisitLogDocument as a collection in MongoDB. This annotation is specific to non-relational databases and is JPA equivalent of @Entity.
2 @Id - Defines the unique identifier field for each document in the MongoDB collection, functioning similarly to a primary key.
3 visitId - Stores the UUID of the associated Visit entry as a String, creating a link between VisitLogDocument and its related Visit record.
4 @HashIndexed - Annotates the visitId with a hashed index, optimizing MongoDB for fast querying by this field.

The VisitLogService class manages data conversions between VisitLog and VisitLogDocument, maintaining a clean separation of concerns between persistence and UI layers.

For more details on indexes, see the documentation on indexing: Index Management in Spring Data MongoDB.

Spring Data Repository

Next, let’s take a look at how to access the database. We define a Spring Data repository interface (leveraging the above dependency spring-boot-starter-data-mongodb). Spring Data provides an abstraction layer specifically designed for data access, taking care of standard CRUD operations and making data interactions simpler and more efficient.

Using Spring Data is an alternative to the DataManager API commonly used in Jmix. The idea is to define an interface that automatically includes standard CRUD methods. Additionally, it allows you to declare custom methods within the interface.

The implementation of this interface is automatically derived and implemented by Spring Data. This means you don’t have to write the code that performs the exact interaction with the database. Instead, you simply declare in the interface what data access methods you need and the implementation is automatically derived from the method name, the return type and the parameters that go into the method.

VisitLogDocumentRepository.java
import org.springframework.data.mongodb.repository.MongoRepository;


public interface VisitLogDocumentRepository extends MongoRepository<VisitLogDocument, String> {

    /**
     * Finds all {@link VisitLogDocument} entries associated with the specified visit ID.
     *
     * @param visitId The ID of the visit associated with the logs to retrieve.
     * @return A list of {@link VisitLogDocument} instances matching the specified visit ID.
     */
    List<VisitLogDocument> findByVisitId(String visitId);
}

This repository interface inherits CRUD operations like save, findById, findAll, and deleteById from MongoRepository, allowing standard data access without requiring additional implementation. It also includes one custom query method that we need to load all VisitLogDocuments for a given Visit: findByVisitId(String visitId). Spring Data MongoDB automatically generates the query for this method based on its naming convention, making it easy to add custom finder methods.

For more details on MongoDB repositories and custom query methods, refer to the Spring Data MongoDB repository documentation.

By the way, Spring Data repositories aren’t just limited to MongoDB or other non-relational databases; you can also use them for regular relational database interactions in Jmix. This provides an alternative to the DataManager API and can be a convenient option in business logic code when Spring Data’s approach feels like a better fit. For more details on using Spring Data repositories in Jmix, see Jmix Data Repositories.

To enable repository scanning, we need to inform Spring Data to search the application’s classpath for MongoDB repository interfaces. This is done through a configuration class located in the base package of our project, annotated with @EnableMongoRepositories. By placing this annotation on a configuration class, we instruct Spring Data to register any interfaces extending MongoRepository within the specified package as beans in the Spring application context, making them available for use in the application.

MongoDbConfiguration.java
@Configuration
@EnableMongoRepositories
public class MongoDbConfiguration {

}

This setup ensures that all custom MongoDB repository interfaces are discovered and registered automatically. With this in place, we are ready to use it in our VisitLogService.

Service Layer for Visit Logs

While the repository handles only database access, the service layer takes on additional responsibilities, in particular data transformations and providing a UI-friendly API.

In our example, we will create a VisitLogService that links the UI and MongoDB by converting VisitLog entries between their UI format (VisitLog) and persistent structure (VisitLogDocument). This setup lets the UI interact with an easy-to-use DTO while ensuring data is correctly stored and retrieved in MongoDB.

Let’s take a look at how the conversion from the Document to the DTO entity works, by reviewing the method to find all VisitLog entities for a given Visit:

VisitLogService.java
@Component("petclinic_VisitLogService")
public class VisitLogService {

    private final EntityStates entityStates;
    private final VisitLogDocumentRepository visitLogDocumentRepository;
    private final DataManager dataManager;

    public VisitLogService(EntityStates entityStates, VisitLogDocumentRepository visitLogDocumentRepository, DataManager dataManager) {
        this.entityStates = entityStates;
        this.visitLogDocumentRepository = visitLogDocumentRepository;
        this.dataManager = dataManager;
    }

    /**
     * Retrieves a list of {@link VisitLog} entries associated with a specific {@link Visit}.
     * <p>
     * This method queries MongoDB for {@link VisitLogDocument} records matching the provided visit ID.
     * Each result is converted to a {@link VisitLog} DTO entity for UI representation. During conversion,
     * the `visit` reference is re-resolved to ensure it is correctly associated for display and interaction in Jmix.
     * </p>
     *
     * @param visit The {@link Visit} entity to retrieve visit logs for.
     * @return A list of {@link VisitLog} entries linked to the specified visit.
     */
    public List<VisitLog> findByVisit(Visit visit) {
        return visitLogDocumentRepository.findByVisitId(visit.getId().toString()).stream()
                .map(this::toVisitLog)
                .toList();
    }

    private VisitLog toVisitLog(VisitLogDocument visitLogDocument) {
        VisitLog visitLog = dataManager.create(VisitLog.class);
        entityStates.setNew(visitLog, false);

        visitLog.setId(visitLogDocument.getId());
        visitLog.setVisit(dataManager.getReference(Visit.class, UUID.fromString(visitLogDocument.getVisitId())));
        visitLog.setTitle(visitLogDocument.getTitle());
        visitLog.setDescription(visitLogDocument.getDescription());

        return visitLog;
    }

The findByVisit() method fetches entries associated with a given Visit and converts them into VisitLog DTOs through toVisitLog(). During this conversion, we use DataManager to resolve the Visit reference. This operation is not actually loading the Visit entity from the database, but instead just creates an in-memory reference to the entity object so that the calling code could use the VisitLog::getVisit and gets back the object of the correct type. Additionally, the resulting VisitLog object is marked as non-new via the corresponding EntityStates method.

UI Integration with Jmix

Now that we have VisitLogService set up, we can use it in the UI to load and store VisitLog data associated with specific visits.

In both the list and detail views, we use load and save delegates to customize how Jmix accesses data. Where Jmix would normally interact with a relational database, we use custom implementations with calls to VisitLogService instead.

Visit Log List View

The VisitLogListView provides a dedicated view for managing VisitLog entries associated with specific Visit entities. This view allows users to:

  1. View a list of VisitLog entries related to a specific Visit.

  2. Create new VisitLog entries, automatically associating them with the selected Visit.

  3. Remove selected VisitLog entries.

Let’s first look at how VisitLogListView receives information about which Visit it should display logs for and how the UI triggers this data loading.

Opening Visit Log List View

In VisitListView, a button allows users to open VisitLogListView for the currently selected Visit.

Visit Log Button

When the button is clicked, the onVisitsDataGridVisitLog method is triggered:

VisitListView.java
@Route(value = "visits", layout = MainView.class)
@ViewController("petclinic_Visit.list")
@ViewDescriptor("visit-list-view.xml")
@DialogMode(width = "64em")
public class VisitListView extends StandardListView<Visit> {

    @Autowired
    private DialogWindows dialogWindows;

    @ViewComponent
    private DataGrid<Visit> visitsDataGrid;

    @Subscribe("visitsDataGrid.visitLog")
    public void onVisitsDataGridVisitLog(final ActionPerformedEvent event) {
        DialogWindow<VisitLogListView> dialog = dialogWindows.view(this, VisitLogListView.class).build();

        VisitLogListView visitLogListView = dialog.getView(); (1)

        visitLogListView.setVisit(visitsDataGrid.getSingleSelectedItem()); (2)

        dialog.open(); (3)
    }
}

This method performs two main actions:

1 First, after calling the DialogWindows.lookup API to build the dialog, we use dialog.getView() to retrieve an instance of VisitLogListView. This enables calling custom methods on VisitLogListView before displaying the dialog (like setVisit).
2 The selected Visit is then passed to the VisitLogListView instance through the setVisit method, setting up the Visit context that will later be used when VisitLogListView loads and displays data.
3 Finally, dialog.open() is called to display VisitLogListView and initiate its lifecycle. Because setVisit is called before open, the visit instance variable is already populated when Jmix UI lifecycle methods execute, ensuring it is available when the data is supposed to be loaded.

Loading Visit Logs in VisitLogListView

List Visit Logs for Visit

Within VisitLogListView, the setVisit method sets the current Visit for which logs will be displayed:

VisitLogListView.java
@Route(value = "visitLogs", layout = MainView.class)
@ViewController("petclinic_VisitLog.list")
@ViewDescriptor("visit-log-list-view.xml")
@LookupComponent("visitLogsDataGrid")
@DialogMode(width = "50em")
public class VisitLogListView extends StandardListView<VisitLog> {

    private Visit visit;

    @Autowired
    private VisitLogService visitLogService;
    @Autowired
    private DialogWindows dialogWindows;


    /**
     * Sets the {@link Visit} reference for this view.
     * <p>
     * This method provides the visit context for the current `VisitLogListView`, ensuring that
     * only `VisitLog` entries related to this `Visit` are loaded and displayed.
     * </p>
     *
     * @param visit The {@link Visit} entity associated with the `VisitLog` entries displayed in this view.
     */
    public void setVisit(Visit visit) {
        this.visit = visit; (1)
    }

    /**
     * Loads `VisitLog` entries associated with the specified {@link Visit}.
     * <p>
     * This method delegates the data loading to {@link VisitLogService#findByVisit}, ensuring that
     * only the logs associated with the current `visit` are retrieved and displayed.
     * </p>
     *
     * @param loadContext The load context provided by Jmix.
     * @return A list of `VisitLog` entries associated with the current visit.
     */
    @Install(to = "visitLogsDl", target = Target.DATA_LOADER) (2)
    protected List<VisitLog> visitLogsDlLoadDelegate(LoadContext<VisitLog> loadContext) {
        return visitLogService.findByVisit(visit); (3)
    }

}
1 The setVisit method stores the Visit instance, making it available for use within the load delegate later in the UI controller lifecycle.
2 Declares this method as the load delegate for visitLogsDl, specifying that data loading for this data loader will be handled by the custom delegate method.
3 Uses VisitLogService to execute the data loading with the previously stored visit instance, retrieving only the relevant VisitLog entries.

The pattern we used to transfer the Visit object to the VisitLogListView applies similarly when working with the VisitLogDetailView. This way, when creating a visit log entry, the Visit reference is already available in the detail view and can be passed directly to the service, ensuring the VisitLog is saved in association with the appropriate Visit.

Next, we’ll take a closer look at how this setup functions within the VisitLogDetailView.

Visit Log Detail View

The VisitLogDetailView enables users to view and edit details for a selected VisitLog entry associated with a specific Visit.

Creating a Visit Log

To save changes to VisitLog entries, this view overrides the default save mechanism by implementing a custom save delegate in the DataContext.

In the saveDelegate method, we leverage VisitLogService to handle persistence. Here’s how it works:

VisitLogDetailView.java
@Route(value = "visitLogs/:id", layout = MainView.class)
@ViewController("petclinic_VisitLog.detail")
@ViewDescriptor("visit-log-detail-view.xml")
@EditedEntityContainer("visitLogDc")
public class VisitLogDetailView extends StandardDetailView<VisitLog> {

    private Visit visit;
    @Autowired
    private VisitLogService visitLogService;

    /**
     * Sets the {@link Visit} reference for this view.
     * <p>
     * This method sets the visit context for the current `VisitLog` view. The provided `Visit` reference
     * is later injected into the `VisitLog` entity before saving, establishing the correct association.
     * </p>
     *
     * @param visit The {@link Visit} entity associated with this `VisitLog`.
     */
    public void setVisit(Visit visit) {
        this.visit = visit;
    }

    /**
     * Saves the {@link VisitLog} entity using {@link VisitLogService}.
     * <p>
     * Before saving, this method sets the associated {@link Visit} reference on the `VisitLog` entity to ensure
     * completeness. The `VisitLogService` then handles the persistence, and the saved `VisitLog` instance
     * is returned in a singleton set as required by the Jmix framework.
     * </p>
     *
     * @param saveContext The save context provided by Jmix.
     * @return A set containing the saved {@link VisitLog} entity.
     */
    @Install(target = Target.DATA_CONTEXT) (1)
    private Set<Object> saveDelegate(final SaveContext saveContext) {
        VisitLog visitLog = getEditedEntity();

        if (visitLog.getVisit() == null && visit != null) {
            visitLog.setVisit(visit); (2)
        }

        VisitLog savedVisitLog = visitLogService.saveVisitLog(visitLog); (3)

        return Set.of(savedVisitLog);
    }

}
1 Configures this method as the save delegate for the DataContext, meaning it will intercept and handle the save process for VisitLog entities.
2 Checks if the VisitLog entry has no Visit reference and, if so, assigns the current Visit to ensure it’s linked correctly.
3 Saves the VisitLog entity by calling visitLogService.saveVisitLog, which ensures that the entity is persisted to MongoDB through the service layer.

By defining this custom save delegate, we maintain control over how VisitLog entries are saved, ensuring any missing Visit reference is populated before persistence. This method also simplifies saving logic in the UI, as it seamlessly integrates the service call within the view.

For more information see Data Context: Save Delegate in the reference documentation.

Summary

In this guide, we integrated MongoDB as an additional datastore to manage visit log entries in our application. By using MongoDB alongside the main relational database, we demonstrated how Jmix can effectively support log data storage and retrieval, making it possible to handle large, flexible datasets outside the primary persistence context.

We combined the use of loadDelegate and saveDelegate methods with DTO entities to enable data loading and storage via a custom service layer. Jmix’s UI framework seamlessly integrates with both types of entities, making it possible to use features like data binding and UI interaction without worrying about the underlying persistence mechanism.

Jmix supports polyglot persistence - making it easy to integrate existing non-relational data sources or use different databases on a per-use-case basis. This flexibility allows you to easily connect to various data sources and choose the best technology depending on the specific needs, whether it involves integrating an existing MongoDB data store or combining different types of databases within the same architecture to optimize for each use case.