Search code examples
spring-bootspring-data-jpa

Generated column isn't returned by save() when performing an update


I am a beginner learning about Spring Boot, and I've been having trouble making generated values work. I would like to have the database (Postgres) generate a timestamp column when a row is inserted, instead of having Spring generate it, but the problem is Spring doesn't seem to select the timestamp from the database when it performs an update.

When I save() a new entity, it is inserted fine in the database, a timestamp is generated and returned to Spring; everything works fine. When I try to find() an existing entity, the timestamp is also returned, so that's fine too. However when I try to update an existing entity, Spring DOESN'T access the timestamp that's in the database, and it instead returns null for the corresponding field.

Here is the entity definition:

import jakarta.persistence.*
import org.hibernate.annotations.CurrentTimestamp
import org.hibernate.generator.EventType
import java.time.OffsetDateTime

@Entity(name = "users")
@Table(name = "users")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Int?,

    val name: String,

    @Column(name = "date_created")
    @CreationTimestamp(source = SourceType.DB)
    val dateCreated: OffsetDateTime?,
)

Say the database already has a row like 1 | 2025-01-02 02:03:04 | user1. A PUT request like { name: "new user" } will update the name field in the database without changing the timestamp, but the updated entity returned by JPA will be { id: 1, name: "new user", dateCreated: null }. I'm not sure why that is. I know that JPA executes an additional SELECT when INSERTing a new row in order to get the timestamp that was generated by Postgres, but I don't see why it wouldn't just get the already existing timestamp when UPDATEing.

The controller and service classes for completeness:

import com.example.hello.User
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.*

@RestController
@RequestMapping(path = ["/users"])
class UserController(private val userService: UserService) {
    @PostMapping
    fun createStaff(@RequestBody user: User): ResponseEntity<User> {
        val createdUser = userService.create(user)
        return ResponseEntity(createdUser, HttpStatus.CREATED)
    }

    @PutMapping(path = ["/{id}"])
    fun updateUser(@PathVariable("id") id: Int, @RequestBody user: User): ResponseEntity<User> {
        val updatedUser = userService.update(id, user)
        return ResponseEntity(updatedUser, HttpStatus.OK)
    }

    @GetMapping(path = ["/{id}"])
    fun readUser(@PathVariable("id") id: Int): ResponseEntity<User> {
        val user = userService.get(id)
        return user?.let { ResponseEntity.ok(it) } ?: ResponseEntity(HttpStatus.NOT_FOUND)
    }
}
import com.example.hello.User
import com.example.hello.UserRepository
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service

@Service
class UserService(private val userRepository: UserRepository) {
    fun create(user: User): User {
        return userRepository.save(user)
    }

    fun update(id: Int, user: User): User {
        val userWithId = user.copy(id = id)
        return userRepository.save(userWithId)
    }

    fun get(id: Int): User? {
        return userRepository.findByIdOrNull(id)
    }
}

Solution

  • When you call PUT api, you only pass name in the body, but the request User type, so Spring will understand that your body is:

    {
      "id": null,
      "name": "your_input_name",
      "date_created": null
    }
    

    And in your update method, you only copy object with id => the dateCreated still null. That why when it save to db, the date_created column is null

    You can first query the object in db with id, change name field then save it back.

    Or you can add updatable = false to make sure date_created will not be updated.

    @Column(name= "date_created", updatable = false)