1

I have two entities: Student and StudentDetails. A Student can have only one or no StudentDetails. When I submit a form to save a Student, a StudentDetails is also being saved. That's okay if I send data for StudentDetails entity. But it's happening even if I don't send any data for StudentDetails entity. Images after saving three Students:

student table:

enter image description here

student_details table:

enter image description here

I want row 1,3 would not be saved in student_details table. And in student table student_details_id would be null for these two.

How can I implement @OneToOne relationship where parent/owner entity can have optional (One if exists, Zero if doesn't exist) child entity?

Here are my codes:

Student.java

@Entity
@Table(name = "student")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "student_name")
    private String studentName;

    @Column(name = "student_roll")
    private String studentRoll;

    @Column(name = "student_class")
    private String studentClass;

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "student_details_id")
    private StudentDetails studentDetails;

    // Constructors, Getters and Setters
}

StudentDetails.java

@Entity
@Table(name = "student_details")
public class StudentDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private Long id;

    @Column(name = "student_contact")
    private String studentContact;

    @Column(name = "student_email")
    private String studentEmail;

    @Column(name = "student_address")
    private String studentAddress;

    @OneToOne(mappedBy = "studentDetails", cascade = { CascadeType.PERSIST, CascadeType.MERGE, CascadeType.DETACH, CascadeType.REFRESH })
    @JsonIgnore
    private Student student;

    // Constructors, Getters and Setters
}

StudentController.java

@Controller
@RequestMapping("/students")
public class StudentController {

    @Autowired
    private StudentRepository studentRepository;

    @GetMapping("/add")
    public String add(Model theModel) {
        Student theStudent = new Student();
        theModel.addAttribute("theStudent", theStudent);
        return "student/student_add_form";
    }

    @PostMapping("/create")
    public String create(@ModelAttribute("theStudent") Student theStudent) {
        theStudent.setId((long) 0);
        studentRepository.save(theStudent);
        return "redirect:/students/index";
    }
}

student_add_form.html

<!doctype html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">

<head>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/css/bootstrap.css">
<title>Student List</title>
</head>

<body>
    <div class="container">
        <div class="row">
            <div class="col-9">
                <h1 th:text="'Add New Student'"></h1>
            </div>
            <div class="col-3">
                <a th:href="@{/students/index}" class="btn btn-success">View Student List</a>
            </div>
        </div>
        <div class="row">
            <form action="#" th:action="@{/students/create}" th:object="${theStudent}" th:method="POST" class="col-12">
                <div class="row">
                    <div class="form-group col-3">
                        <label for="studentName">Name:</label> <input type="text" class="form-control"
                            id="studentName" name="studentName" placeholder="Enter Student's Name" th:field="*{studentName}">
                    </div>
                    <div class="form-group col-3">
                        <label for="studentClass">Class:</label> <input type="text" class="form-control"
                            id="studentClass" name="studentClass" placeholder="Enter Student's Class" th:field="*{studentClass}">
                    </div>
                    <div class="form-group col-3">
                        <label for="studentRoll">Roll:</label> <input type="text" class="form-control"
                            id="studentRoll" name="studentRoll" placeholder="Enter Student's Roll" th:field="*{studentRoll}">
                    </div>
                    <div class="form-group col-3">
                        <label for="studentContact">Contact:</label> <input type="text" class="form-control"
                            id="studentContact" name="studentContact" placeholder="Enter Student's Contact"
                            th:field="*{studentDetails.studentContact}">
                    </div>
                </div>
                <div class="row">
                    <div class="form-group col-3">
                        <label for="studentEmail">Email:</label> <input type="text" class="form-control"
                            id="studentEmail" name="studentEmail" placeholder="Enter Student's Email"
                            th:field="*{studentDetails.studentEmail}">
                    </div>
                    <div class="form-group col-6">
                        <label for="studentAddress">Address:</label> <input type="text" class="form-control"
                            id="studentAddress" name="studentAddress" placeholder="Enter Student's Address"
                            th:field="*{studentDetails.studentAddress}">
                    </div>
                                        <div class="form-group col-3">
                        <label></label> <input type="submit" class="form-control btn btn-success" id="saveStudent"
                            name="saveStudent" value="Save Student">
                    </div>
                </div>
            </form>
        </div>
    </div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.3.1/js/bootstrap.js"></script>
    <script>

    </script>
</body>
</html>
Partho63
  • 3,117
  • 2
  • 21
  • 39
  • `Column 'student_id' cannot be null` is a database constraint. You need to alter the column to allow nulls. And for an optional relationship, use `optional = true` in your `@OneToOne` – XtremeBaumer Sep 24 '19 at 07:47
  • Its unclear in whcih table you see the error. Maybe its `Student.studentId` maybe its `StudentDetails.student`. What table does it originate from? Also the issue I always have with `cascade = CascadeType.ALL` is persisting a new entity. I believe that the `StudentDetails` are to be persisted first, but the `Student` is not yet persisted. Though its not clear from your question – XtremeBaumer Sep 24 '19 at 07:57
  • Have you checked if `StudentDetails` has an `Student` set? – XtremeBaumer Sep 24 '19 at 08:06
  • What is the request you are sending? Does the student being saved have a student details record? If so, it is because you have not set the inverse side of the relationship. i.e. StudentDetails has no student set. In a bidirectional relationship it is your responsibility to ensure both sides of the in-memory model are set correctly. – Alan Hay Sep 24 '19 at 10:00
  • The only way a SudentDetails record would be persisted would be if you have created one for the Student. You say you haven't but you must be mistaken. What is the request you are sending because Student.studentDetails != null at point of saving. – Alan Hay Sep 24 '19 at 10:24
  • Additionally, why are you doing this? `theStudent.setId((long) 0);` – Alan Hay Sep 24 '19 at 10:27
  • I can see perfectly well your problem. As I said previously, **Hibernate is only going to write a record to StudentDetails if Student.studentDetails != null**. The issue is then with your code setting a student details record. Also, why, exactly, are you doing this: `theStudent.setId((long) 0);` What problem do you believe that is solving? – Alan Hay Sep 24 '19 at 11:23
  • @AlanHay Sorry for late response. `theStudent.setId((long) 0);` - this is not solving any problem. It's just to make sure a new entry is being created instead of updating an existing entry. Though it is not needed I put it for extra (!) safety. Is this causing the problem? I am not sending any data for `Student.studentDetails`, then why is **`Student.studentDetails != null`** ? ! – Partho63 Sep 25 '19 at 05:37

1 Answers1

3

You state that you are not sending any details for StudentDetails however your form clearly has fields for this data e.g:

<div class="form-group col-3">
    <label for="studentEmail">Email:</label> <input type="text" class="form-control"
        id="studentEmail" name="studentEmail" placeholder="Enter Student's Email"
        th:field="*{studentDetails.studentEmail}">
</div>

Even if the form fields are left blank, empty strings will be submitted for these values and Spring will therefore bind a new StudentDetails instance on the Student model attribute and set the bound fields accordingly i.e. to empty strings.

To prevent this tell Spring to trim empty Strings to null: if there are then, at the time of binding, no non-null properties in the request pertaining to StudentDetails then the StudentDetails instance on the Student model attribute will not be set by Spring.

You can do this globally or on a per controller basis. See, for example:

Can spring mvc trim all strings obtained from forms?

I am not sure that Spring has always behaved like this i.e. auto instantiating non-simple nested properties however I have tested in Boot 2.1.8 (MVC 5.1.9) and this is what is happening.

Alan Hay
  • 22,665
  • 4
  • 56
  • 110