Jmix Data Access with DataManager and Fetch Plans
🎧 Focus sounds
Soft beat sounds that many learners like while reading. Pick one, press Play, and keep studying.
Keep the volume low. These sounds are for comfort while studying, not a medical treatment. If you have epilepsy or are sensitive to pulsing sounds, use the background noise only.
In this lesson
Views load data declaratively. In services, scheduled jobs and event handlers you work with data in Java through DataManager, Jmix’s main data access API. It’s similar to Spring Data repositories, but security-aware and fetch-plan-aware.
DataManager basics#
@Component
public class CourseService {
private final DataManager dataManager;
public CourseService(DataManager dataManager) { this.dataManager = dataManager; }
public List<Course> beginnerCourses() {
return dataManager.load(Course.class)
.query("select c from Course c where c.level = :level order by c.title")
.parameter("level", CourseLevel.BEGINNER)
.list();
}
public Optional<Course> byTitle(String title) {
return dataManager.load(Course.class)
.query("select c from Course c where lower(c.title) = lower(:t)")
.parameter("t", title)
.optional();
}
public Course byId(UUID id) {
return dataManager.load(Course.class).id(id).one(); // throws if not found
}
public Course createCourse(String title, Category category) {
Course c = dataManager.create(Course.class); // new instance with UUID assigned
c.setTitle(title);
c.setCategory(category);
c.setLevel(CourseLevel.BEGINNER);
return dataManager.save(c); // INSERT, returns the saved instance
}
public void raisePrice(Course course, BigDecimal percent) {
course.setPrice(course.getPrice().multiply(BigDecimal.ONE.add(percent.movePointLeft(2))));
dataManager.save(course); // UPDATE
}
public void delete(Course course) {
dataManager.remove(course); // soft delete if the entity has the trait
}
}
Key points:
load(...).query(...)with named parameters (:level) only, never string concatenation.list(),one()(exactly one, else exception),optional()..firstResult(n).maxResults(m)for paging.save(...)accepts several entities and saves them in one transaction:dataManager.save(course, lesson1, lesson2).- Use
SaveContextfor complex saves with removals.
Fetch plans: loading related data correctly#
Entities loaded by DataManager are detached. If a reference wasn’t loaded, accessing it later fails. A fetch plan says what to load:
List<Course> courses = dataManager.load(Course.class)
.query("select c from Course c")
.fetchPlan(fp -> fp.addFetchPlan(FetchPlan.BASE) // all local attributes
.add("category", FetchPlan.BASE) // + the category
.add("lessons", FetchPlan.BASE)) // + the lessons
.list();
FetchPlan.BASE(_base): all local (non-reference) attributes.FetchPlan.INSTANCE_NAME(_instance_name): only what’s needed for the display name.- Named fetch plans can also be declared once in a
fetch-plans.xmlfile and reused by name.
Jmix builds efficient queries from the plan (joins or batch loading), avoiding the N+1 problem automatically.
Security: DataManager vs UnconstrainedDataManager#
DataManager applies the current user’s permissions: entity operations (read/create/update/delete) and row-level policies. A user without permission to read salaries simply doesn’t get those rows or attributes.
For system code that must see everything (a nightly job, a migration), use the unconstrained variant explicitly:
@Component
public class NightlyReport {
private final UnconstrainedDataManager system;
public NightlyReport(UnconstrainedDataManager system) { this.system = system; }
public long countAllCourses() {
return system.load(Course.class).all().list().size();
}
}
(dataManager.unconstrained() returns the same thing.) Be deliberate about it: unconstrained access in user-facing code is a security hole.
Services and transactions#
Put business logic in Spring beans. DataManager operations each run in their own transaction; for several operations that must succeed together, save them in one call or use Spring’s @Transactional with a TransactionalDataManager.
@Component
public class EnrollmentService {
private final DataManager dataManager;
public EnrollmentService(DataManager dataManager) { this.dataManager = dataManager; }
public Enrollment enroll(Student student, Course course) {
long active = dataManager.loadValue(
"select count(e) from Enrollment e where e.student = :s and e.completed = false", Long.class)
.parameter("s", student)
.one();
if (active >= 3) throw new IllegalStateException("Finish a course first");
Enrollment e = dataManager.create(Enrollment.class);
e.setStudent(student);
e.setCourse(course);
return dataManager.save(e);
}
}
loadValue / loadValues return scalar values and aggregates (counts, sums) instead of entities.
Entity events#
React to data changes in one place, whatever saved the data (a view, a service, the REST API):
@Component
public class CourseEventListener {
@EventListener
public void onCourseSaving(final EntitySavingEvent<Course> event) {
Course c = event.getEntity();
c.setTitle(c.getTitle().trim()); // normalise before saving
}
@TransactionalEventListener
public void onCourseChanged(final EntityChangedEvent<Course> event) {
if (event.getType() == EntityChangedEvent.Type.CREATED) {
// after commit: send a notification, update a search index…
}
}
}
EntitySavingEvent/EntityLoadingEvent: change the entity before it’s saved or after it’s loaded.EntityChangedEvent: what changed (old values available); with@TransactionalEventListenerit runs after the commit, the right place for emails and integrations.
Same rule as Spring Boot: views stay thin, services hold business rules, events handle side effects. Your Jmix code then stays testable with normal Spring Boot tests (@SpringBootTest + DataManager).
Exercise#
- Write
CourseService.totalPriceByCategory()usingloadValueswithgroup by. - Load courses with their lessons using a fetch plan and print the lesson count of each.
- Add an
EntitySavingEventlistener that rejects lessons withdurationMinutes > 180. - Call your service from a button in the course list view and show the result in a notification.
Hint for 1
dataManager.loadValues("select c.category.name, sum(c.price) from Course c group by c.category.name").properties("category", "total").list() returns KeyValueEntity objects; read them with kv.getValue("total").
Interview corner#
- What is DataManager? Jmix’s main API to load, save and remove entities, applying security and fetch plans.
- DataManager vs UnconstrainedDataManager? The first enforces the current user’s permissions; the second bypasses them for system operations.
- What problem do fetch plans solve? They define the object graph to load, avoiding lazy-loading errors and N+1 queries.
- When does EntityChangedEvent fire? On entity changes; with @TransactionalEventListener it’s handled after the transaction commits.
Summary#
DataManager.load(...).query(...).parameter(...).list()/one()/optional(), pluscreate,save,remove.- Fetch plans load exactly the graph you need.
- DataManager is security-aware; use UnconstrainedDataManager deliberately.
- Entity events centralise normalisation and side effects.
Next: securing the app with roles.
Quick quiz
3 questions. Check what stuck and earn 2 XP for each right answer.
Progress is saved in this browser. Create a free account to keep it on every device.
Get new lessons in your inbox
One email when we publish a new tutorial or guide. No spam, unsubscribe in one click.