I have a question about Cascading in MongoDB;
So. My project is based on Java 10 and Spring Boot 2.0.5 and Lombok.
I've created CascadeSave event listener and here is it
public class CascadeSaveMongoEventListener extends AbstractMongoEventListener {
#Autowired
private MongoOperations mongoOperations;
#Override
public void onBeforeConvert(BeforeConvertEvent event) {
Object source = event.getSource();
ReflectionUtils.doWithFields(source.getClass(), field -> {
ReflectionUtils.makeAccessible(field);
if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(CascadeSave.class)) {
Object fieldValue = field.get(source);
if (fieldValue instanceof Collection<?>) {
Collection collection = (Collection<?>) fieldValue;
for (Object o : collection) {
mongoOperations.save(o);
}
} else {
mongoOperations.save(fieldValue);
}
}
});
}
}
Event listener is also included in mongo configuration like this:
#Bean
public CascadeSaveMongoEventListener cascadingMongoEventListener() {
return new CascadeSaveMongoEventListener();
}
And this how domain class looks like:
#Data
#AllArgsConstructor
#NoArgsConstructor
#Document
public class Article {
#Id
private String id;
#DBRef(lazy = true)
#CascadeSave
private List<Comment> comments;
}
#Data
#AllArgsConstructor
#NoArgsConstructor
#Document
public class Comment {
#Id
private String id;
private String text;
}
The problem is that after saving I've take a look at DB via Mongo Compass and Article.comments is collection of null's, but the comments are saved correctly in separate collection. What is the problem? Thanks!
Related
I had a class like:
public class EmailAddress {
public String value;
public String tld() {...}
public String host() {...}
public String mailbox() {...}
}
Now I use this class in an Object / Entity:
#Entity
public class Customer {
public String name;
public EmailAddress mail;
}
Now, when I do a rest service for Customer, I get this format:
{
"id": 1,
"name": "Test",
"email": {
"value": "test#test.de"
}
}
But I only want "email": "test#test.de"
{
"id": 1,
"name": "Test",
"email": "test#test.de"
}
What I must do? I use Spring Boot and Hibernate Entities.
Thank you for any support
You should use DTO class in request handling and make mappings from DTO to Entity and backwards, e.g.:
public class CustomerDTO {
private Integer id;
private String name;
private String email;
}
You should use DataTransferObjects for your (REST) APIs.
The DTOs only contain the fields the interface should provide (or receive).
When receiving objects from the client and before returning the object from your Controller you can convert the DTOs to your domain model (Which could be your JPA entites classes).
Example for a controller method. We assume you get an object from an user-editor which contains all data you want to update in your database-objects and return the updated company DTO:
#PutMapping
public CustomerDto updateCustomer(CustomerEditorDto updatedCustomerDto) {
Customer updatedCustomer = CustomerConverter.convert(updatedCustomerDto);
updatedCustomer = customerService.updateCustomer(updatedCustomer);
return CustomerConverter.convert(updatedCustomer);
}
and your Converter class:
#NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CustomerConverter {
public static CustomerDto convert(Customer customer) {
CustomerDto result = null;
if (customer != null) {
// TODO: set fields in result-dto
}
return result;
}
public static Customer convert(CustomerEditorDto customer) {
Customer result = null;
if (customer != null) {
// TODO set fields in result;
}
return result;
}
}
and here are the DTOs
#Getter
#Setter
public class CustomerDto {
private Integer id;
private String name;
private String email;
}
#Getter
#Setter
public class CustomerEditorDto {
private Integer id;
private String firstName;
private String lastName;
private String email;
private String otherPropertyOrStuff;
}
This way you can separate the API modell from your JPA entites. You can use the same models for input/output. And you can even use a different model to work with inside your services and the finally convert them into your JPA entites, before persisting the data (or after reading the data).
There are tools which can take care of the conversion, like mapstruct.
* The above annotations #Getter, #Setter, ... are from project lombok and very are handy to generate boiler-plate code automatically.
I found an other easier solution, use a JsonSerializer on the entity Property:
#JsonSerialize(using = EmailAddressSerializer.class)
private EmailAddress email;
The serializer class:
public class EmailAddressSerializer extends StdSerializer<EmailAddress> {
public EmailAddressSerializer() {
super(EmailAddress.class);
}
protected EmailAddressSerializer(Class<EmailAddress> t) {
super(t);
}
#Override
public void serialize(EmailAddress email,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeString(email.value);
}
}
I am currently setting up a Rest API server using Spring Boot (v2.5.5), Spring Data Couchbase (v4.2.5) and Couchbase (v6.6.1).
I get a really strange behavior when requesting
count() -> 0
findAll() -> []
Whereas
findById() is returning a result.
My entity:
{"mkey": { "keyContent": "AA", "mkeyStatus": "L" }, "sequences": [ { "direction": "B", "loc1Value": "NCE", "loc2Value": "NYC" } ] }
#Document #Data #AllArgsConstructor #NoArgsConstructor #EqualsAndHashCode public class AirlineProfile {
#Id private String id;
#Field private MKey mkey;
#Field private List<Sequence> sequences;
#EqualsAndHashCode #AllArgsConstructor #NoArgsConstructor #Data static class MKey {
#Field private String keyContent;
#Field private String mkeyStatus;
}
#EqualsAndHashCode #AllArgsConstructor #NoArgsConstructor #Data static class Sequence {
#Field private String loc1Value;
#Field private String loc2Value;
#Field private String direction;
}
}
My repository is extending the CrudRepository.
public interface AirlineProfileRepository extends CrudRepository<AirlineProfile, String> {}
While my Service is the following:
#Service #Qualifier("AirlineProfileServiceImpl") public class AirlineProfileServiceImpl
implements AirlineProfileService {
#Autowired private AirlineProfileRepository airlineProfileRepository;
#Override
public long count() {
return airlineProfileRepository.count();
}
#Override
public List<AirlineProfile> findAll() {
List<AirlineProfile> airlineProfiles = new ArrayList<>();
for (AirlineProfile airlineProfile : airlineProfileRepository.findAll()) {
airlineProfiles.add(airlineProfile);
}
return airlineProfiles;
}
#Override public AirlineProfile findById(String id) {
return airlineProfileRepository.findById(id).orElse(null);
}
}
And my controller the following:
#RestController #RequestMapping("/api") public class AirlineProfileController {
#Autowired AirlineProfileService airlineProfileService;
#GetMapping("/airlineprofile/count") public long count() {
System.out.println("Count");
return airlineProfileService.count();
}
#GetMapping("/airlineprofile/all") public List<AirlineProfile> getAllAirlineProfiles() {
System.out.println("Get all AirlineProfile");
return airlineProfileService.findAll();
}
#GetMapping("/airlineprofile/id={id}") public AirlineProfile getAirlineProfileById(#PathVariable String id) {
System.out.println("Get AirlineProfile for id = " + id);
return airlineProfileService.findById(id);
}
}
I do not know if I missed something at Server or Couchbase side ... :(
Thank you for your help!
Ok, found that:
public interface AirlineProfileRepository extends CrudRepository<AirlineProfile, String> {
#Query("#{#n1ql.selectEntity}")
List<AirlineProfile> findAll();
}
Is working ...
So, I am questioning myself about the usability of findAll() ...
I have an ExampleRequest entity that can optionally have one or more ExampleRequestYear. It's currently configured this way (unrelated fields and gettters/setters omitted for brevity, please let me know if you need anything else):
#Entity
#Table(name = "EXAMPLE_REQUEST")
#SequenceGenerator(name = "EXAMPLE_REQUEST_ID_SEQ", sequenceName = "EXAMPLE_REQUEST_ID_SEQ", allocationSize = 1)
#Cacheable(false)
public class ExampleRequest implements Serializable {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "EXAMPLE_REQUEST_ID_SEQ")
#Column(name="EXAMPLE_REQUEST_ID", nullable = false)
private Long exampleRequestId;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "exampleRequest")
private List<ExampleRequestYear> exampleRequestYearList;
public ExampleRequest() {
}
public List<ExampleRequestYear> getExampleRequestYearList() {
if(this.exampleRequestYearList == null){
this.exampleRequestYearList = new ArrayList<ExampleRequestYear>();
}
return this.exampleRequestYearList;
}
public void setExampleRequestYearList(List<ExampleRequestYear> exampleRequestYearList) {
this.exampleRequestYearList = exampleRequestYearList;
}
public ExampleRequestYear addExampleRequestYear(ExampleRequestYear exampleRequestYear) {
getExampleRequestYearList().add(exampleRequestYear);
exampleRequestYear.setExampleRequest(this);
return exampleRequestYear;
}
public ExampleRequestYear removeExampleRequestYear(ExampleRequestYear exampleRequestYear) {
getExampleRequestYearList().remove(exampleRequestYear);
exampleRequestYear.setExampleRequest(null);
return exampleRequestYear;
}
}
#Entity
#Table(name = "EXAMPLE_REQUEST_YEAR")
#IdClass(ExampleRequestYearPK.class)
public class ExampleRequestYear implements Serializable {
#Id
#Column(nullable = false)
private Integer year;
#Id
#ManyToOne
#JoinColumn(name = "EXAMPLE_REQUEST_ID", referencedColumnName = "EXAMPLE_REQUEST_ID")
private ExampleRequest exampleRequest;
public ExampleRequestYear() {
}
public void setExampleRequest(ExampleRequest exampleRequest) {
this.exampleRequest = exampleRequest;
}
public ExampleRequest getExampleRequest() {
return exampleRequest;
}
}
Part of the code was auto-generated by the IDE and I'm still wrapping my head around JPA so there're probably design mistakes all around.
My app works (apparently) when I create a new ExampleRequest:
ExampleRequest exampleRequest = new ExampleRequest();
ExampleRequestYear exampleRequestYear = new ExampleRequestYear(2020);
request.addExampleRequestYear(exampleRequestYear);
However, I can't figure out how to edit an existing ExampleRequest because I'm unsure on how I'm meant to retrieve the linked entities. According to articles I've read, lazy fetching should be automatic, yet when I try this:
ExampleRequest exampleRequest = employeeRequestsController.getExampleRequestById(123);
System.out.println(exampleRequest.getExampleRequestYearList().size());
... I get a null pointer exception upon .size() because the getter runs but neither initialises an empty list, nor retrieves items from DB:
public List<ExampleRequestYear> getExampleRequestYearList() {
if(this.exampleRequestYearList == null){
// Field is null and conditional is entered
this.exampleRequestYearList = new ArrayList<ExampleRequestYear>();
// After initialisation, field is still null!
}
return this.exampleRequestYearList;
}
Also, switch to FetchType.EAGER solves this particular problem entirely. What am I missing?
Further details regarding app design. The Resource classes that handle HTTP requests interact with a set of Controller classes like this:
#Stateless(name = "ISomeActionController", mappedName = "ISomeActionController")
public class SomeActionController implements ISomeActionController {
#EJB
private IFooDAO fooDao;
#EJB
private IBarDAO barDao;
#Override
public ExampleRequest getExampleRequestById(Long exampleRequestId) {
return fooDao.getEntityById(exampleRequestId);
}
}
It's in the DAO classes where EntityManager is injected an used:
#Local
public interface IGenericDAO<T> {
public T persistEntity(T o);
public T persistEntityCommit(T o);
public void removeEntity(T o);
public void removeEntity(long id);
public T mergeEntity(T o);
public List<T> getEntitiesFindAll();
public List<T> getEntitiesFindAllActive();
public T getEntityById(Object id);
}
public interface IFooDAO extends IGenericDAO<ExampleRequest> {
public void flushDAO();
public ExampleRequest getExampleRequestById(Long exampleRequestId);
}
#Stateless(name = "IFooDAO", mappedName = "IFooDAO")
public class FooDAO extends GenericDAO<ExampleRequest> implements IFooDAO {
public FooDAO() {
super(ExampleRequest.class);
}
#Override
public void flushDAO(){
em.flush();
}
#Override
public ExampleRequest getExampleRequestById(Long exampleRequestId){
String sql = "...";
Query query = em.createNativeQuery(sql, ExampleRequest.class);
//...
}
}
I'm trying to map source object which property is set to null to destination object of which this property is set to another object.
Expected result would be that property of destination object will be null after mapping. Instead of that, this property is set to an object and all of its properties are set to null.
Here is an example:
public class ModelMapperTest {
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
User user = new User();
user.setStatus(null);
StatusDto statusDto = new StatusDto();
statusDto.setId(1);
statusDto.setName("Active");
UserDto userDto = new UserDto();
userDto.setStatus(statusDto);
// user.status=null, userDto.status=StatusDto(id=1, name="Active")
modelMapper.map(user, userDto);
System.out.println("user = " + user);
System.out.println("userDto = " + userDto);
}
#Getter
#Setter
#ToString
public static class User {
private Status status;
}
#Getter
#Setter
#ToString
public static class Status {
private Integer id;
private String name;
}
#Getter
#Setter
#ToString
public static class UserDto {
private StatusDto status;
}
#Getter
#Setter
#ToString
public static class StatusDto {
private Integer id;
private String name;
}
}
Output:
user = ModelMapperTest.User(status=null)
userDto = ModelMapperTest.UserDto(status=ModelMapperTest.StatusDto(id=null, name=null))
Is it possible to somehow configure model mapper to sets UserDto.status to null?
I know this is an older question and you seem to have moved on to a different library, but I had the same problem recently and came up with this solution (building on your example):
Converter<?, ?> preserveNullConverter = (context) ->
context.getSource() == null
? null
: modelMapper.map(context.getSource(), context.getDestinationType());
modelMapper.createTypeMap(User.class, UserDto.class)
.addMappings(mapper -> mapper.using(preserveNullConverter).map(User::getStatus, UserDto::setStatus));
It's not ideal because the .addMappings part needs to be done for every property where the issue occurs, but at least the Converter can be reused.
Signed in user creates a project. I would like to update createdBy field of ProjectModel before persist to DB.
#Entity
#Table(name = "PROJECTS")
#Inheritance(strategy = InheritanceType.JOINED)
public #Data class ProjectModel extends BaseModel {
#Length(min = 4, max = 30)
#NotNull
private String name;
#NotNull
#ManyToOne(fetch = FetchType.EAGER)
#JoinColumn(name = "USER_ID")
private User createdBy;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "PROJECT_ID")
private List<RinexFileModel> rinexFileModels;
#Enumerated(EnumType.STRING)
private Status status = Status.NEW;
public void addRinexFiles(List<RinexFileModel> rinexFiles) {
this.rinexFileModels.addAll(rinexFiles);
}
public void addRinexFile(RinexFileModel rinexFile) {
this.rinexFileModels.add(rinexFile);
}
public enum Status {
NEW, READY, PROCESSING, PROCESSED
}
#Override
public String toString() {
return ToStringBuilder.reflectionToString(this);
}
}
There are at least two options to do it.
By callback methods (#PrePersist, etc.) Similar to code below but I need UserModel and access to spring bean
similar to this SpringContext example
This approach I don't like because of the single responsibility principle has been broken I think so.
Interceptor (EmptyInterceptor)
HibernateInterceptorImpl Interceptor
public class HibernateInterceptorImpl extends EmptyInterceptor {
#Resource
private HibernateInterceptorStrategy hibernateInterceptorStrategy;
#Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) throws CallbackException {
boolean entityChanged = false;
final EmptyInterceptor interceptor = hibernateInterceptorStrategy.getStrategy(entity);
if (nonNull(interceptor)) {
entityChanged = interceptor.onSave(entity, id, state, propertyNames, types);
}
return entityChanged;
}
}
#Component("hibernateInterceptorStrategy")
public class HibernateInterceptorStrategy {
#Resource
private Map<Class, EmptyInterceptor> hibernateInterceptorStrategies;
public EmptyInterceptor getStrategy(Object object) {
for (Class clazz : hibernateInterceptorStrategies.keySet()) {
if (object.getClass() == clazz) {
return hibernateInterceptorStrategies.get(clazz);
}
}
return null;
}
}
The ProjectInterceptor implemenation
public class ProjectInterceptorImpl extends EmptyInterceptor {
#Resource
private SessionService sessionService;
#Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) throws CallbackException {
final ProjectModel project = (ProjectModel) entity;
if (isNull(project.getCreatedBy())) {
project.setCreatedBy(sessionService.getCurrentUser());
}
return true;
}
}
Debug results (User successfully added, but nevertheless exception on #NotNull validation for createdBy)
FIX IS: modify state array as well. But don't have a good solution for this.
To populate user data before model saves could be used Spring Data #CreateBy annotation.
Check this article first
User + embedded class UserPrincipal was changed to UserPrincipal only due to spring security based on User Service returns UserPrincipal. So, not a big deal for my project to change it into one user model
Enable JPA Audit
Result