JPA #OneToMany doesn't save parent ID - spring-boot

i develop API using Spring Boot and JPA
I found a case in Bidirectional OneToMany relationship, my parent ID doesn't save in DB when requested by JSON payload like this :
{
"name": "Rice",
"description": "Lorem ipsum dolor sit amet",
"variants": [
{
"amount": 1,
"unit": "kg",
"price": 60000
}
]
}
Here is my Model :
Product.kt
data class Product(
#Id
#GeneratedValue
var id: Long = 0,
var name: String = "",
var photo: String = "",
var description: String = "",
#OneToMany(mappedBy = "product", cascade = [CascadeType.ALL])
#JsonBackReference
var variants: MutableList<Variant> = mutableListOf()
)
Variant.kt
#Entity
data class Variant(
#Id
#GeneratedValue
var id: Long = 0,
var amount: Int = 0,
var unit: String = "",
var price: Double = 0.0,
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "product_id")
#JsonManagedReference
var product: Product? = null
)
Is there any other configuration need to be applied?
Thank you

The problem is, that during JSON deserialization the back reference from Variant to Product is not set.
And you define
#OneToMany(mappedBy = "product", cascade = [CascadeType.ALL])
#JsonBackReference
var variants: MutableList<Variant> = mutableListOf()
mappedBy says that the back reference is the owning side and therefor in charge for setting the foreign key.
You have two options:
Either you set the product reference in Variant before saving the Product
or you remove the back reference from Variant to Product and remove the mappedBy = "product" from the variants collection.

Related

Spring mongo aggregate nested elements null

I have 3 collections: attributes, products and product_attributes. I am trying to retrieve product attributes with aggregation with nested attribute and product in it. But product and attribute always null
service method:
fun findById(id: String): Mono<ProductAttribute> {
val idField = "_id"
val productId = "productId"
val attributeId = "attributeId"
val attributesDb = "attributes"
val productsDb = "products"
val attributeName = "attribute"
val productName = "product"
fun lookup(from: String, localField: String, name: String) = LookupOperation.newLookup()
.from(from)
.localField(localField)
.foreignField(idField)
.`as`(name)
fun match(id: String) = Aggregation.match(Criteria.where(idField).`is`(ObjectId(id)))
fun unwind(field: String) = Aggregation.unwind(field)
val aggregation = Aggregation.newAggregation(
match(id),
lookup(productsDb, productId, productName),
unwind(productName),
lookup(attributesDb, attributeId, attributeName),
unwind(attributeName)
).withOptions(AggregationOptions.builder().allowDiskUse(true).build())
return operations.aggregate(aggregation, "product_attributes", ProductAttribute::class.java)
.last()
.doOnError { throwable -> logger.error("Failed to get productAttribute", throwable) }
}
ProductAttribute.kt
#Document(collection = "product_attributes")
data class ProductAttribute(
#Id
#JsonProperty("_id")
val id: String? = ObjectId().toHexString(),
#get:Transient #Value("null") val product: Product?,
#get:Transient #Value("null") val attribute: Attribute?,
val attributeId: String,
val productId: String,
val name: String,
val quantity: Int,
val photos: List<String>
) : Serializable
Response:
{
"data": {
"productAttributeId": {
"id": "62a3bff787418b6f837e9150",
"product": null,
"attribute": null,
"name": "string",
"quantity": 1,
"photos": [
"string"
]
}
}
}
As a workaround I have ended up with this result:
I made aggregate method with generic type. It will aggregate to Any type and then map to my specific type.
inline fun <reified T> aggregate(
aggregation: Aggregation,
collectionName: String,
operations: ReactiveMongoOperations,
objectMapper: ObjectMapper
): Flux<T> =
operations.aggregate(aggregation, collectionName, Any::class.java)
.map { data -> objectMapper.convertValue(data, T::class.java) }
I think there is a way to implement custom converter. I will think about this later. I will update answer on success

Bad Request with 400 status code in Spring boot

Recently I am using Spring boot framework with Kotlin. Every thing is all okey with GET method. But when registering a new user with POST method I go faced some problem having Bad Request with status code 400.
Here is my code associate with my spring boot project
User.kt
#Entity
#Table(name = "user_info")
data class User(
#Id
#SequenceGenerator(
name = "user_seq",
sequenceName = "user_seq",
allocationSize = 1
)
#GeneratedValue(
strategy = SEQUENCE,
generator = "user_seq"
)
#Column(
name = "id",
updatable = false
)
val id: Long = -1,
#Column(
name = "first_name",
nullable = false,
length = 50,
updatable = true
)
val firstName: String,
#Column(
name = "last_name",
nullable = false,
length = 50,
updatable = true
)
val lastName: String,
#Column(
name = "email",
nullable = true,
length = 150,
updatable = true
)
val email: String,
#Column(
name = "gender",
nullable = false,
length = 2,
updatable = true
)
val gender: String,
#Column(
name = "date_of_birth",
nullable = false,
updatable = true
)
val dateOfBirth: LocalDate,
#Column(
name = "country",
nullable = false,
length = 50,
updatable = true
)
val country: String
)
UserController.kt
#RestController
#RequestMapping(
path = [
"/api/v1/"
]
)
class UserController(
#Autowired private val userService: UserService
) {
#PostMapping("register")
fun registerUser(#RequestBody user: User) {
userService.registerUser(user)
}
#GetMapping("users")
fun getUsers(): List<User> {
return userService.getUsers()
}
#GetMapping("user/{id}")
fun getUsers(#PathVariable("id") id: Long): User {
return userService.getUserInfo(id)
}
}
My Request Payload is
POST http://localhost:8080/api/v1/register
Content-Type: application/json
{
"first_name" : "Abid",
"last_name" : "Affan",
"email" : "aminul15-5281#diu.edu.bd",
"gender" : "M",
"date_of_birth" : "2019-05-03",
"country" : "Bangladesh"
}
and my response payload is
POST http://localhost:8080/api/v1/register
HTTP/1.1 400
Content-Type: application/json
Transfer-Encoding: chunked
Date: Mon, 01 Mar 2021 05:52:03 GMT
Connection: close
{
"timestamp": "2021-03-01T05:52:03.634+00:00",
"status": 400,
"error": "Bad Request",
"message": ""JSON parse error: Instantiation of [simple type, class com.example.demo.user.User] value failed for JSON property firstName due to missing (therefore NULL) value for creator parameter firstName which is a non-nullable type; nested exception is com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException: Instantiation of [simple type, class com.example.demo.user.User] value failed for JSON property firstName due to missing (therefore NULL) value for creator parameter firstName which is a non-nullable type\n at [Source: (PushbackInputStream); line: 8, column: 1] (through reference chain: com.example.demo.user.User[\"firstName\"])",
"path": "/api/v1/register"
}
Response code: 400; Time: 214ms; Content length: 119 bytes
Add following property in your spring boot configuration
logging.level.org.springframework.web=DEBUG you will get the exact reason for getting 400 Bad requests on console logs.
You used camelCase in your entity - User and snake_case in your request payload. It is recommended not to mix syntax for object mapping. Can you try with this request poayload:
{
"firstName" : "Abid",
"lastName" : "Affan",
"email" : "aminul15-5281#diu.edu.bd",
"gender" : "M",
"dateOfBirth" : "2019-05-03",
"country" : "Bangladesh"
}

Nested objects with Spring Boot, MongoDB

How get nested objects with MongoDb using Spring Boot?
I have 3 DTO, BoardResponse, ColumnsResponse, CardResponse.
public class BoardResponse {
//id, name, createdBy,createdDate,updatedBy,updatedDate, getters
List<ColumnResponse> columns = new ArrayList<ColumnResponse>();
public BoardResponse(KanbanBoard board, List<ColumnResponse> columns) {
super();
this.id = board.getId();
this.name = board.getName();
this.createdBy = board.getCreatedBy();
this.createdDate = board.getCreatedDate();
this.updatedBy = board.getUpdatedBy();
this.updatedDate = board.getUpdatedDate();
this.columns = columns;
}
public class ColumnResponse {
//id, name, createdBy,createdDate,updatedBy,updatedDate, getters
private ObjectId idBoard;
List<CardResponse> cards = new ArrayList<>();
public ColumnResponse(KanbanColumn column, List<CardResponse> cards) {
super();
this.id = column.getId();
this.name = column.getName();
this.createdBy = column.getCreatedBy();
this.createdDate = column.getCreatedDate();
this.updatedBy = column.getUpdatedBy();
this.updatedDate = column.getUpdatedDate();
this.id = column.getIdBoard();
this.idBoard = column.getIdBoard();
this.cards = cards;
}
public class CardResponse {
//id, name, createdBy,createdDate,updatedBy,updatedDate, getters
private ObjectId idColumn;
public CardResponse(KanbanCard card) {
super();
this.id = card.getId();
this.name = card.getName();
this.createdBy = card.getCreatedBy();
this.createdDate = card.getCreatedDate();
this.updatedBy = card.getUpdatedBy();
this.updatedDate = card.getUpdatedDate();
this.idColumn = card.getIdColumn();
}
I want how to do nested with MongoTemplate, I got It using business logic if board exist find by column with board id, if column exist find by card with card id.
I don't know if doing that is a good way.
KanbanBoard, KanbanColumn, is the same as the entity Board and Column, the same properties.
public BoardResponse findBoardById(ObjectId id, UserPrincipal currentUser) {
KanbanBoard board = this.kanbanBoardRepository.findById(id).orElse(null);//find board by id
//find all columns by board ID within the entity #document KanbanColumn
List<KanbanColumn> columns = this.kanbanColumnRepository.findAllByIdBoard(board.getId());
//return list of ColumnResponse DTO
List<ColumnResponse> columnResponse = columns.stream().map(column -> {
return new ColumnResponse(column);
}).collect(Collectors.toList());
//List Column map
List<ColumnResponse> columnMap = new ArrayList<>();
for (ColumnResponse column : columnResponse) {
List<CardResponse> cards = this.kanbanCardRepository.findAllByIdColumn(column.getId()).stream()
.map(card -> new CardResponse(card)).collect(Collectors.toList());
column.setCards(cards); //column set list of cards
columnMap.add(column);//add columns with list of cards inside list of column map
}
//return board with list of column map, with list of cards
return new BoardResponse(board, columnMap);
}
The result is
{
"id": "5e717d6d6e7cbf226074c3fe",
"name": null,
"createdBy": "admin",
"createdDate": 1584495981290,
"updatedBy": "admin",
"updatedDate": 1584495981290,
"columns": [
{
"id": "5e72bfa6cc3ff9000ae93c92",
"name": null,
"createdBy": "admin",
"createdDate": 1584578470269,
"updatedBy": "admin",
"updatedDate": 1584578470269,
"idBoard": null,
"cards": [
{
"id": "5e72de720715f131878b4ed2",
"name": "esse é o card",
"createdBy": "admin",
"createdDate": 1584586354958,
"updatedBy": "admin",
"updatedDate": 1584586354958,
"idColumn": "5e72bfa6cc3ff9000ae93c92"
}
]
},
{
"id": "5e72bfefcc3ff9000ae93c95",
"name": "coluna criada com sucesso.",
"createdBy": "admin",
"createdDate": 1584578543201,
"updatedBy": "admin",
"updatedDate": 1584578543201,
"idBoard": null,
"cards": [
{
"id": "5e72de550715f131878b4ed0",
"name": "esse é o card",
"createdBy": "admin",
"createdDate": 1584586325485,
"updatedBy": "admin",
"updatedDate": 1584586325485,
"idColumn": "5e72bfefcc3ff9000ae93c95"
},
{
"id": "5e72de630715f131878b4ed1",
"name": "esse é o card",
"createdBy": "admin",
"createdDate": 1584586339140,
"updatedBy": "admin",
"updatedDate": 1584586339140,
"idColumn": "5e72bfefcc3ff9000ae93c95"
}
]
}
]
}
The use of MongoTemplate, Aggregation, can be verbose, but the result is the same as MongoRepository.
public BoardResponse findBoardById(ObjectId id, UserPrincipal currentUser) {
//board
AggregationOperation boardId = Aggregation.match(Criteria.where("id").is(id));
Aggregation boardAggregation = Aggregation.newAggregation(boardId);
KanbanBoard boards = mongoTemplate.aggregate(boardAggregation, KanbanBoard.class, KanbanBoard.class)
.getUniqueMappedResult();
//column idBoard equal board ID
AggregationOperation ColumnIdBoardIsLikeBoardId = Aggregation
.match(Criteria.where("idBoard").is(boards.getId()));
Aggregation columnAggregation = Aggregation.newAggregation(ColumnIdBoardIsLikeBoardId);
List<ColumnResponse> listaColumns = mongoTemplate
.aggregate(columnAggregation, KanbanColumn.class, ColumnResponse.class).getMappedResults();
//map every column, column set list of cards
listaColumns.stream().map(column -> {
List<CardResponse> cards =
// find all cards by column ID
this.kanbanCardRepository.findAllByIdColumn(column.getId()).stream()
.map(card -> new CardResponse(card)).collect(Collectors.toList());
column.setCards(cards);
return column;
}).collect(Collectors.toList());
//board DTO set board, list of columns with list of cards
return new BoardResponse(boards, listaColumns);
}

Why is the ids in my jpa hibernate postgres database skipping id values?

I have these tables that are autogenerated by JPA and Hibernate on startup with these spring configuration.
This is my application.yml config:
spring:
datasource:
url: "jdbc:postgresql://postgres-cinema:5432/postgres"
username: "postgres"
password: ""
driver-class-name: "org.postgresql.Driver"
jpa:
database: "postgresql"
hibernate:
ddl-auto: "update"
These are my Entity classes:
#Entity
class Cinema(
#get:Id #get:GeneratedValue
var id: Long? = null,
#get:NotBlank #get:Size(max = 128)
var name: String,
#get:NotBlank #get:Size(max = 128)
var location: String? = null,
#get:OneToMany(mappedBy = "cinema", cascade = [CascadeType.ALL], fetch = FetchType.EAGER)
var rooms: MutableSet<Room> = mutableSetOf()
)
#Entity
class Room(
#get:Id #get:GeneratedValue
var id: Long? = null,
#get:NotBlank #get:Size(max = 128)
var name: String,
#get:ElementCollection
#get:NotNull
var seats: MutableSet<String>,
#get:ManyToOne(fetch = FetchType.EAGER)
#get:JoinColumn(name = "cinema_id")
var cinema: Cinema? = null
)
When i'm creating a new Cinema it gets the id of 1
Next i'm creating a new Room, and it gets the id of 2.
Am i doing something rong with the entity tables? and aren't these supposed to be independent table with their own ids?

Sort a list of Person objects using Linq

I need a query on sorting list of objects based on the category of property of the objects. I need the groups to be in an order other than the usual alphabetical order which I have seen in many other samples. I am using an example I took from elsewhere. How can I generate a list of Person objects based on HomeProvince but in terms of this ordering:
Ontario, Quebec, Alberta, Manitoba, British Columbia. The ordering within each group does not matter.
Person[] people = new Person[]
{
new Person() { FirstName = "Tony", LastName = "Montana", Age = 39, HomeProvince = "Ontario" },
new Person() { FirstName = "Bill", LastName = "Smith", Age = 23, HomeProvince = "Ontario" },
new Person() { FirstName = "Jane", LastName = "Doe", Age = 23, HomeProvince = "Alberta" },
new Person() { FirstName = "John", LastName = "Doe", Age = 23, HomeProvince = "Alberta" },
new Person() { FirstName = "Alex", LastName = "DeLarge", Age = 19, HomeProvince = "British Columbia" },
new Person() { FirstName = "Travis", LastName = "Bickle", Age = 42, HomeProvince = "Quebec" },
new Person() { FirstName = "Ferris", LastName = "Beuller", Age = 17, HomeProvince = "Manitoba" },
new Person() { FirstName = "Maggie", LastName = "May", Age = 23, HomeProvince = "Ontario" },
new Person() { FirstName = "Mickey", LastName = "Mouse", Age = 93, HomeProvince = "Alberta" },
new Person() { FirstName = "Frank", LastName = "Darabont", Age = 49, HomeProvince = "Ontario" }
};
You could do this:
// Provinces in the desired order
string[] provinces = { "Ontario", "Quebec", "Alberta", "Manitoba",
"British Columbia" };
var query = from province in provinces
join person in people on province equals person.HomeProvince
select person;
That will basically:
Ignore anyone not in the specified provinces
Return a sequence of people in the province order specified
If you need the people grouped by province, that's easy too:
var query = from province in provinces
join person in people on province equals person.HomeProvince
into grouped
select new { Province = province, People = grouped.ToList() };
Another option would be to create a mapping from province to "priority" and simply order by that. It really depends on exactly what you need as output.
I'm not sure if this is the best way but I would probably just get every person with Ontario as HomeProvince and add them to the array. Then repeat for Quebec and so on... I don't know any other way because what you need is "random".
I say array because that's what you used in your code sample. Obviously it doesn't matter what kind of container you use as long as you don't order them as you put them in or after they are in.

Resources