hibernate changes id when saving the object - java

I've got an object with this parameters
{
"id" : "36461dd3-2bdb-42de-8e3d-b44e28696b1e",
"race" : "HUMAN",
"age" : "18",
"name" : "Alisa"
}
I attempt to save it
List<MainFemaleCharacter> batch = Arrays.asList(sampleMainCharacter());
try (Session session = sessionFactory.openSession()) {
session.beginTransaction();
batch.forEach(session::save);
session.getTransaction().commit();
}
In debug, before saving, it shows id with expected value. But when I retrieve object it shows another id for example dccaf5d0-5c2b-4336-a0f3-ff65f92bf5f1. Why? MainFemaleCharacter class looks like this
#Entity
#Table(name="main_female_character")
#EqualsAndHashCode(callSuper=true)
#ToString(callSuper=true)
public #Data class MainFemaleCharacter extends BasicCharacter {
}
#MappedSuperclass
#EqualsAndHashCode(callSuper=true)
#ToString(callSuper=true)
public #Data class BasicCharacter extends UidNamedObject {
#OneToOne
private Race race;
private int age;
}
#MappedSuperclass
public #Data class UidNamedObject {
#Id
#GeneratedValue
private UUID id;
#Column(unique=true)
private String name;
}

The annotation #GeneratedValue will generate an id automatically. It is the same as the #GeneratedValue(strategy=GenerationType.AUTO) annotation.
GenerationType.AUTO means that the persistence provider chooses a strategy which will restart the values after a server restart in your case.
I recommend you to consider using GenerationType.SEQUENCE.

Related

JPA ManyToOne returning complete parent entity vs id

I have a springboot application with JPA. The ORM setup is a ManyToOne and I have roughly followed the excellent post here from Vlad so that I have only setup the #ManyToOne on the child.
My Entities are HealthCheck (Many) which must have a Patient (One). The issue is that when I retrieve a HealthCheck via my Rest controller instead of getting just the id of the patient I get the whole entity.
For my project I probably will get the whole patient with a HealthCheck, but I would like to know how i could get just the HealthCheck with the patient_id instead of the whole patient entity if I so needed to do so.
HEALTH CHECK
#AllArgsConstructor
#NoArgsConstructor
#Entity
#Data
public class HealthCheck {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
private Patient patient;
//Getters and Setters
PATIENT
#Entity
#Data
public class Patient {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
#Column(unique = true)
#NotEmpty(message = "Name must not be null or empty")
private String name;
// Getters and Setters
The HealthCheckServiceImpl uses the derived queries to get one by id, and its this call thats used to get a HealthCheck by the REST controller:
#Override
public HealthCheck get(Long id) {
log.info("Getting a single Health Check");
return healthCheckRepository.getById(id);
}
The result of a call to the REST controller results in something like:
{
"id": 2,
"patient": {
"id": 2,
"name": "Jono",
"hibernateLazyInitializer": {}
},
"other fields": "some comment",
"hibernateLazyInitializer": {}
}
Note1 the whole patient entity is returned
Note2 that because I have only used the ManyToOne annottaion on the child side I dont get the Jackson recursion issues some others do
QUESTION: How can I control the returned HealthCheck so that it includes the patient_id not the whole object?
UPDATE
The line that calls to the service is :
#GetMapping("get/{id}")
public ResponseEntity<HealthCheck> getHealthCheck(#PathVariable("id") Long id) {
return ResponseEntity.ok()
.header("Custom-Header", "foo")
.body(healthCheckService.get(id));
I breakpoint on healthCheckService.get(id)) but noting on the debugger looks like it contains an entity reference:
UPDATE2
Well, it seems you're returning your entities objects directly from your controller.
The issue is that when I retrieve a HealthCheck via my Rest controller instead of getting just the id of the patient I get the whole entity.
You can use the DTO Pattern explained here and here as response in your controller.
That, also decouples your domain from your controller layer.
This is a simplified example:
#Data
#NoArgsConstructor
public class HealthCheckDto {
private Long id;
private PatientDto patient;
private String otherField;
public HealthCheckDto(HealthCheck healthCheck) {
this.id = healthCheck.getId();
this.patient = new PatientDto(healthCheck.getPatient());
this.otherField = healthCheck.getOtherField();
}
}
#Data
#NoArgsConstructor
public class PatientDto {
private Long id;
public PatientDto(Patient patient) {
this.id = patient.getId();
}
}
// In your controller
public ResponseEntity<HealthCheckDto> getHealthCheck(Long id) {
HealthCheck healthCheck = healthCheckService.getById(id);
return ResponseEntity.ok(new HealthCheckDto(healthCheck));
}

JPA - how to get auto generated id in constructor?

What I want to achieve to take auto generated ID, hash it and save it into other field in the class, but at the stage of creating object by constructor ID is not yet generated. Any workaround ideas?
#Entity
public class MyClass {
#Id
#Column(name="id")
#GeneratedValue(strategy=GenerationType.AUTO)
Long id;
String hashID;
MyClass(){
this.hashID = Utils.hashID(id);
}
//setters and getters
}
```
One way that I can think of is you can use an entity lifecycle callback event like #PostLoad which is called when the entity is loaded in the persistence context, and initialize your hashed field from the id.
E.g.
#Entity
public class MyClass {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
Long id;
String hashID;
#PostLoad
public void postLoad() {
// Here id is initialized
this.hashID = Utils.hashID(id);
}
}

Spring hibernate CrudRepository make the save method update based on a unique constraint

I understand by default that the CrudRepository.save method inserts and updates based on a primary key.
Consider the following entity
#Entity
public class BookEntity {
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#Basic
#Column(name = "isbn", unique = true)
private String isbn;
#Basic
#Column(name = "title")
private String title;
#Basic
#Column(name = "author")
private String author;
#Basic
#Column(name = "publication_date")
private Date publicationDate;
#Basic
#Column(name = "rank")
private Integer rank;
}
Because I can not have books with the same isbn in the table and I don't want to worry about generating ids I normally post the following json to an enpoint
{
"isbn": "10932011",
"title": "harry",
"author": "jk",
"publicationDate": "2018-10-10",
"rank": 1000
}
Which returns the following with an auto generated id
{
"id": 3,
"isbn": "10932011",
"title": "harry",
"author": "jk",
"publicationDate": "2018-10-10T00:00:00.000+0000",
"rank": 1000
}
If I make second call using the same isbn I'll get a constraint error
{
"isbn": "10932011",
"title": "harry",
"author": "jk3",
"publicationDate": "2018-10-10",
"rank": 1000
}
But I would like to in fact update the book with the same isbn and not have to specify the auto generated id in the post json as this is not important to me. Is there a way of doing this without having to add logic to a service class?
You can get the BookEntity, change it and save it.
Since your isbn is unique, you can do something like this:
BookEntity book = bookRepository.findByIsbn(isbn);
book.setWhateverFieldYouWant(value);
bookRepository.save(book);.
Or another solution
You can create a method with #Query annotation in BookRepository:
#Query("UPDATE BookEntity b SET b.whateverField = :value WHERE b.isbn = :isbn")
void updateBook(#Param("value") String value, #Param("isbn") String isbn);
and call it from service:
bookRepository.updateBook(value, isbn);
Since you are using Hibernate, you can also take a look at the NaturalId API Hibernate provides. In addition to your generated ID you annotate your isbn as a #NaturalId and then use the NaturalId API to retrieve your books.
#Entity
public class BookEntity {
#Id
#Column(name = "id")
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#NaturalId
#Column(name = "isbn")
private String isbn;
Load example:
BookEntity book = entityManager.unwrap(Session.class)
.bySimpleNaturalId(BookEntity.class)
.load("your isbn goes here");
For further reading on NaturalIds take a look at
this article (its sample is with isbns) or this one.
A big plus of NaturalIds is you can benefit of hibernate caching mechanisms.
The best way I have found is based in this article using #NaturalId:
Step 1. Create a NaturalRepository interface (to fine-tune the built-in JpaRepository):
#NoRepositoryBean
interface NaturalRepository<T, ID extends Serializable> extends JpaRepository<T, ID> {
Optional<T> findBySimpleNaturalId(ID naturalId);
}
Step 2. Extend SimpleJpaRepository and implement NaturalRepository(to customize it):
import org.hibernate.Session;
#Transactional(readOnly = true)
class NaturalRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements NaturalRepository<T, ID> {
final EntityManager entityManager;
NaturalRepositoryImpl(JpaEntityInformation entityInformation, EntityManager entityManager) {
super(entityInformation, entityManager);
this.entityManager = entityManager;
}
#Override
Optional<T> findBySimpleNaturalId(ID naturalId) {
return entityManager.unwrap(Session.class)
.bySimpleNaturalId(this.getDomainClass())
.loadOptional(naturalId);
}
}
Step 3. Tell Spring to use this customized repository base:
#SpringBootApplication
#EnableJpaRepositories(repositoryBaseClass = NaturalRepositoryImpl.class)
class MySpringApp {
...
Step 4. Time to use #NaturalId:
#Entity
public class BookEntity {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private int id;
#NaturalId
#Column(name = "isbn")
private String isbn;
Step 5. Define a classical Spring repository:
#Repository
interface BookRepository extends NaturalRepository<Book, String> {
// You can add other methods here like:
List<Book> findByTitle(String title);
List<Book> findByAuthor(String author);
}
Take note:
its now using #Repository (instead of #RepositoryRestResource)
that it should now extends NaturalRepository (instead of CrudRepository)
the second type should be String
Step 6. Finally:
...
#Autowired
BookRepository repository;
...
String isbn = "...";
try {
Book book = repository.findBySimpleNaturalId(isbn).get();
book.author = "gal";
repository.save(book);
} catch(NoSuchElementException notfound) {
...
}
The nice thing is that you can reuse the NaturalRepository and NaturalRepositoryImpl in any other project or with any other repository for other tables.

How to display the error message obtained from #Column (unique = true)?

I use JPA technology in my project.
Here is the entity:
#Entity
public class Cars implements Serializable {
#Id
#GeneratedValue(strategy=GenerationType.IDENTITY)
#Column(name="idCars")
private long idCars;
#Column(name="nomCars",unique=true)
private String nomCars;
}
Here is the controller:
#RestController
#RequestMapping(value="/Cars")
public class CarsRestController {
#RequestMapping(value="/AddCars")
public Cars AddCars(Cars cr){
return repository.save(cr);
}
}
I used the unique = true attribute to check if the new Cars object exists or does not exist in the DB. In case the object exists I want to retrieve this error message to display it after the web user.
Thanks,

Make the properties of a mapped entity read-only

Consider the following entities:
#Entity
public class MyEntity implements Serializable {
#Id
private String id;
#OneToOne
private Person person;
}
#Entity
public class Person implements Serializable {
#Id
private String id;
private String name;
// ... many more properties which should be read-only
}
Sometimes the name of the mapped Person is modified, and Hibernate generates update statements. But I don't want these to happen.
Is there a way to mark the properties of the mapped person read-only?
Changes to the id of the mapped person (I mean, a different person is attached to MyEntity) should however still make Hibernate update MyEntity.
#Column(updatable=false)
From the docs:updatable (optional): whether or not the column will be part of the update statement (default true)

Categories