Spring Data MongoDB - Audit issue with unique index - java

If the createdBy references to a document with unique indexes, it fails throwing dup key error.
AbstractDocument.java
public abstract class AbstractDocument implements Auditable<User, String> {
#Version
private Long version;
#Id
private String id;
private User createdBy;
private DateTime createdDate;
private User lastModifiedBy;
private DateTime lastModifiedDate;
}
User.java
#Document(collection = "users")
public class User extends AbstractDocument {
private String name;
private String surname;
#Indexed(unique = true)
private String username;
}
Book.java
#Document(collection = "books")
public Book extends AbstractDocument {
private String title;
}
Now, I have a script (Spring Batch) which initializes the db with some books. The script defines the auditor this way:
#Configuration
#EnableMongoAuditing
public class MongoConfig {
#Bean
public AuditorAware<User> auditorProvider() {
return new AuditorAware<User>() {
#Override
public User getCurrentAuditor() {
User auditor = new User();
auditor.setUsername("init-batch");
auditor.setName("Data initializer");
auditor.setSurname("Data initializer");
return auditor;
}
};
}
}
The script in somewhere does (for each book I need to persist) bookRepository.save(book)
The first book is persisted, but the second one throws:
nested exception is com.mongodb.DuplicateKeyException: Write failed with error code 11000 and error message 'E11000 duplicate key error index: mydb.books.$createdBy.username dup key: { : "init-batch" }'
Why? The unique index is for users collection, why is it checked for audit references?

Related

spring boot mongodb audit gives duplicate collection error

spring boot mongodb audit gives duplicate collection error. I'm trying to create dateCreate and dateUpdate fields when I insert and update a collection but when updated it gives the error:
org.springframework.dao.DuplicateKeyException: Write operation error
on server user.domain.com:27017. Write error: WriteError{code=11000,
message='E11000 duplicate key error collection: springboot.category
index: id dup key: { _id: "21" }', details={}}.
the execution is duplicating the key, below is my structure
My class AuditingConfig.java:
Configuration
#EnableMongoAuditing
public class AuditingConfig {
#Bean
public AuditorAware<String> myAuditorProvider() {
return new AuditorAwareImpl();
}
}
My class AuditMetadata.java:
#Setter
#Getter
public class AuditMetadata {
#CreatedDate
private LocalDateTime createdDate;
#LastModifiedDate
private LocalDateTime lastModifiedDate;
#Version
private Long version;
// #CreatedBy
// private String createdByUser;
// #LastModifiedBy
// private String modifiedByUser;
//...getters and setters omitted
}
My class AuditorAwareImpl.java:
public class AuditorAwareImpl implements AuditorAware<String> {
#Override
public Optional<String> getCurrentAuditor() {
return Optional.of("Admin");
}
}
My class entity CategoryMongo.java:
Getter
#Setter
#NoArgsConstructor
#Document(collection = "category")
public class CategoryMongo extends AuditMetadata{
#Id
#JsonProperty("category_id")
private String category_id;
#JsonProperty("id_colletion")
private String emberId;
public String getEmberId() {
return category_id;
}
#JsonProperty("category_name")
private String name;
#JsonProperty("category_active")
private ProductEnum active = ProductEnum.ativo;
#JsonProperty("category_slug")
private String slug;
// #JsonProperty("category_updateAt")
// #LastModifiedDate
// private Date updateAt;
// #JsonProperty("category_createdAt")
// #CreatedDate
// private Date createdAt;
}
My method save:
CategoryMongo catm = new CategoryMongo();
catm.setName(category.getName());
catm.setSlug(category.getSlug());
catm.setActive(category.getActive());
catm.setCategory_id(category.getCategory_id().toString());
catm.setEmberId(category.getCategory_id().toString());
categoryRepositoryMongo.save(catm);
SOLVED
I solved the error I use in the document an interface follows the updated classes:
class AuditMetadata.java:
#Setter
#Getter
public class AuditMetadata {
#CreatedDate
private LocalDateTime createdDate;
#LastModifiedDate
private LocalDateTime lastModifiedDate;
#Version
private Long version;
protected boolean persisted;
// #CreatedBy
// private String createdByUser;
// #LastModifiedBy
// private String modifiedByUser;
//...getters and setters omitted
}
class Document CategoryMongo.java:
#Getter
#Setter
#NoArgsConstructor
#Document(collection = "category")
public class CategoryMongo extends AuditMetadata implements Persistable<String>{
#Id
#JsonProperty("category_id")
private String category_id;
#JsonProperty("id_colletion")
private String emberId;
public String getEmberId() {
return category_id;
}
#JsonProperty("category_name")
private String name;
#JsonProperty("category_active")
private ProductEnum active = ProductEnum.ativo;
#JsonProperty("category_slug")
private String slug;
#Override
#Nullable
public String getId() {
return category_id;
}
#Override
public boolean isNew() {
return !persisted;
}
// #JsonProperty("category_updateAt")
// #LastModifiedDate
// private Date updateAt;
// #JsonProperty("category_createdAt")
// #CreatedDate
// private Date createdAt;
}
save method:
CategoryMongo catm = new CategoryMongo();
catm.setName(category.getName());
catm.setSlug(category.getSlug());
catm.setActive(category.getActive());
catm.setCategory_id(category.getCategory_id().toString());
catm.setPersisted(true);
categoryRepositoryMongo.save(catm);
But something happens that I didn't want to happen: the #CreatedDate field disappears when I update it and only #LastModifiedDate appears in the result. if anyone knows how to solve this post here

Calling methods in two different ReactiveMongoRepository's in a transaction using Spring Data MongoDB?

When using the reactive programming model with Spring Data MongoDB it's possible to execute transactions like this:
Mono<DeleteResult> result = template.inTransaction()
.execute(action -> action.remove(query(where("id").is("step-1")), Step.class));
But Spring Data MongoDB also has support for "reactive repositories", for example:
public interface PersonRepository extends ReactiveMongoRepository<Person, String>
Flux<Person> findByLocationNear(Point location, Distance distance);
}
and
public interface CarRepository extends ReactiveMongoRepository<Car, String>
Flux<Car> findByYear(int year);
}
My question is, given that you have ReactiveMongoRepository's, can you somehow leverage MongoDB transactions and e.g. insert both a Person and Car in the same transaction (using PersonRepository and CarRepository in the case)? If so, how do you do this?
I had also been trying hard to find solution for the Transactional support in Reactive style of Mongo DB & Spring Boot
But luckily I figured it myself. Though few of the things from google were also helpful but those were non reactive.
Important Note - For Spring boot 2.2.x it works well, but with spring boot 2.3.x it has some other issues, it has internal re-write & changes all together
You need to use ReactiveMongoTransactionManager along with ReactiveMongoDatabaseFactory, most of the details at the end, also sharing the code repo for the same
For getting the mongo db to support the Transactions we need to make sure that the DB should be running in replica mode.
Why we need that? Because you will get some error like this otherwise:-
Sessions are not supported by the MongoDB cluster to which this client is connected
The instructions for the same are below:-
run the docker-compose based mongo db server using docker-compose.yml as shared below:-
version: "3"
services:
mongo:
hostname: mongo
container_name: localmongo_docker
image: mongo
expose:
- 27017
ports:
- 27017:27017
restart: always
entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "rs0" ]
volumes:
- ./mongodata:/data/db # need to create a docker volume named as mongodata first
After the image comes up, execute the command(here localmongo_docker is the name of the container):-
docker exec -it localmongo_docker mongo
Copy and paste the command below and execute that
rs.initiate(
{
_id : 'rs0',
members: [
{ _id : 0, host : "mongo:27017" }
]
}
)
And then exit the execution by entering exit
Important - The code repo can be found here on my github - https://github.com/krnbr/mongo-spring-boot-template
Important notes for the code are as below:-
MongoConfiguration class in the config package is the important part to make the transactions working, link to the configuration class is here
Main part is the Bean
#Bean
ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
return new ReactiveMongoTransactionManager(dbFactory);
}
For checking the working of the code's Transactional requirement you may go through the class UserService in service package here
Code shared in case the links do not work for someone:-
The Configuration and inside the Beans
#Configuration
public class MongoConfiguration extends AbstractMongoClientConfiguration {
#Autowired
private MongoProperties mongoProperties;
#Bean
ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory dbFactory) {
return new ReactiveMongoTransactionManager(dbFactory);
}
#Override
protected String getDatabaseName() {
return mongoProperties.getDatabase();
}
#Override
public MongoClient mongoClient() {
return MongoClients.create(mongoProperties.getUri());
}
}
application.properties (related to mongo db)
spring.data.mongodb.database=mongo
spring.data.mongodb.uri=mongodb://localhost:27017/mongo?replicaSet=rs0
Document Classes
Role Class
#Getter
#Setter
#Accessors(chain = true)
#Document(collection = "roles")
#TypeAlias("role")
public class Role implements Persistable<String> {
#Id
private String id;
#Field("role_name")
#Indexed(unique = true)
private String role;
#CreatedDate
private ZonedDateTime created;
#LastModifiedDate
private ZonedDateTime updated;
private Boolean deleted;
private Boolean enabled;
#Override
#JsonIgnore
public boolean isNew() {
if(getCreated() == null)
return true;
else
return false;
}
}
User Class
#Getter
#Setter
#Accessors(chain = true)
#Document(collection = "users")
#JsonInclude(JsonInclude.Include.NON_NULL)
#TypeAlias("user")
public class User implements Persistable<String> {
#Id()
private String id;
#Field("username")
#Indexed(unique = true)
#JsonProperty("username")
private String userName;
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
#CreatedDate
private ZonedDateTime created;
#LastModifiedDate
private ZonedDateTime updated;
private Boolean deleted;
private Boolean enabled;
#DBRef(lazy = true)
#JsonProperty("roles")
private List<Role> roles = new ArrayList();
#Override
#JsonIgnore
public boolean isNew() {
if(getCreated() == null)
return true;
else
return false;
}
}
UserProfile Class
#Getter
#Setter
#Accessors(chain = true)
#Document(collection = "user_profiles")
#JsonInclude(JsonInclude.Include.NON_NULL)
#TypeAlias("user_profile")
public class UserProfile implements Persistable<String> {
#Id
private String id;
#Indexed(unique = true)
private String mobile;
#Indexed(unique = true)
private String email;
private String address;
private String firstName;
private String lastName;
#DBRef
private User user;
#CreatedDate
private ZonedDateTime created;
#LastModifiedDate
private ZonedDateTime updated;
private Boolean deleted;
private Boolean enabled;
#Override
#JsonIgnore
public boolean isNew() {
if(getCreated() == null)
return true;
else
return false;
}
}
ReactiveMongoRepository Interface(s)
RoleRepository
public interface RoleRepository extends ReactiveMongoRepository<Role, String> {
Mono<Role> findByRole(String role);
Flux<Role> findAllByRoleIn(List<String> roles);
}
UserRepository
public interface UserRepository extends ReactiveMongoRepository<User, String> {
Mono<User> findByUserName(String userName);
}
UserProfileRepository
public interface UserProfileRepository extends ReactiveMongoRepository<UserProfile, String> {
}
The User Service Class Need to create your own RuntimeException Class here, here it is AppRuntimeException Class, I had been using
#Slf4j
#Service
public class UserService {
#Autowired
private RoleRepository roleRepository;
#Autowired
private UserRepository userRepository;
#Autowired
private UserProfileRepository userProfileRepository;
#Transactional
public Mono<UserProfile> saveUserAndItsProfile(final UserRequest userRequest) {
Mono<Role> roleMono = roleRepository.findByRole("USER");
Mono<User> userMono = roleMono.flatMap(r -> {
User user = new User()
.setUserName(userRequest.getUsername())
.setPassword(userRequest.getPassword());
user.setRoles(Arrays.asList(r));
return userRepository.save(user);
}).onErrorResume(ex -> {
log.error(ex.getMessage());
if(ex instanceof DuplicateKeyException) {
String errorMessage = "The user with the username '"+userRequest.getUsername()+"' already exists";
log.error(errorMessage);
return Mono.error(new AppRuntimeException(errorMessage, ErrorCodes.CONFLICT, ex));
}
return Mono.error(new AppRuntimeException(ex.getMessage(), ErrorCodes.INTERNAL_SERVER_ERROR, ex));
});
Mono<UserProfile> userProfileMono = userMono.flatMap(u -> {
UserProfile userProfile = new UserProfile()
.setAddress(userRequest.getAddress())
.setEmail(userRequest.getEmail())
.setMobile(userRequest.getMobile())
.setUser(u);
return userProfileRepository.save(userProfile);
}).onErrorResume(ex -> {
log.error(ex.getMessage());
if(ex instanceof DuplicateKeyException) {
String errorMessage = "The user with the profile mobile'"+userRequest.getMobile()+"' and/or - email '"+userRequest.getEmail()+"' already exists";
log.error(errorMessage);
return Mono.error(new AppRuntimeException(errorMessage, ErrorCodes.CONFLICT, ex));
}
return Mono.error(new AppRuntimeException(ex.getMessage(), ErrorCodes.INTERNAL_SERVER_ERROR, ex));
});
return userProfileMono;
}
}
Controller and the Model Class
UserRequest Model Class
#Getter
#Setter
#Accessors(chain = true)
#Slf4j
#JsonInclude(JsonInclude.Include.NON_NULL)
public class UserRequest {
private String username;
private String password;
private String mobile;
private String email;
private String address;
private String firstName;
private String lastName;
}
UserProfileApisController class
#Slf4j
#RestController
#RequestMapping("/apis/user/profile")
public class UserProfileApisController {
#Autowired
private UserService userService;
#PostMapping
public Mono<UserProfile> saveUserProfile(final #RequestBody UserRequest userRequest) {
return userService.saveUserAndItsProfile(userRequest);
}
}
Just an addition to the accepted answer regarding MongoDB replica set initialization.
If one needs a non-fixed port single replica set for testing they might use the Testcontainers’ MongoDB Module that encapsulates such initialization:
final MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.2.8");
We can start it via ‘mongoDBContainer.start()’ and stop it via try-with-resources or ‘mongoDBContainer.stop()’. See more details on this module and Spring Data MongoDB here.
If one needs a non-fixed port multi-node replica set for testing complex production issues, they might use this project, for example:
try (
//create a PSA mongoDbReplicaSet and auto-close it afterwards
final MongoDbReplicaSet mongoDbReplicaSet = MongoDbReplicaSet.builder()
//with 2 working nodes
.replicaSetNumber(2)
//with an arbiter node
.addArbiter(true)
//create a proxy for each node to simulate network partitioning
.addToxiproxy(true)
.build()
) {
//start it
mongoDbReplicaSet.start();
assertNotNull(mongoDbReplicaSet.getReplicaSetUrl());
//do some testing
}

Spring HATEOAS with nested resources and JsonView filtering

I am trying to add HATEOAS links with Resource<>, while also filtering with #JsonView. However, I don't know how to add the links to nested objects.
In the project on on Github, I've expanded on this project (adding in the open pull request to make it work without nested resources), adding the "Character" entity which has a nested User.
When accessing the ~/characters/resource-filtered route, it is expected that the nested User "player" appear with the firstNm and bioDetails fields, and with Spring generated links to itself, but without the userId and lastNm fields.
I have the filtering working correctly, but I cannot find an example of nested resources which fits with the ResourceAssembler paradigm. It appears to be necessary to use a ResourceAssembler to make #JsonView work.
Any help reconciling these two concepts would be appreciated. If you can crack it entirely, consider sending me a pull request.
User.java
//package and imports
...
public class User implements Serializable {
#JsonView(UserView.Detail.class)
private Long userId;
#JsonView({ UserView.Summary.class, CharacterView.Summary.class })
private String bioDetails;
#JsonView({ UserView.Summary.class, CharacterView.Summary.class })
private String firstNm;
#JsonView({ UserView.Detail.class, CharacterView.Detail.class })
private String lastNm;
public User(Long userId, String firstNm, String lastNm) {
this.userId = userId;
this.firstNm = firstNm;
this.lastNm = lastNm;
}
public User(Long userId) {
this.userId = userId;
}
...
// getters and setters
...
}
CharacterModel.java
//package and imports
...
#Entity
public class CharacterModel implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#JsonView(CharacterView.Summary.class)
private Long characterId;
#JsonView(CharacterView.Detail.class)
private String biography;
#JsonView(CharacterView.Summary.class)
private String name;
#JsonView(CharacterView.Summary.class)
private User player;
public CharacterModel(Long characterId, String name, String biography, User player) {
this.characterId = characterId;
this.name = name;
this.biography = biography;
this.player = player;
}
public CharacterModel(Long characterId) {
this.characterId = characterId;
}
...
// getters and setters
...
}
CharacterController.java
//package and imports
...
#RestController
#RequestMapping("/characters")
public class CharacterController {
#Autowired
private CharacterResourceAssembler characterResourceAssembler;
...
#JsonView(CharacterView.Summary.class)
#RequestMapping(value = "/resource-filtered", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.OK)
public Resource<CharacterModel> getFilteredCharacterWithResource() {
CharacterModel model = new CharacterModel(1L, "TEST NAME", "TEST BIOGRAPHY", new User(1L, "Fred", "Flintstone"));
return characterResourceAssembler.toResource(model);
}
...
}
CharacterResourceAssembler.java
//package and imports
...
#Component
public class CharacterResourceAssembler implements ResourceAssembler<CharacterModel, Resource<CharacterModel>>{
#Override
public Resource<CharacterModel> toResource(CharacterModel user) {
Resource<CharacterModel> resource = new Resource<CharacterModel>(user);
resource.add(linkTo(CharacterController.class).withSelfRel());
return resource;
}
}

Spring-data-mongodb not persist multiple objects on list

I am using Spring-data-mongodb and i can persist an object on a list, but when i try to add another, it doesn't work, the application doesn't throw an exception.
this is my Json:
[
{
idUser: "4a9f10d9-e19f-42af-ba00-891a567cc41f",
login: "peter",
password: "mypassword",
email: "peter#eeee.com",
patients:
[
{
idPatient: "d31e8052-36d3-4285-9f97-454f3437812d",
name: "ada",
birthday: 1363474800000,
idUser: "4a9f10d9-e19f-42af-ba00-891a567cc41f",
region:
{
idRegion: "d8acfa45-486e-49e0-b4e6-edde6743cf30",
name: "Madrid"
},
personalCalendars: null
},
null
]
}
]
As you can see, my first Patient element is correctly, and the second was insert as null.
I leave my code:
User.java
#Document(collection = "users")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private String id;
#Indexed
private UUID idUser;
#Indexed(unique = true)
private String login;
private String password;
#Indexed(unique = true)
private String email;
#DBRef
private List<Patient> patients;
#PersistenceConstructor
public User(UUID idUser, String login, String password, String email, List<Patient> patients){
this.idUser = idUser;
this.login = login;
this.password = password;
this.email = email;
this.patients = patients;
}
Patient.java
#Document(collection = "patients")
public class Patient implements Serializable {
private static final long serialVersionUID = 1L;
#Id
private String id;
#Indexed
private UUID idPatient;
private String name;
private Date birthday;
private UUID idUser;
private Region region;
#Transient
private List<PersonalCalendar> personalCalendars;
#PersistenceConstructor
public Patient(UUID idPatient, String name, Date birthday,UUID idUser, Region region){
this.idPatient = idPatient;
this.name = name;
this.birthday = birthday;
this.idUser = idUser;
this.region = region;
}
and the DAO whereI do the insert.
#Override
public Patient createPatient(User user, Patient patient) {
this.mongoOps.save(patient , "patients");
this.mongoOps.save(user , "users");
return this.getPatientById(patient.getIdPatient());
}
The console returns this, but no persists the patient:
15:16:16.718 [tomcat-http--6] DEBUG o.s.data.mongodb.core.MongoTemplate - Saving DBObject containing fields: [_class, _id, idPatient, name, birthday, idUser, region]
15:16:16.723 [tomcat-http--6] DEBUG o.s.data.mongodb.core.MongoDbUtils - Getting Mongo Database name=[application]
15:16:16.747 [tomcat-http--6] DEBUG org.mongodb.driver.protocol.insert - Inserting 1 documents into namespace application.patients on connection [connectionId{localValue:2, serverValue:119}] to server 127.0.0.1:27017
15:16:16.761 [tomcat-http--6] DEBUG org.mongodb.driver.protocol.insert - Insert completed
I need help.
Thanks a lot
First, if you use Spring Data with MongoDB, use it properly:
#Repository
public interface UserRepository extends MongoRepository<User, String> {
}
Now just inject UserRepository via #Autowired annotation:
#Autowired
private UserRepository userRepository;
User user = new User();
Patient patient = new Patient();
user.addPatient(patient);
// Just call save from userRepository to save your User with Patient.
// save method will return instance of saved user (together with instance of
// patient)
User user = userRepository.save(user);
Note that save method can also be used for updating of existing User. If User is new (not having generated id) it will be inserted. If user exists (has generated id) it will be just updated.
Presuming that User class has a addPatient method that looks like this:
public void addPatient(Patient patient) {
this.patients.add(patient);
}
Also, make sure that your list is initialized: List<Patient> patients = new ArrayList<>();

JPA2: how to persist data using #MapKeyColumn

In my example I have two entities to persist. But when I try to using #MapKeyColumn annotation , I am confused on how to use this annotation and how to persist the data. This is the example that is explained in the "PRO JPA2 book". When I try to run this code it will generate the exception.
two entities:
#Entity
#Access(AccessType.FIELD)
public class Employee {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
private String name;
private String salary;
#Enumerated(EnumType.STRING)
private EmployeeType type;
#ManyToOne
#JoinColumn(name="dept_id")
private Department deparment;
and
#Entity
public class Department {
#Id
#Column(name="dept_id")
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
private String Name;
#OneToMany(mappedBy="deparment")
#MapKeyColumn(name="cabin_id")
private Map<String , Employee> empCabin;
......................................
public Map<String, Employee> getEmpCabin() {
return empCabin;
}
public void assignEmpToCabin(Employee emp , String cabinNo) {
System.out.println(emp+" "+cabinNo);
getEmpCabin().put(cabinNo, emp);
if(emp.getDeparment() != null){
emp.getDeparment().removeEmpFromDept(emp);
}
emp.setDeparment(this);
}
public void removeEmpFromDept(Employee emp) {
Iterator<Map.Entry<String, Employee>> entrySet = getEmpCabin().entrySet().iterator();
if(entrySet.hasNext()){
Employee current = ((Map.Entry<String, Employee>) entrySet.next()).getValue();
if(current.getId() == emp.getId()){
System.out.println("dept remove");
entrySet.remove();
current.setDeparment(null);
}
}
}
my service class function
public class EmployeeService {
private EntityManager em;
.............
public void setEmpCabin(Employee emp , Department dept , String cab_id) {
dept.assignEmpToCabin(emp, cab_id);
}
My main code :
tr.begin();
Department dept = deps.createDepartment("Sci");
Employee emp = es.createEmployee("Harmeet Singh", "500" , "5684", dept);
es.setEmpCabin(emp, dept, "10");
tr.commit();
when i try to insert the data using setEmpCabin(...) function , it wil generate the exception
Caused by: org.hibernate.exception.GenericJDBCException: Field 'cabin_id' doesn't have a default value

Categories