0

I have used Spring Data JPA and @Embedabble to create the composite key. And one Base class BaseDate will be extended by all the Entity.

sysCreationDate will be generated during insertion (not null and non-updatable)

save user is working fine for the first time but there are 3 issues here-

  1. During the second call instead of throwing an exception it is updating the sysUpdateDate and userType
  2. During the first call sysUpdateDate is not null (@UpdateTimestamp)
  3. During the second call in response it returns the sysCreationDate as null

Below is the code- Embeddable class

@Embeddable
public class CompKey implements Serializable {  
    @Column(name ="USER_ID")
    private String userId;
    @Column(name ="USER_NAME")
    private String userName;    
    public CompKey(String userId, String userName) {
        super();
        this.userId = userId;
        this.userName = userName;
    }
    public CompKey() {
        super();
    }
    //Getters /Setters /Equual and Hashcode
}

Base Class for Date

@MappedSuperclass
public abstract class BaseDate {    
    @CreationTimestamp
    @Column(name = "SYS_CREATION_DATE",  updatable=false, nullable=false)
    private Calendar sysCreationDate;
    @Column(name = "SYS_UPDATE_DATE")
    @UpdateTimestamp
    private Calendar sysUpdateDate; 
    public BaseDate(Calendar sysCreationDate, Calendar sysUpdateDate) {
        this.sysCreationDate = sysCreationDate;
        this.sysUpdateDate = sysUpdateDate;
    }
    public BaseDate() {
    }   
    //Getters and Setters
}

Entity Class

@Entity
public class User extends BaseDate{ 
    @Column(name = "USER_TYPE")
    private String userType;
    @EmbeddedId
    private CompKey compkey;
    
    public User() {
        super();
    }
    public User(Calendar sysCreationDate, Calendar sysUpdateDate, String userType, CompKey compkey) {
        super(sysCreationDate, sysUpdateDate);
        this.userType = userType;
        this.compkey = compkey;
    }   
    //Getters and setters
}

Repo -

@Repository
public interface UserRepo extends CrudRepository<User, CompKey> {
}

Service and Controller -

@Service
public class UserService {
    @Autowired
    UserRepo userRepo;

    public User saveUser(User user) {
        
        return userRepo.save(user);
    }

    public Optional<User> getUser(CompKey key) {
        
        return userRepo.findById(key);
    }
}

@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    UserService userService;
    
    @PostMapping("/save")
    public User saveUser(@RequestBody User user) {
        
        return userService.saveUser(user);
    }
    
    @GetMapping("/get")
    public Optional<User> getUser(@RequestBody CompKey key) {
        
        return userService.getUser(key);
    }

Input -

{
    "userType": "K",
    "compkey": {
        "userId": "1002",
        "userName": "ASDF"
    }
}

Output 1)-

{
    "sysCreationDate": "2021-01-08T18:09:28.802+00:00",
    "sysUpdateDate": "2021-01-08T18:09:28.802+00:00",
    "userType": "K",
    "compkey": {
        "userId": "1002",
        "userName": "ASDF"
    }
{
    "sysCreationDate": null,
    "sysUpdateDate": "2021-01-08T18:10:43.206+00:00",
    "userType": "K",
    "compkey": {
        "userId": "1002",
        "userName": "ASDF"
    }
}

Thanks in advance

s4n17a
  • 1
  • 3
  • Can you post your controller methods for saving/updating a user? – hjoeren Jan 08 '21 at 19:01
  • To point 2: According to [this discussion](https://discourse.hibernate.org/t/do-not-populate-updatetimestamp-field-when-entity-gets-created/2613) it is intended to set `@UpdateTimestmp` during persisting an entity. – hjoeren Jan 08 '21 at 19:11

1 Answers1

2

The integrity constraint violation exception is not thrown because your Spring repository just updates the object.

Spring repositories do not differentiate between insert and update. There is only one general-purpose method -- save. By default, this method persists (inserts) a new object only when a primary key is null or 0; otherwise, it merges (updates) into an existing object. You always have a primary key set, so it always calls merge, which updates the second time.

Its basic implementation in SimpleJpaRepository looks like:

    @Transactional
    public <S extends T> S save(S entity) {
        Assert.notNull(entity, "Entity must not be null.");
        if (this.entityInformation.isNew(entity)) {
            this.em.persist(entity);
            return entity;
        } else {
            return this.em.merge(entity);
        }
    }

The key part is isNew method with its default implementation like:

    public boolean isNew(T entity) {

        ID id = getId(entity);
        Class<ID> idType = getIdType();

        if (!idType.isPrimitive()) {
            return id == null;
        }

        if (id instanceof Number) {
            return ((Number) id).longValue() == 0L;
        }

        throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
    }

The available solutions are:

  1. call EntityManager directly.
  2. implement Persistable interface from Spring and implement your own isNew to inform a Spring repository whether your object is new or was already persisted.
  3. use a surrogate primary key (long, @GeneratedValue) and a unique constraint on your logical key

I would recommend the third solution (with a surrogate primary key) as it's simple and has better extensibility. For example, it will be easier to add a foreign key referencing your entity.

There also is a solution with calling find first, just to check if the object exists in a database. However, this solution is prone to a race issue (two concurrent REST requests to create a new object, both call find, both receive null, thus both save, and one data is lost/overwritten).

For @UpdateTimestamp, you've already got a comment, and for @CreationTimestamp null, please, post your controller.

  • 2
    By reading this answer: I can imagine, that problem 3 belongs to problem 1: The entity gets updated, in the request data the `sysCreationDate` field is `null` and so the field gets overwritten ... by `null`. Possible solution: Mark the column as "not updatable" (`@Column(/* ... */ updatable = false)`) – hjoeren Jan 09 '21 at 06:22
  • Thanks for your quick answer, I have updated the code with the service and controller – s4n17a Jan 09 '21 at 06:38