Propper way to describe request body as java class - java

What is the propper way to describe json body like this in spring-boot app?
{
"name": "name",
"releaseDate": "2000-01-01",
"description": "desc",
"duration": 10,
"rate": 1,
"mpa": { "id": 3},
"genres": [{ "id": 1}]
}
For now i have class like bellow, but i have problem with serialization of mpa and genres fields.
#Data
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Film extends Entity implements Comparable<Film> {
#Builder
public Film(long id, String name, #NonNull String description, #NonNull LocalDate releaseDate, #NonNull int duration, List<Genre> genres, Rating mpa, Set<Long> likes) {
super(id);
this.name = name;
this.description = description;
this.releaseDate = releaseDate;
this.duration = duration;
this.genres = genres;
this.mpa = mpa;
this.likes = likes;
}
#NotBlank
private final String name;
#NonNull
#Size(max = 200, message = "Description name longer than 200 symbols")
private final String description;
#NonNull
#Past
#JsonFormat(pattern = "yyyy-MM-dd", shape = JsonFormat.Shape.STRING)
private LocalDate releaseDate;
#NonNull
#Positive
private int duration;
private Rating mpa;
private List<Genre> genres;
#Setter(value = AccessLevel.PRIVATE)
private Set<Long> likes;
}
Genre and Rating:
#Data
public class Genre {
#Positive
private final long id;
}
#Data
public class Rating {
#Positive
private final long id;
}

Jackson ObjectMapper cannot create the object, because neither default constructor exists nor any other creator is provided (e.g. #JsonCreator, #ConstructorProperties).
You also have a rate property that is not defined in the Film class, which will cause problems unless you use the #JsonIgnoreProperties annotation (possibly with ignoreUnknown attribute set to true) or configure the ObjectMapper globally (DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES). It could also be that you wanted the likes property to handle that - anyway, it should be fixed.
I've also stumbled upon lack of the jackson-datatype-jsr310 dependency, but maybe your project already has it (it's required for the Java 8 classes like LocalDate).
There are different ways to solve the first problem described above, but generally you need to either provide a default constructor or define a creator for Jackson.
If you don't want to expose the default constructor, you can change the settings of the ObjectMapper (for Spring Boot read about Jackson2ObjectMapperBuilder) to allow usage of private creators (setVisibility method). In Lombok there's an annotation: #NoArgsConstructor. To limit the visibility, use the access annotation attribute.
The creator can be handled by annotating the constructor with ordered argument names:
#ConstructorProperties({"id", "name", "description", "releaseDate", "duration", "genres", "mpa", "likes"}). It gets complicated with the Genre and Rating classes as you do not have an explicit access to their constructors. You could either create them and mark appropriately or create a lombok.config file in your project's root directory and inside the file define the property:
lombok.anyConstructor.addConstructorProperties = true
This way Lombok will automatically add the #ConstructorProperties annotations to the classes.
I've created a GitHub repository with the tests for the solution - you can find it here. The lombok.config file is also included, as well as the fixed Film class.

In another way, I put Genre and Rating classes in Film class and the variables are not final in Genre and Rating , tried like this and it worked
#Data
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Film extends Entity implements Comparable<Film> {
#Builder
public Film(long id, String name, #NonNull String description, #NonNull LocalDate releaseDate, #NonNull int duration, List<Genre> genres, Rating mpa, Set<Long> likes) {
super(id);
this.name = name;
this.description = description;
this.releaseDate = releaseDate;
this.duration = duration;
this.genres = genres;
this.mpa = mpa;
this.likes = likes;
}
#NotBlank
private final String name;
#NonNull
#Size(max = 200, message = "Description name longer than 200 symbols")
private final String description;
#NonNull
#Past
#JsonFormat(pattern = "yyyy-MM-dd", shape = JsonFormat.Shape.STRING)
private LocalDate releaseDate;
#NonNull
#Positive
private int duration;
private Rating mpa;
private List<Genre> genres;
#Setter(value = AccessLevel.PRIVATE)
private Set<Long> likes;
#Data
public static class Genre {
#Positive
private long id;
}
#Data
public static class Rating {
#Positive
private long id;
}
}

Related

Hibernate MappingException: Could not determine type of custom object type using id class

So, I'm trying to persist an entity in the database that has a composite key, declared using the #IdClass annotation, which one of the ID keys I have turned into an object so ensure some validation of the data.
Before, when this ID was just a String, it was working without any problems, but now that I have changed it's type, it seens that Hibernate can't determine it's type in the database.
I found a question with a problem that was almost exactly the same as the mine, here. After I added the #Column annotation to the fields in the IdClass, I feel that the Hibernate could determine the type of the field in the database, but now it fails to perform the conversion.
I already have the converter class with the #Converter annotation and implementing the AttributeConverter interface, but I think that it isn't being reached by the Spring/Hibernate.
The involved classes bellow:
The converter
#Converter
public class ChapterNumberConverter implements AttributeConverter<ChapterNumber, String> {
#Override
public String convertToDatabaseColumn(ChapterNumber attribute) {
String value = attribute.getValue();
return value;
}
#Override
public ChapterNumber convertToEntityAttribute(String dbData) {
ChapterNumber chapterNumber = new ChapterNumber(dbData);
return chapterNumber;
}
}
The composite ID class
public class ChapterID implements Serializable {
private static final long serialVersionUID = 4324952545057872260L;
#Column
private Long id;
#Column
#Convert(converter = ChapterNumberConverter.class)
private String number;
#Column
private Long publisher;
#Column
private Long manga;
public ChapterID() {
}
public ChapterID(Long id, String number, Long publisher, Long manga) {
this.id = id;
this.number = number;
this.publisher = publisher;
this.manga = manga;
}
// ... getters and setters
}
The entity class
#Entity
#Table(name = "chapter", uniqueConstraints = #UniqueConstraint(columnNames = {"number", "publisher_id", "manga_id"}))
#IdClass(ChapterID.class)
public class Chapter {
#Id
#GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
#Id
#Convert(converter = ChapterNumberConverter.class)
private ChapterNumber number;
#Id
#ManyToOne
#JoinColumn(name = "publisher_id")
private Publisher publisher;
#Id
#ManyToOne
#JoinColumn(name = "manga_id")
private Manga manga;
#Column(nullable = false)
#Convert(converter = ChapterLanguageEnumConverter.class)
private ChapterLanguage language;
public Chapter() {
}
public Chapter(ChapterNumber chapterNumber, Publisher publisher, Manga manga, ChapterLanguage language) {
this.number = chapterNumber;
this.publisher = publisher;
this.manga = manga;
this.language = language;
}
public Chapter(String chapterNumber, Publisher publisher, Manga manga, ChapterLanguage language) {
this(new ChapterNumber(chapterNumber), publisher, manga, language);
}
// ... getters and setters
}
I just want to validate the number field in the entity class, so, if there is another way to do this without using a custom type, otherwise, if anyone knows what I can do to teach correctly the Hibernate how to persist this field, tell me please 😢

Java Spring Reactive, returning one Mono<..> from many multiple requests

[Java, Spring Reactive, MongoDB]
I'm currently trying to learn Reactive programming by doing and I found a challenge.
I have db object CategoryDB which looks like this:
#NoArgsConstructor
#Getter
#Setter
#Document(collection = DBConstraints.CATEGORY_COLLECTION_NAME)
class CategoryDB {
#Id
private String id;
private String name;
private String details = "";
#Version
private Long version;
private String parentCategoryId;
private Set<String> childCategoriesIds = new HashSet<>();
}
In a service layer I want to use model object Category.
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
}
I want to create Service with method Mono<Category getById(String id). In this case I want to fetch just one level of childCategories and direct parent Category. By default repository deliver Mono findById(..) and Flux findAllById(..) which I could use, but I'm not sure what would be the best way to receive expected result. I would be grateful for either working example or directions where can I find solution for this problem.
I've spent some time to figure out solution for this problem, but as I'm learning I don't know if it's good way of solving problems.
Added some methods to Category:
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
public void addChildCategory(Category childCategory) {
childCategory.updateParentCategory(this);
this.childCategories.add(childCategory);
}
public void updateParentCategory(Category parentCategory) {
this.parentCategory = parentCategory;
}
}
Function inside service would look like this:
#Override
public Mono<Category> findById(String id) {
return categoryRepository.findById(id).flatMap(
categoryDB -> {
Category category = CategoryDBMapper.INSTANCE.toDomain(categoryDB);
Mono<CategoryDB> parentCategoryMono;
if(!categoryDB.getParentCategoryId().isBlank()){
parentCategoryMono = categoryRepository.findById(categoryDB.getParentCategoryId());
}
else {
parentCategoryMono = Mono.empty();
}
Mono<List<CategoryDB>> childCategoriesMono = categoryRepository.findAllById(categoryDB.getChildCategoriesIds()).collectList();
return Mono.zip(parentCategoryMono, childCategoriesMono, (parentCategoryDB, childCategoriesDB) -> {
Category parentCategory = CategoryDBMapper.INSTANCE.toDomain(parentCategoryDB);
category.updateParentCategory(parentCategory);
childCategoriesDB.forEach(childCategoryDB -> {
Category childCategory = CategoryDBMapper.INSTANCE.toDomain(childCategoryDB);
category.addChildCategory(childCategory);
});
return category;
});
}
);
}
Where mapper is used for just basic properties:
#Mapper
interface CategoryDBMapper {
CategoryDBMapper INSTANCE = Mappers.getMapper(CategoryDBMapper.class);
#Mappings({
#Mapping(target = "parentCategoryId", source = "parentCategory.id"),
#Mapping(target = "childCategoriesIds", ignore = true)
})
CategoryDB toDb(Category category);
#Mappings({
#Mapping(target = "parentCategory", ignore = true),
#Mapping(target = "childCategories", ignore = true)
})
Category toDomain(CategoryDB categoryDB);
}
As I said I don't know if it's correct way of solving the problem, but it seem to work. I would be grateful for review and directions.

Mongo Template Aggregate by a date interval in Spring.Mongodb

I need to aggregate some data using java and Mongodb.
So my JS script it's that:
db.post.aggregate([
{$match: {date: {$gte: ISODate("2019-08-28T17:50:09.803Z"), $lte: ISODate("2019-12-03T21:45:51.412+00:00")}}},
{$project: {author: 1, _id: 0}},
{$group: {_id: "$author", count: {$sum:1}}}])
And my Java Code using spring+java is:
Aggregation aggregation =
newAggregation(
match(Criteria.where("date")
.lte(dynamicQuery.getEndDate())
.gte(dynamicQuery.getInitDate())),
project("author").andExclude("_id"),
group("author")
.count()
.as("total"));
AggregationResults<Post> result = mongoTemplate.aggregate(aggregation,Post.class, PostSummary.class);
List<Post> map = result.getMappedResults();
And I need to aggregate the sum of documents by authorId. My code returns the user code and no sum of documents.
And we have 2 Collections:
#EqualsAndHashCode(of = "id")
//TODO: Change the #Data to the properly scenario, we don't need
setters in this class anymore.
#Data
#Document (collection = "user")
//TODO: Change collection name to post, because collection are a group
of elements
public class User {
#Id
private String id;
private String name;
private String username;
#Email
private String email;
private String imageUrl;
private String providerId;
private LocalDateTime firstAccess;
}
Post Document:
#Data
#Document(collection = "post")
//TODO: Change collection name to post, because collection are a group of elements
public class Post {
public static final String AUTHOR_FIELD_NAME = "author";
public static final String DATE_FIELD_NAME = "date";
#Id
private String id;
private String content;
#DBRef(lazy = true)
#Field(AUTHOR_FIELD_NAME)
private User author;
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
#Field(DATE_FIELD_NAME)
private LocalDateTime date;
public Post(String content, User author, LocalDateTime date) {
this.content = content;
this.author = author;
this.date = date;
}
}
I have one solution, but I do not know if it is a better solution, so, let's go.
Because the Entity represented inside the Post Domain is one DBRef, the MongoDB does not parse the entire Stringo to the PostSummary.Class
#Getter
#Setter
public class PostSummary {
private User author;
private int count;
}
So, for the solution, I only parse the #Id in the author, and it's working. So If anyone has a better solution we have one solution now.
And should be like this:
#Getter
#Setter
public class PostSummary {
#Id
private User author;
private int count;
}

Use Java 8 optional with Mapstruct

I have these two classes:
public class CustomerEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private long id;
private String firstName;
private String lastName;
private String address;
private int age;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}
and
public class CustomerDto {
private Long customerId;
private String firstName;
private String lastName;
private Optional<String> address;
private int age;
}
The problem is that Mapstruct doesn't recognize the Optional variable "address".
Anyone has an idea how to solve it and let Mapstruct map Optional fields?
This is not yet supported out of the box by Mapstruct. There is an open ticket on their Github asking for this functionality: https://github.com/mapstruct/mapstruct/issues/674
One way to solve this has been added in the comments of the same ticket: https://github.com/mapstruct/mapstruct/issues/674#issuecomment-378212135
#Mapping(source = "child", target = "kid", qualifiedByName = "unwrap")
Target map(Source source);
#Named("unwrap")
default <T> T unwrap(Optional<T> optional) {
return optional.orElse(null);
}
As pointed by #dschulten, if you want to use this workaround while also setting the option nullValueCheckStrategy = NullValueCheckStrategy.ALWAYS, you will need to define a method with the signature boolean hasXXX() for the field XXX of type Optional inside the class which is the mapping source (explanation in the docs).

Ebean and Play! not filtering columns with .select()

I'm trying to fetch just a part of the model using Ebean in Play! Framework, but I'm having some problems and I didn't found any solutions.
I have these models:
User:
#Entity
#Table(name = "users")
#JsonInclude(JsonInclude.Include.NON_NULL)
public class User extends Model{
#Id
private int id;
#NotNull
#Column(name = "first_name", nullable = false)
private String firstName;
#Column(name = "last_name")
private String lastName;
#NotNull
#Column(nullable = false)
private String username;
#NotNull
#Column(nullable = false)
private String email;
private String gender;
private String locale;
private Date birthday;
private String bio;
#NotNull
#Column(nullable = false)
private boolean active;
private String avatar;
#Column(name = "created_at",nullable = false)
private Date createdAt;
#OneToMany
private List<UserToken> userTokens;
// Getters and Setters omitted for brevity
}
UserToken:
#Entity
#Table(name = "user_tokens")
public class UserToken extends Model {
#Id
private int id;
#Column(name = "user_id")
private int userId;
private String token;
#Column(name = "created_at")
#CreatedTimestamp
private Date createdAt;
#ManyToOne
private User user;
// Getters and Setters omitted for brevity
}
And then, I have a controller UserController:
public class UserController extends Controller{
public static Result list(){
User user = Ebean.find(User.class).select("firstName").where().idEq(1).findUnique();
return Results.ok(Json.toJson(user));
}
}
I expected that, when using the .select(), it would filter the fields and load a partial object, but it loads it entirely.
In the logs, there is more problems that I don't know why its happening.
It is making 3 queries. First is the one that I want. And then it makes one to fetch the whole Model, and another one to find the UserTokens. I don't know why it is doing these last two queries and I wanted just the first one to be executed.
Solution Edit
After already accepted the fact that I would have to build the Json as suggested by #biesior , I found (out of nowhere) the solution!
public static Result list() throws JsonProcessingException {
User user = Ebean.find(User.class).select("firstName").where().idEq(1).findUnique();
JsonContext jc = Ebean.createJsonContext();
return Results.ok(jc.toJsonString(user));
}
I render only the wanted fields selected in .select() after using JsonContext.
That's simple, when you using select("...") it always gets just id field (cannot be avoided - it's required for mapping) + desired fields, but if later you are trying to access the field that wasn't available in first select("...") - Ebean repeats the query and maps whole object.
In other words, you are accessing somewhere the field that wasn't available in first query, analyze your controller and/or templates, find all fields and add it to your select (even if i.e. they're commented with common HTML comment in the view!)
In the last version of Play Framework (2.6) the proper way to do this is:
public Result list() {
JsonContext json = ebeanServer.json();
List<MyClass> orders= ebeanServer.find(MyClass.class).select("id,property1,property2").findList();
return ok(json.toJson(orders));
}

Categories

Resources