Spring Boot #PrePersist does not work properly - spring

I have a Spring Boot application with Kotlin and want to store the creation timestamp and the last updated timestamp. The following code should do the trick:
#Entity
data class Entity(
// ...
) {
lateinit var createdAt: Instant
lateinit var updatedAt: Instant
#PrePersist
private fun onCreate() {
createdAt = Instant.now().also { updatedAt = it }
}
#PreUpdate
private fun onUpdate() {
updatedAt = Instant.now()
}
}
The PostgreSQL table looks like this:
create table entity
(
-- Other fields...
created_at timestamp not null,
updated_at timestamp not null
);
When i now try to call repository.save(entity) I get the following error:
Caused by: org.postgresql.util.PSQLException: ERROR: null value in column "created_at" violates not-null constraint
updatedAt however has the correct value which means #PreUpdate is working as expected. Does anyone know what is the problem here?

Related

Error while testing with H2 database with Reactive Spring boot

I am using Reactive Spring boot to create a REST API and while the API works perfectly (I have tested it with postman), I also need to write some Unit tests and for the Unit tests I need to use a seperate in-memory database for it. I am using H2 database for testing and a hosted Postgres DB for the actual application and while my application entities are being loaded successfully in the postgres DB, None of my tests are passing as I receieve the following error:
Failed to execute SQL script statement #1 of class path resource [data.sql]: CREATE TABLE IF NOT EXISTS full_connection ( id BIGINT PRIMARY KEY , aa TEXT NOT NULL, fip TEXT NOT NULL, status TEXT, created_on TIMESTAMP, created_by TEXT, last_updated_on TIMESTAMP, last_updated_by TEXT ); nested exception is java.lang.ClassCastException: class java.lang.Long cannot be cast to class java.lang.Integer (java.lang.Long and java.lang.Integer are in module java.base of loader 'bootstrap')
I am assuming the error is being thrown because of the id paramater which has the datatype BIGINT which the official documentation of H2 says is mapped to java.long.Long.
Here's my entity:
#Getter
#Setter
#NoArgsConstructor
#AllArgsConstructor
#Builder
#ToString
#Table
public class FullConnection {
#Id
#Column(name = "id")
private Long id;
#Column(name = "AA", nullable = false)
private String aa;
#Column(name = "FIP", nullable = false)
private String fip;
private String status;
private LocalDateTime createdOn;
private String createdBy;
private LocalDateTime lastUpdatedOn;
private String lastUpdatedBy;
}
Here's how I am initializing the data:
#Bean
ConnectionFactoryInitializer initializer(#Qualifier("connectionFactory") ConnectionFactory connectionFactory) {
ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer();
initializer.setConnectionFactory(connectionFactory);
ResourceDatabasePopulator resource =
new ResourceDatabasePopulator(new ClassPathResource(dataInitializerName));
initializer.setDatabasePopulator(resource);
return initializer;
}
where it's reading the dataInitializerName string from application-test.yaml and is being read correctly, the script (for testing)(the script for prod and testing are different) is:
CREATE TABLE IF NOT EXISTS full_connection (
id BIGINT PRIMARY KEY ,
aa TEXT NOT NULL,
fip TEXT NOT NULL,
status TEXT,
created_on TIMESTAMP,
created_by TEXT,
last_updated_on TIMESTAMP,
last_updated_by TEXT
);
Here: https://www.h2database.com/html/datatypes.html#bigint_type it clearly states that BIGINT is mapped to java.long.Long, then why is it trying to cast it to int? Or is it regarding some other field?
I had a similar problem. Explicitly defining the version for the dependency r2dbc-h2 in pom.xml is causing the problem. I removed the version so that springboot will automatically use the compatible version of r2dbc-h2, and then the problem resolved.

Getting JSON parse error on JSON property createdAt when not included in request body

I am recently learning to build REST APIs with Kotlin and Spring Boot. I was trying to create my first domain model and controller end-point. Here is the implementation of the Country model and CountryController countroller.
/model/Country.kt
package com.example.example.model
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.EntityListeners
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import org.springframework.data.jpa.domain.support.AuditingEntityListener
import java.sql.Timestamp
#Entity
#Table(name = "country")
class Country (
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
#Column(name = "country_id")
val id: Int,
val code: String,
val displayName: String,
#CreatedDate
#Column(name = "created_at", nullable = false, updatable = false)
val createdAt: Timestamp,
#LastModifiedDate
#Column(name = "updated_at", nullable = false)
val updatedAt: Timestamp
)
/controller/CountryCountroller.kt
package com.example.example.controller
import com.example.example.model.Country
import com.example.example.repository.CountryRepository
import jakarta.validation.Valid
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController
import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping
#RestController
class CountryController {
#Autowired
lateinit var countryRepository: CountryRepository
#PostMapping("/countries", consumes = arrayOf(MediaType.APPLICATION_JSON_VALUE))
fun createCountry(#Valid #RequestBody country: Country): Country {
return countryRepository.save(country);
}
}
Schema of the Country table:
country_id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
code VARCHAR(3) NOT NULL,
displayName VARCHAR(40) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (country_id)
When I started running the Spring Boot application and use Postman to call the POST /countries API, I received a 400 Bad Request error. The application error log says:
2023-01-12T14:09:15.423Z WARN 90037 --- [nio-8080-exec-3] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Instantiation of [simple type, class com.example.example.model.Country] value failed for JSON property createdAt due to missing (therefore NULL) value for creator parameter createdAt which is a non-nullable type]
The request body is a json and the request header has include the correct Content-Type information:
{
"code": "FRA",
"displayName": "France"
}
I suspected that the Spring Boot did not ignore the two fields createdAt and updatedAt, therefore I have tried to add #JsonIgnore above createdAt and updatedAt. Unfortunately the same 400 Bad Request error was still returned.
Do I actually have to include createdAt and updatedAt in the JSON request body? However, what I wanted to achieve is that the createdAt and updatedAt should be added/updated either by the Spring Boot application putting the createdAt and updatedAt data when calling the repository methods or by MySQL DB when the query is being run. Is there any way to do so?
You are mixing up two concepts here
whether on not createdAt and updatedAt are allowed be null in your data model
serialization:
whether you want the RestController to deserialize the fields when you receive them in the POST
whether you want the RestController to serialize the fields when you implement a GET request
By using #JsonIgnore (or the Kotlin variation #set:JsonIgnore you are just avoiding the data deserialization but still the data model requires a value. The JPA annotations are going to get executed when you put the data in the database, but in the time that the object is created by the MVC infrastructure, and before the database save happens, the data does not match the model contract.
There are two solutions to what you want to do:
Relax the data model
Change your data model to allow nulls in the two fields:
#CreatedDate
#Column(name = "created_at", nullable = false, updatable = false)
val createdAt: Timestamp?,
#LastModifiedDate
#Column(name = "updated_at", nullable = false)
val updatedAt: Timestamp?
BTW I would expect you to get the error on id as well, perhaps an Exception would be given for that once you dealt with the two dates, because again, your POST data does not supply id and this not nullable.
Use a Request object
Create a simpler data model that is used to receive your POSTed data and then create the Domain/Database model object from it.
I use a mix of the two techniques depending on circumstances. The latter approach has benefit, e.g. when you have large objects which may only be partially sent by the client (think PATCH).

Failed parsing for a LocalDate value while using Spring Data Rest

I am still a newbie with Spring Data Rest and I'm having some issues with it while I have to parse a LocalDate value to an endpoint. I have searched info's in other topics too but I'm still stucked, this is the problem:
I have one Entity with this code .
#Entity
#Table(name="calendario_parcheggio")
#Setter
#Getter
public class CalendarioParcheggio {
#Id
#Column(name="data_parcheggio")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE )
#JsonFormat(pattern="yyyy-MM-dd")
private LocalDate data;
#Column(columnDefinition = "ENUM('ATTIVO', 'ARCHIVIATO')")
#Enumerated(EnumType.STRING)
private Stato stato;
#OneToMany(cascade=CascadeType.ALL)
#JoinColumn(name="data_parcheggio")
private List<Parcheggio> parcheggio;
public enum Stato {
ATTIVO,
ARCHIVIATO,
}
}
It's an Entity linking the Date and its status for a Parking that works hourly.Matching this table on MySQL
CREATE TABLE calendario_parcheggio (
data_parcheggio DATE PRIMARY KEY,
stato ENUM ('ATTIVO','ARCHIVIATO') NOT NULL DEFAULT ('ATTIVO')
);
When I start the server everything is ok , but when i try (by browser or Postman) to check the data of a particular instance (in my case : "http://localhost:8080/parkingsystem/api/calendario-parcheggio/2022-10-18") ,I get this problem :
{"cause":
{"cause":
{"cause": null,
"message": "Text '2022-10-18' could not be parsed at index 2"
},
"message": "Parse attempt failed for value [2022-10-18]"
},
"message": "Failed to convert from type [java.lang.String] to type [java.time.LocalDate] for value '2022-10-18';
nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2022-10-18]"
}
And this is the Repository
#RepositoryRestResource(collectionResourceRel="calendarioParcheggio", path="calendario-parcheggio")
public interface CalendarioParcheggioRepository extends JpaRepository<CalendarioParcheggio, LocalDate> {
}
Can you help me to find the solution please?I hope I have explained the problem well enough, my English is still in training :)

Why doesn't Mybatis map a simple ENUM correctly?

I'm not doing anything out of the ordinary from what I can tell. I have a spring boot application using mybatis:
implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.1.1'
I have an application.properties config for mybatis that is pretty simple:
## MyBatis ##
mybatis.configuration.map-underscore-to-camel-case=true
mybatis.configuration.default-statement-timeout=30
My database table looks like this:
CREATE TABLE workspace_external_references (
id CHAR(36) PRIMARY KEY,
workspace_id CHAR(36) NOT NULL,
site VARCHAR(255) NOT NULL,
external_id VARCHAR(255) NOT NULL,
created_at DATETIME(6) NOT NULL DEFAULT NOW(6),
updated_at DATETIME(6) NOT NULL DEFAULT NOW(6),
FOREIGN KEY (workspace_id) REFERENCES workspaces (id) ON DELETE CASCADE
)
With just a single entry like this:
'a907c0af-216a-41e0-b16d-42107a7af05f', 'e99e4ab4-839e-405a-982b-08e00fbfb2d4', 'ABC', '6', '2020-06-09 00:19:20.135822', '2020-06-09 00:19:20.135822'
In my mapper file I'm doing a select of all references like this:
#Select("SELECT * FROM workspace_external_references WHERE workspace_id = #{workspaceId}")
List<WorkspaceExternalReference> findByWorkspace(#Param("workspaceId") final UUID workspaceId);
And the java object that this is supposed to map to looks like this:
public class WorkspaceExternalReference {
private UUID id;
private UUID workspaceId;
private Sites site;
private String externalId;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
public WorkspaceExternalReference(
final Sites site,
final UUID workspaceId,
final String externalId) {
this.site = site;
this.workspaceId = workspaceId;
this.externalId = externalId;
}
}
public enum Sites {
ABC, XYZ;
}
Sooooo why doesn't this work? I get this error back:
Caused by: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'id' from result set. Cause: java.lang.IllegalArgumentException: No enum constant com.acme.Sites.a907c0af-216a-41e0-b16d-42107a7af05f
When there is no default constructor, you need to let MyBatis know which columns to pass to the constructor explicitly (in most cases).
With annotations, it would look as follows.
You can use <resultMap> and <constructor> in XML mapper.
#ConstructorArgs({
#Arg(column = "site", javaType = Sites.class),
#Arg(column = "workspace_id", javaType = UUID.class),
#Arg(column = "external_id", javaType = String.class)
})
#Select("SELECT * FROM workspace_external_references WHERE workspace_id = #{workspaceId}")
List<WorkspaceExternalReference> findByWorkspace(#Param("workspaceId") final UUID workspaceId);
Other columns (i.e. id, created_at, updated_at) will be auto-mapped via setters (if there are) or reflection.
Alternatively, you can just add the default (no-arg) constructor to the WorkspaceExternalReference class. Then all columns will be auto-mapped after the class is instantiated.
Note: To make it work, there needs to be a type handler registered for UUID, but you seem to have done it already (otherwise the parameter mapping wouldn't work).

How do I annotate a JPA Entity to use a Date with OracleDB?

I have a OracleDB column of type Date. I can make a JPA entity that does not include this column and everything works great. However when I try to add this column like...
import java.sql.Timestamp
case class MyDate(
#Column(
name = "MY_DATE",
)
#BeanProperty date: Timestamp
) {
def this() {
this(null)
}
...
}
I get...
ORA-01747: invalid user.table.column, table.column, or column specification
How do I properly annotate a JPA Date Entity using Oracle?
I am not going to accept this because I would like to do it in the constructor like I described but this does work....
import java.sql.Timestamp
case class MyDate() {
#Column(
name = "MY_DATE",
)
#BeanProperty
val date: Timestamp = null
...
}

Resources