In my real project there is an entity with ~15 properties (columns). I need to store ~10 more properties related to the entity. To avoid creating of new columns in the same table, one approach is to store the new properties in another entity and to link them with One-to-One relationship. Here I have found another approach, which implies to store an emdebbable object in a secondary table. The problem is that an embeddable object with not initialized properties is not being persistent to database. I have reproduced the issue in demo project:
@Entity
@Table(name = "STUDENTS")
@SecondaryTable(
name = ADDRESSES,
pkJoinColumns = @PrimaryKeyJoinColumn(name = "STUDENT_ID")
)
public class Student {
@Id
@GeneratedValue
private Long id;
private String firstName;
private String lastName;
@Embedded // not needed
private Address address;
//constructors, getters and setters
@Embeddable
public class Address {
public static final String ADDRESSES = "ADDRESSES";
@Column(table = ADDRESSES)
private String country;
@Column(table = ADDRESSES)
private String city;
@Column(table = ADDRESSES)
private String street;
@Column(table = ADDRESSES)
private Integer houseNumber;
//constructors, getters and setters
@Service
@Transactional
public class StudentService {
@Autowired
private StudentRepository studentRepository;
@Transactional(readOnly = true)
public List<Student> findAll() {
return studentRepository.findAll();
}
public Student createStudentWithEmptyAddress() {
Student student = new Student("Caleb", "Baker", new Address());
return studentRepository.save(student);
}
public Student createStudentWithNonEmptyAddress() {
Address address = new Address("France", "Paris", "Rue Vieille Du Temple", 88);
Student student = new Student("Cayden", "Hoover", address);
return studentRepository.save(student);
}
public Student findById(Long id) {
return studentRepository
.findById(id)
.orElseThrow(() -> new RuntimeException("Student with id: " + id + " was not found"));
}
public Student updateStudentWithEmptyAddress(Long id) {
Student student = findById(id);
student.setAddress(new Address());
return student;
}
public Student updateStudentWithNonEmptyAddress(Long id) {
Student student = findById(id);
Address address = new Address("USA", "New York", "Stanton Street", 17);
student.setAddress(address);
return student;
}
}
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@GetMapping("/all")
@ResponseStatus(OK)
public List<Student> findAll() {
return studentService.findAll();
}
@GetMapping("/{id}")
@ResponseStatus(OK)
public Student findOne(@PathVariable Long id) {
return studentService.findById(id);
}
@PostMapping("/emptyAddress")
@ResponseStatus(CREATED)
public Student createStudentWithEmptyAddress() {
return studentService.createStudentWithEmptyAddress();
}
@PostMapping("/nonEmptyAddress")
@ResponseStatus(CREATED)
public Student createStudentWithNonEmptyAddress() {
return studentService.createStudentWithNonEmptyAddress();
}
@PutMapping("/{id}/emptyAddress")
@ResponseStatus(OK)
public Student updateStudentWithEmptyAddress(@PathVariable Long id) {
return studentService.updateStudentWithEmptyAddress(id);
}
@PutMapping("/{id}/nonEmptyAddress")
@ResponseStatus(OK)
public Student updateStudentWithNonEmptyAddress(@PathVariable Long id) {
return studentService.updateStudentWithNonEmptyAddress(id);
}
}
POST localhost:8080/student/nonEmptyAddress returns the expected output:
jdbc.sqlonly: insert into students (first_name, last_name, id) values ('Cayden', 'Hoover', 4)
jdbc.sqlonly: insert into addresses (city, country, house_number, street, student_id) values ('Paris', 'France', 88, 'Rue Vieille Du Temple', 4)
{
"id": 4,
"firstName": "Cayden",
"lastName": "Hoover",
"address": {
"country": "France",
"city": "Paris",
"street": "Rue Vieille Du Temple",
"houseNumber": 88
}
}
GET localhost:8080/student/4 returns the result above. Now let's create a student with an Address object where all properties are not initialized:
POST localhost:8080/student/nonEmptyAddress returns the expected output:
jdbc.sqlonly: insert into students (first_name, last_name, id) values ('Caleb', 'Baker', 5)
jdbc.sqlonly: insert into addresses (city, country, house_number, street, student_id) values (NULL, NULL, NULL, NULL, 5)
{
"id": 5,
"firstName": "Caleb",
"lastName": "Baker",
"address": {
"country": null,
"city": null,
"street": null,
"houseNumber": null
}
}
However, when I call GET endpoint, I receive another result. GET localhost:8080/student/5 :
jdbc.sqlonly: select student0_.id as id1_1_0_, student0_.first_name as first_na2_1_0_, student0_.last_name as last_nam3_1_0_, student0_1_.city as city1_0_0_, student0_1_.country as country2_0_0_, student0_1_.house_number as house_nu3_0_0_, student0_1_.street as street4_0_0_ from students student0_ left outer join addresses student0_1_ on student0_.id=student0_1_.student_id where student0_.id=5
{
"id": 5,
"firstName": "Caleb",
"lastName": "Baker",
"address": null
}
Content of ADRESSES table:
CITY COUNTRY HOUSE_NUMBER STREET STUDENT_ID
Paris France 88 Rue Vieille Du Temple 4
null null null null 5
The address is null despite the presence of the second record in ADRESSES table. Updating a student with an address when there is at least one initialized property in an Address object works as expected. However, if you update a student with an address where all properties are null (PUT localhost:8080/student/3/emptyAddress), then the table ADDRESSES is not even being updated.
What is wrong in the example above? I assume that it is not right approach to use @Embeddable and @Secondary table in order achieve the desired result.