This project uses Spring Data MongoDB to persist documents in MongoDB.
spring-boot-starter-data-mongodb autoconfigures a MongoClient and a MongoTemplate;
repositories are proxy-generated at runtime from CrudRepository interfaces — no boilerplate DAO code.
An embedded MongoDB instance (Flapdoodle) is included on the classpath, allowing the app to run without a local MongoDB installation.
spring-boot-starter-data-mongodb is the only MongoDB-specific dependency. It pulls in the MongoDB Java driver and Spring Data MongoDB.de.flapdoodle.embed.mongo (the core embedder) and de.flapdoodle.embed.mongo.spring4x (the Spring Boot 4.x integration). Neither is scoped to test here — the embedded instance runs in both main and test contexts. The embedded MongoDB version is pinned separately via de.flapdoodle.mongodb.embedded.version in application.properties.spring-boot-h2console dependencies appear twice in the pom — duplicate declarations with no effect, likely a copy-paste artifact. H2 is not used in this project.<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo</artifactId>
<version>4.24.0</version>
</dependency>
<dependency>
<groupId>de.flapdoodle.embed</groupId>
<artifactId>de.flapdoodle.embed.mongo.spring4x</artifactId>
<version>4.24.0</version>
</dependency>
spring.mongodb.* properties configure the real MongoDB connection. When Flapdoodle is on the classpath, Spring Boot autoconfiguration detects it and overrides these properties to point to the embedded instance instead — the host/port values are effectively ignored at runtime.de.flapdoodle.mongodb.embedded.version=7.0.2 pins the MongoDB binary version that Flapdoodle downloads and runs. Without this, Flapdoodle uses its own default version which may not match the target production MongoDB version.logging.level.org.springframework.jdbc.core.JdbcTemplate=debug is present but has no effect — there is no JDBC or JdbcTemplate in this project. Likely a leftover from another project.spring.mongodb.host=localhost spring.mongodb.port=27017 spring.mongodb.database=mtitek-spring-mongodb de.flapdoodle.mongodb.embedded.version=7.0.2
@Document(collection = "appProfiles") maps the class to a MongoDB collection. Without the collection attribute, Spring Data MongoDB defaults the collection name to the lowercased class name (appProfile).@Id maps to MongoDB's _id field. Using String for the ID type means MongoDB stores the value as provided — no auto-generation unless the field is null at save time, in which case MongoDB generates an ObjectId and Spring Data writes it back to the field.Role enum is stored as its string name by default — Spring Data MongoDB's default enum codec serializes enums by name, not ordinal.@Document(collection = "appProfiles")
@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
public class AppProfile {
@Id
private String id;
private String name;
private Role role;
public enum Role {
USER, ADMIN, SUPPORT;
}
}
AppUser embeds a List<AppProfile> directly — no join collection, no references. MongoDB stores the full AppProfile documents as a nested array inside each appUsers document. This is embedding, not referencing — updates to an AppProfile in the appProfiles collection do not propagate to embedded copies in appUsers.@NoArgsConstructor(force = true) without access = PRIVATE generates a public no-arg constructor. Spring Data MongoDB requires a no-arg constructor for deserialization.@Size(min = 1) on appProfiles is a Bean Validation constraint enforced at the controller layer via @Valid, not at the persistence layer — MongoDB will persist an empty list without error.@Document(collection = "appUsers")
@Data
@AllArgsConstructor
@NoArgsConstructor(force = true)
public class AppUser {
@Id
private String id;
@NotNull
@Size(min = 3, message = "Name must be at least 3 characters long")
private String name;
@Size(min = 1, message = "You must choose at least 1 appProfile")
private List<AppProfile> appProfiles = new ArrayList<>();
public void addAppProfile(AppProfile appProfile) {
this.appProfiles.add(appProfile);
}
}
CrudRepository<T, String> — the ID type is String, matching the @Id String id in each document class. Spring Data MongoDB generates the implementation at runtime.findAll(), findById(), and save() from CrudRepository cover all usage in this project.public interface AppProfileRepository extends CrudRepository<AppProfile, String> {}
public interface AppUserRepository extends CrudRepository<AppUser, String> {}
repo.save(new AppProfile("1", ...)) with a non-null id performs an upsert — if a document with _id: "1" already exists, it is replaced. This means restarting the app does not produce duplicate documents for the seeded profiles.@Bean
public CommandLineRunner saveAppProfiles(AppProfileRepository repo) {
return new CommandLineRunner() {
@Override
public void run(String... args) throws Exception {
repo.save(new AppProfile("1", "AppProfile 1 USER", Role.USER));
repo.save(new AppProfile("2", "AppProfile 2 ADMIN", Role.ADMIN));
repo.save(new AppProfile("3", "AppProfile 3 SUPPORT", Role.SUPPORT));
}
};
}
@ExceptionHandler(IllegalArgumentException.class) is declared at the controller level — it handles exceptions thrown within any handler method of this controller only, not globally. It catches the IllegalArgumentException that would propagate if appProfileRepository.findById() or any related logic throws one, renders the form again with an error message.filterByRole uses StreamSupport.stream(appProfiles.spliterator(), false) to bridge Iterable (returned by CrudRepository.findAll()) to a stream — Iterable has no direct stream() method. The full profile list is fetched once and filtered in memory per role.@ModelAttribute(name = "appUser") factory method initializes a new AppUser only when one is not already present in the session. Once @SessionAttributes("appUser") has stored it, Spring binds the session object on subsequent requests without calling the factory method again.appUserRepository.save(appUser) persists the full AppUser document including the embedded appProfiles list. The embedded profiles are stored as subdocuments — they are copies of the data at save time, not references to the appProfiles collection.sessionStatus.setComplete() clears the appUser session attribute immediately after save. Without this, the accumulated AppUser (with its profile list) persists in the HTTP session and is reused on the next form visit.th:each="role : ${T(com.mtitek.spring.model.AppProfile.Role).values()}" in appProfileForm.html calls the static values() method on the enum directly from the template using Thymeleaf's T()
${#ctx.getVariable(role.toString().toLowerCase())} resolves the model attribute dynamically by name at render time — the controller adds "user", "admin", and "support" as separate model attributes, and the template looks them up by the lowercased role name.appUserForm.html, *{appProfiles} and *{name} are selection variable expressions — shorthand for ${appUser.appProfiles} and ${appUser.name} because the form declares th:object="${appUser}". th:errors="*{appProfiles}" renders the @Size validation message when the list is empty.