In this article, we will learn how to create a Spring Boot JPA One To One mapping using Spring Data JPA with Foreign Key. In this approach, we will have two tables with different primary keys. The primary key of the STUDENT table (ID) will act as a foreign key for the PASSPORT table and PASSPORT table will have its own primary key (ID).
Creating table
Create STUDENT & PASSPORT Table, simply Copy and Paste the following MySQL query in the query editor to get the table created.
CREATE TABLE STUDENT ( ID INT NOT NULL, NAME VARCHAR(50) NOT NULL, PRIMARY KEY(ID) ); insert into STUDENT (ID, NAME) values(1, 'John'); insert into STUDENT (ID, NAME) values(2, 'Smith'); insert into STUDENT (ID, NAME) values(3, 'Tom'); insert into STUDENT (ID, NAME) values(4, 'Bruce'); CREATE TABLE PASSPORT ( ID INT NOT NULL, NUMBER VARCHAR(50) NOT NULL, STUDENT_ID INT NOT NULL, FOREIGN KEY (STUDENT_ID) REFERENCES STUDENT (ID) ); insert into PASSPORT (ID, NUMBER, STUDENT_ID) values(1,'A123456', 1); insert into PASSPORT (ID, NUMBER, STUDENT_ID) values(2, 'B123456', 2); insert into PASSPORT (ID, NUMBER, STUDENT_ID) values(3, 'C123456', 3);
Folder Structure:
- Create a Maven project (maven-archetype-quickstart) “SpringBootOneToOne” and create packages for our source files “com.javainterviewpoint“, “com.javainterviewpoint.controller”, “com.javainterviewpoint.model” and “com.javainterviewpoint.repository” under src/main/java
- Now add the following dependency in the POM.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javainterviewpoint</groupId> <artifactId>SpringBootOneToOne</artifactId> <packaging>war</packaging> <version>0.0.1-SNAPSHOT</version> <name>SpringBootOneToOne Maven Webapp</name> <url>http://maven.apache.org</url> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.7.RELEASE</version> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
- Create application.properties file under src/main/resources directory and provide the MySQL Connections and JPA Properties
#MySQL Connection settings spring.datasource.driver = com.mysql.jdbc.Driver spring.datasource.url = jdbc:mysql://localhost:3306/mydb spring.datasource.username = root spring.datasource.password = root #JPA properties spring.jpa.show-sql = true spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5Dialect
The spring-boot-starter-parent is a special starter, it provides useful Maven defaults. Since we are developing a web application we also need to add a spring-boot-starter-web dependency. This will add dependencies such Tomcat, Jackson, Spring boot etc which is required to create a web app.
spring-boot-starter-data-jpa uses Spring Data JPA with Hibernate as the implementation
Spring Boot JPA One To One Example with MySQL – Unidirectional
Dependency Tree
[INFO] -------------< com.javainterviewpoint:SpringBootOneToOne >-------------- [INFO] Building SpringBootOneToOne Maven Webapp 0.0.1-SNAPSHOT [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- maven-dependency-plugin:3.1.1:tree (default-cli) @ SpringBootOneToOne --- [INFO] com.javainterviewpoint:SpringBootOneToOne:war:0.0.1-SNAPSHOT [INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.1.7.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:2.1.7.RELEASE:compile [INFO] | | +- org.springframework:spring-aop:jar:5.1.9.RELEASE:compile [INFO] | | \- org.aspectj:aspectjweaver:jar:1.9.4:compile [INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.1.7.RELEASE:compile [INFO] | | +- com.zaxxer:HikariCP:jar:3.2.0:compile [INFO] | | \- org.springframework:spring-jdbc:jar:5.1.9.RELEASE:compile [INFO] | +- javax.transaction:javax.transaction-api:jar:1.3:compile [INFO] | +- javax.xml.bind:jaxb-api:jar:2.3.1:compile [INFO] | | \- javax.activation:javax.activation-api:jar:1.2.0:compile [INFO] | +- org.hibernate:hibernate-core:jar:5.3.10.Final:compile [INFO] | | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile [INFO] | | +- javax.persistence:javax.persistence-api:jar:2.2:compile [INFO] | | +- org.javassist:javassist:jar:3.23.2-GA:compile [INFO] | | +- net.bytebuddy:byte-buddy:jar:1.9.16:compile [INFO] | | +- antlr:antlr:jar:2.7.7:compile [INFO] | | +- org.jboss:jandex:jar:2.0.5.Final:compile [INFO] | | +- com.fasterxml:classmate:jar:1.4.0:compile [INFO] | | +- org.dom4j:dom4j:jar:2.1.1:compile [INFO] | | \- org.hibernate.common:hibernate-commons-annotations:jar:5.0.4.Final:compile [INFO] | +- org.springframework.data:spring-data-jpa:jar:2.1.10.RELEASE:compile [INFO] | | +- org.springframework.data:spring-data-commons:jar:2.1.10.RELEASE:compile [INFO] | | +- org.springframework:spring-orm:jar:5.1.9.RELEASE:compile [INFO] | | +- org.springframework:spring-context:jar:5.1.9.RELEASE:compile [INFO] | | +- org.springframework:spring-tx:jar:5.1.9.RELEASE:compile [INFO] | | +- org.springframework:spring-beans:jar:5.1.9.RELEASE:compile [INFO] | | \- org.slf4j:slf4j-api:jar:1.7.26:compile [INFO] | \- org.springframework:spring-aspects:jar:5.1.9.RELEASE:compile [INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.1.7.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.1.7.RELEASE:compile [INFO] | | +- org.springframework.boot:spring-boot:jar:2.1.7.RELEASE:compile [INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.1.7.RELEASE:compile [INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.1.7.RELEASE:compile [INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile [INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile [INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.11.2:compile [INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.11.2:compile [INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.26:compile [INFO] | | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile [INFO] | | \- org.yaml:snakeyaml:jar:1.23:runtime [INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.1.7.RELEASE:compile [INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.9.9:compile [INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.0:compile [INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.9.9:compile [INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.9.9:compile [INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.9.9:compile [INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.9.9:compile [INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.1.7.RELEASE:compile [INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.22:compile [INFO] | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.22:compile [INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.22:compile [INFO] | +- org.hibernate.validator:hibernate-validator:jar:6.0.17.Final:compile [INFO] | | \- javax.validation:validation-api:jar:2.0.1.Final:compile [INFO] | +- org.springframework:spring-web:jar:5.1.9.RELEASE:compile [INFO] | \- org.springframework:spring-webmvc:jar:5.1.9.RELEASE:compile [INFO] | \- org.springframework:spring-expression:jar:5.1.9.RELEASE:compile [INFO] +- mysql:mysql-connector-java:jar:8.0.17:compile [INFO] \- org.springframework.boot:spring-boot-starter-test:jar:2.1.7.RELEASE:test [INFO] +- org.springframework.boot:spring-boot-test:jar:2.1.7.RELEASE:test [INFO] +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.1.7.RELEASE:test [INFO] +- com.jayway.jsonpath:json-path:jar:2.4.0:test [INFO] | \- net.minidev:json-smart:jar:2.3:test [INFO] | \- net.minidev:accessors-smart:jar:1.2:test [INFO] | \- org.ow2.asm:asm:jar:5.0.4:test [INFO] +- junit:junit:jar:4.12:test [INFO] +- org.assertj:assertj-core:jar:3.11.1:test [INFO] +- org.mockito:mockito-core:jar:2.23.4:test [INFO] | +- net.bytebuddy:byte-buddy-agent:jar:1.9.16:test [INFO] | \- org.objenesis:objenesis:jar:2.6:test [INFO] +- org.hamcrest:hamcrest-core:jar:1.3:test [INFO] +- org.hamcrest:hamcrest-library:jar:1.3:test [INFO] +- org.skyscreamer:jsonassert:jar:1.5.0:test [INFO] | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test [INFO] +- org.springframework:spring-core:jar:5.1.9.RELEASE:compile [INFO] | \- org.springframework:spring-jcl:jar:5.1.9.RELEASE:compile [INFO] +- org.springframework:spring-test:jar:5.1.9.RELEASE:test [INFO] \- org.xmlunit:xmlunit-core:jar:2.6.3:test [INFO] ------------------------------------------------------------------------
Defining JPA Entities – Shared Primary Key in JPA
Passport Entity
Create Passport.java under the package com.javainterviewpoint.model
package com.javainterviewpoint.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.Id; import javax.persistence.OneToOne; import javax.persistence.Table; @Entity @Table(name="PASSPORT") public class Passport { @Id private int id; private String number; @OneToOne(cascade = CascadeType.ALL) private Student student; public Passport() { super(); } public Passport(int id, String number, Student student) { super(); this.id = id; this.number = number; this.student = student; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getNumber() { return number; } public void setNumber(String number) { this.number = number; } public Student getStudent() { return student; } public void setStudent(Student student) { this.student = student; } @Override public String toString() { return "Passport [id=" + id + ", number=" + number + ", student=" + student + "]"; } }
Our Passport class is a simple POJO class consisting of the getters and setters for the Passport properties (id, name).
In the POJO class, we have used the below JPA Annotations.
- @Entity – This annotation will mark our Passport class as an Entity Bean.
- @Table – @Table annotation will map our class to the corresponding database table. You can also specify other attributes such as indexes, catalog, schema, uniqueConstraints. The @Table annotation is an optional annotation if this annotation is not provided then the class name will be used as the table name.
- @Id – The @Id annotation marks the particular field as the primary key of the Entity.
- @OneToOne – This annotation indicates that the Passport entity owns the relationship. cascade = CascadeType.ALL , This attribute applies the cascading effect to the related entity as well. Whenever we perform any update/modification operation on the Passport entity it will be cascaded to the Student entity as well.
Student Entity
Create Student.java also under the package com.javainterviewpoint.model
package com.javainterviewpoint.model; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "STUDENT") public class Student { @Id private int id; private String name; public Student() { super(); } public Student(int id, String name) { super(); this.id = id; this.name = name; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Student [id=" + id + ", name=" + name + "]"; } }
Adding the Repository – CrudRepository
We simply have created an interface PassportRepository which in turn extends CrudRepository that’s all we have to do, Spring Data will automatically create an implementation in the runtime.
CrudRepository will by default provide you with the generic methods like save(), findAll(), insert(), etc.. Out-of-the-box we can also add our custom methods and Spring Data has the query builder mechanism built in which strips the prefixes find…By, read…By, and get…By
We have defined a single method getStudent() which retrieves the Student object from the STUDENT table.
package com.javainterviewpoint.repository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.CrudRepository; import org.springframework.stereotype.Repository; import com.javainterviewpoint.model.Passport; import com.javainterviewpoint.model.Student; @Repository public interface PassportRepository extends CrudRepository<Passport, Integer> { @Query("select s from Student s where id= :studentid") public Student getStudent(int studentid); }
Rest Controller – PassportController
package com.javainterviewpoint.controller; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.javainterviewpoint.model.Passport; import com.javainterviewpoint.model.Student; import com.javainterviewpoint.repository.PassportRepository; @RestController public class PassportController { @Autowired public PassportRepository passportRepository; @GetMapping("/passport/{id}") public Passport getPassportById(@PathVariable("id") int id) { Passport passport = passportRepository.findById(id).get(); return passport; } @GetMapping("/passports") public List<Passport> getAllPassport() { List<Passport> passportList = new ArrayList<>(); Iterable<Passport> passport = passportRepository.findAll(); for (Passport p : passport) { passportList.add(p); } return passportList; } @PostMapping("/passport/{studentid}") public Passport createPassport(@PathVariable("studentid") int studentid, @RequestBody Passport passport) { Student student = passportRepository.getStudent(studentid); passport.setStudent(student); passportRepository.save(passport); return getPassportById(passport.getId()); } @DeleteMapping("/passport/{id}") public List<Passport> deletePassport(@PathVariable("id") int id) { passportRepository.deleteById(id); return getAllPassport(); } }
We have annotated our “StudentController” class with @RestController, In Spring 4 @RestController annotation is introduced it is a combination of @Controller + @ResponseBody. So when using @RestController, you do not need to use @ResponseBody it is optional now
Main Application – App.java
package com.javainterviewpoint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class App { public static void main(String[] args) { SpringApplication.run(App.class, args); } }
The App class main() method is the triggering point of our application, it in-turn calls Spring Boot’s SpringApplication class run() method which bootstrap our App application and starts the tomcat server. We need to pass our App.class as an argument to our run() method.
Let’s Run our Application!
Select the Project –>Run As –> Run Configuration –>Maven –> New Configuration. In the Main tab, key in the Goals as “spring-boot:run” and click on Run.
Get a Single Passport
Using POSTMAN or any other Rest Client and place a GET request on the URL http://localhost:8080/passport/1
Get All the Passport
Using POSTMAN or any other Rest Client and place a GET request on the URL http://localhost:8080/passports
Create a Passport
Place a POST request on the URL http://localhost:8080/passport/4
Make sure to pass the passport details in the body of the request
{
“id” : 4,
“number” : “D123456”
}
We are passing the Student id ‘4’ in the above URL for which the Passport needs to be created.
Delete Passport
Place a DELETE request on the URL http://localhost:8080/passport/4
Spring Boot JPA One To One Example with MySQL – Bidirectional
Till now what we have seen is One To One unidirectional approach. We are able to access Student details through Passport entity only, whereas the vice-versa Passport details through the Student entity is not possible.
In order to make it bidirectional, we need to make some minor modifications to the Student Entity class and create a new repository for Student.
Student Entity
package com.javainterviewpoint.model; import javax.persistence.CascadeType; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.OneToOne; import javax.persistence.Table; import com.fasterxml.jackson.annotation.JsonIdentityInfo; import com.fasterxml.jackson.annotation.ObjectIdGenerators; @Entity @Table(name = "STUDENT") @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id") public class Student { @Id private int id; private String name; @OneToOne(mappedBy = "student", cascade = CascadeType.ALL) private Passport passport; public Student() { super(); } public Student(int id, String name, Passport passport) { super(); this.id = id; this.name = name; this.passport = passport; } public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Passport getPassport() { return passport; } public void setPassport(Passport passport) { this.passport = passport; } @Override public String toString() { return "Student [id=" + id + ", name=" + name + ", passport=" + passport + "]"; } }
We didn’t have Passport class reference in Student entity before, now we have added it and annotated it with @OneToOne annotation and we have added mappedBy attribute to it.
mappedBy =”student” – The mappedBy attribute on the Student entity informs Spring Data that it is not the owner for the relationship.
We have also added a new annotation @JsonIdentityInfo in both the Student and Passport entities in order to prevent Cyclic dependency and Stackoverflow issue
We will be getting the below error when we have not added the annotation @JsonIdentityInfo due to Cyclic dependency
java.lang.StackOverflowError: null at java.lang.ClassLoader.defineClass1(Native Method) ~[na:1.8.0_211] at java.lang.ClassLoader.defineClass(ClassLoader.java:763) ~[na:1.8.0_211] at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) ~[na:1.8.0_211] at java.net.URLClassLoader.defineClass(URLClassLoader.java:468) ~[na:1.8.0_211] at java.net.URLClassLoader.access$100(URLClassLoader.java:74) ~[na:1.8.0_211] at java.net.URLClassLoader$1.run(URLClassLoader.java:369) ~[na:1.8.0_211] at java.net.URLClassLoader$1.run(URLClassLoader.java:363) ~[na:1.8.0_211] at java.security.AccessController.doPrivileged(Native Method) ~[na:1.8.0_211] at java.net.URLClassLoader.findClass(URLClassLoader.java:362) ~[na:1.8.0_211] at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_211] at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) ~[na:1.8.0_211] at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_211] at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:737) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:727) ~[jackson-databind-2.9.9.jar:2.9.9] at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:719) ~[jackson-databind-2.9.9.jar:2.9.9]
StudentRepository
Let’s create a simple CrudRepository for our Student Entity.
package com.javainterviewpoint.repository; import org.springframework.data.repository.CrudRepository; import com.javainterviewpoint.model.Student; public interface StudentRepository extends CrudRepository<Student, Integer> { }
StudentController
package com.javainterviewpoint.controller; import java.util.ArrayList; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import com.javainterviewpoint.model.Passport; import com.javainterviewpoint.model.Student; import com.javainterviewpoint.repository.StudentRepository; @RestController public class StudentController { @Autowired public StudentRepository studentRepository; @GetMapping("/student/{id}") public Student getStudentById(@PathVariable("id") int id) { Student student = studentRepository.findById(id).get(); return student; } @GetMapping("/students") public List<Student> getAllStudents() { List<Student> studentList = new ArrayList<>(); Iterable<Student> student = studentRepository.findAll(); for (Student s : student) { studentList.add(s); } return studentList; } @PostMapping("/student") public Student createStudent(@RequestBody Student student) { Passport passport = new Passport(); passport.setId(4); passport.setNumber("D123456"); passport.setStudent(student); student.setPassport(passport); studentRepository.save(student); return getStudentById(student.getId()); } @DeleteMapping("/student/{id}") public List<Student> deletePassport(@PathVariable("id") int id) { studentRepository.deleteById(id); return getAllStudents(); } }
Let’s test our code
Get a particular Student
Place a GET request on the URL http://localhost:8080/student/2
Get All Students
Place a GET request on the URL http://localhost:8080/students
Create a Student
Place a POST request on the URL http://localhost:8080/student
Pass the Student details in the body of the request.
{
“id” : 4,
“name” : “Darcy”
}
Delete a Student
Place a DELETE request on the URL http://localhost:8080/student/4
Hope this article helps you in getting some understanding on Spring Boot JPA One To One. Happy Learning !!
Leave a Reply