DEV Community

Full Stack Hacker
Full Stack Hacker

Posted on • Edited on

2 1

Build API for CRUD application with Spring Boot JPA + H2

Project Directory

Let me explain it briefly.

  • Tutorial data model class corresponds to entity and table tutorials.
  • TutorialRepository is an interface that extends JpaRepository for CRUD methods and custom finder methods. It will be autowired in TutorialController.
  • TutorialController is a RestController which has request mapping methods for RESTful requests such as: getAllTutorials, createTutorial, updateTutorial, deleteTutorial, findByPublished…
  • Configuration for Spring Datasource, JPA & Hibernate in application.properties.
  • pom.xml contains dependencies for Spring Boot and H2 Database.

Maven

  • pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.1</version>
		<relativePath/> 
	</parent>
	<groupId>com.example</groupId>
	<artifactId>springbootjpah2</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>spring-boot-jpa-h2</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>javax.persistence</groupId>
			<artifactId>javax.persistence-api</artifactId>
			<version>2.2</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-jpa</artifactId>
			<version>2.7.1</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

Display the project dependencies.

> mvn dependency:tree
[INFO] Scanning for projects...
[INFO] 
[INFO] --------------------< com.example:springbootjpah2 >---------------------
[INFO] Building spring-boot-jpa-h2 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-dependency-plugin:3.3.0:tree (default-cli) @ springbootjpah2 ---
[INFO] com.example:springbootjpah2:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.1:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:2.7.1:compile
[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.1:compile
[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] |  |  \- org.yaml:snakeyaml:jar:1.30:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-json:jar:2.7.1:compile
[INFO] |  |  +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.3:compile
[INFO] |  |  |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.3:compile
[INFO] |  |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.13.3:compile
[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.3:compile
[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.3:compile
[INFO] |  |  \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.3:compile
[INFO] |  +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.7.1:compile
[INFO] |  |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.64:compile
[INFO] |  |  +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.64:compile
[INFO] |  |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.64:compile
[INFO] |  +- org.springframework:spring-web:jar:5.3.21:compile
[INFO] |  |  \- org.springframework:spring-beans:jar:5.3.21:compile
[INFO] |  \- org.springframework:spring-webmvc:jar:5.3.21:compile
[INFO] |     +- org.springframework:spring-aop:jar:5.3.21:compile
[INFO] |     +- org.springframework:spring-context:jar:5.3.21:compile
[INFO] |     \- org.springframework:spring-expression:jar:5.3.21:compile
[INFO] +- org.springframework.boot:spring-boot-devtools:jar:2.7.1:runtime
[INFO] |  +- org.springframework.boot:spring-boot:jar:2.7.1:compile
[INFO] |  \- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.1:compile
[INFO] +- com.h2database:h2:jar:2.1.214:runtime
[INFO] +- org.springframework.boot:spring-boot-configuration-processor:jar:2.7.1:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.7.1:test
[INFO] |  +- org.springframework.boot:spring-boot-test:jar:2.7.1:test
[INFO] |  +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.7.1:test
[INFO] |  +- com.jayway.jsonpath:json-path:jar:2.7.0:test
[INFO] |  |  +- net.minidev:json-smart:jar:2.4.8:test
[INFO] |  |  |  \- net.minidev:accessors-smart:jar:2.4.8:test
[INFO] |  |  |     \- org.ow2.asm:asm:jar:9.1:test
[INFO] |  |  \- org.slf4j:slf4j-api:jar:1.7.36:compile
[INFO] |  +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:compile
[INFO] |  |  \- jakarta.activation:jakarta.activation-api:jar:1.2.2:compile
[INFO] |  +- org.assertj:assertj-core:jar:3.22.0:test
[INFO] |  +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] |  +- org.junit.jupiter:junit-jupiter:jar:5.8.2:test
[INFO] |  |  +- org.junit.jupiter:junit-jupiter-api:jar:5.8.2:test
[INFO] |  |  |  +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] |  |  |  +- org.junit.platform:junit-platform-commons:jar:1.8.2:test
[INFO] |  |  |  \- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] |  |  +- org.junit.jupiter:junit-jupiter-params:jar:5.8.2:test
[INFO] |  |  \- org.junit.jupiter:junit-jupiter-engine:jar:5.8.2:test
[INFO] |  |     \- org.junit.platform:junit-platform-engine:jar:1.8.2:test
[INFO] |  +- org.mockito:mockito-core:jar:4.5.1:test
[INFO] |  |  +- net.bytebuddy:byte-buddy:jar:1.12.11:compile
[INFO] |  |  +- net.bytebuddy:byte-buddy-agent:jar:1.12.11:test
[INFO] |  |  \- org.objenesis:objenesis:jar:3.2:test
[INFO] |  +- org.mockito:mockito-junit-jupiter:jar:4.5.1: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.3.21:compile
[INFO] |  |  \- org.springframework:spring-jcl:jar:5.3.21:compile
[INFO] |  +- org.springframework:spring-test:jar:5.3.21:test
[INFO] |  \- org.xmlunit:xmlunit-core:jar:2.9.0:test
[INFO] +- javax.persistence:javax.persistence-api:jar:2.2:compile
[INFO] \- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.7.1:compile
[INFO]    +- org.springframework.boot:spring-boot-starter-aop:jar:2.7.1:compile
[INFO]    |  \- org.aspectj:aspectjweaver:jar:1.9.7:compile
[INFO]    +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.7.1:compile
[INFO]    |  +- com.zaxxer:HikariCP:jar:4.0.3:compile
[INFO]    |  \- org.springframework:spring-jdbc:jar:5.3.21:compile
[INFO]    +- jakarta.transaction:jakarta.transaction-api:jar:1.3.3:compile
[INFO]    +- jakarta.persistence:jakarta.persistence-api:jar:2.2.3:compile
[INFO]    +- org.hibernate:hibernate-core:jar:5.6.9.Final:compile
[INFO]    |  +- org.jboss.logging:jboss-logging:jar:3.4.3.Final:compile
[INFO]    |  +- antlr:antlr:jar:2.7.7:compile
[INFO]    |  +- org.jboss:jandex:jar:2.4.2.Final:compile
[INFO]    |  +- com.fasterxml:classmate:jar:1.5.1:compile
[INFO]    |  +- org.hibernate.common:hibernate-commons-annotations:jar:5.1.2.Final:compile
[INFO]    |  \- org.glassfish.jaxb:jaxb-runtime:jar:2.3.6:compile
[INFO]    |     +- org.glassfish.jaxb:txw2:jar:2.3.6:compile
[INFO]    |     +- com.sun.istack:istack-commons-runtime:jar:3.0.12:compile
[INFO]    |     \- com.sun.activation:jakarta.activation:jar:1.2.2:runtime
[INFO]    +- org.springframework.data:spring-data-jpa:jar:2.7.1:compile
[INFO]    |  +- org.springframework.data:spring-data-commons:jar:2.7.1:compile
[INFO]    |  +- org.springframework:spring-orm:jar:5.3.21:compile
[INFO]    |  \- org.springframework:spring-tx:jar:5.3.21:compile
[INFO]    \- org.springframework:spring-aspects:jar:5.3.21:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  2.500 s
[INFO] Finished at: 2022-07-21T09:35:05+07:00
[INFO] ------------------------------------------------------------------------

Overview of Spring Boot JPA + H2

These are APIs that we need to provide:

MethodsUrlsActions
POST/api/tutorialscreate new Tutorial
GET/api/tutorialsretrieve all Tutorials
GET/api/tutorials/:idretrieve a Tutorial by :id
PUT/api/tutorials/:idupdate a Tutorial by :id
DELETE/api/tutorials/:iddelete a Tutorial by :id
DELETE/api/tutorialsdelete all Tutorials
GET/api/tutorials/publishedfind all published Tutorials
GET/api/tutorials?title=[keyword]find all Tutorials which title contains keyword
  • We make CRUD operations & finder methods with Spring Data JPA’s JpaRepository.
  • The database will be H2 Database (in memory or on disk) by configuring project dependency & datasource.
  • Configure Spring Boot, JPA, h2, Hibernate: Under src/main/resources folder, open application.properties and write these lines.
    • spring.datasource.url: jdbc:h2:mem:[database-name] for In-memory database and jdbc:h2:file:[path/database-name] for disk-based database.

    • spring.datasource.username & spring.datasource.password properties are the same as your database installation.

    • Spring Boot uses Hibernate for JPA implementation, we configure H2Dialect for H2 Database

    • spring.jpa.hibernate.ddl-auto is used for database initialization. We set the value to update value so that a table will be created in the database automatically corresponding to defined data model. Any change to the model will also trigger an update to the table. For production, this property should be validate.

    • spring.h2.console.enabled=true tells the Spring to start H2 Database administration tool and you can access this tool on the browser: http://localhost:8080/h2-console.

    • spring.h2.console.path=/h2-ui is for H2 console’s url, so the default url http://localhost:8080/h2-console will change to http://localhost:8080/h2-ui.

Define Data Model

Data model is Tutorial with four fields: id, title, description, published.

package com.example.springbootjpah2.model;

import javax.persistence.*;

@Entity
@Table(name = "tutorials")
public class Tutorial {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;
    @Column(name = "title")
    private String title;
    @Column(name = "description")
    private String description;
    @Column(name = "published")
    private boolean published;
    public Tutorial() {
    }
    public Tutorial(String title, String description, boolean published) {
        this.title = title;
        this.description = description;
        this.published = published;
    }
    public long getId() {
        return id;
    }
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getDescription() {
        return description;
    }
    public void setDescription(String description) {
        this.description = description;
    }
    public boolean isPublished() {
        return published;
    }
    public void setPublished(boolean isPublished) {
        this.published = isPublished;
    }
    @Override
    public String toString() {
        return "Tutorial [id=" + id + ", title=" + title + ", desc=" + description + ", published=" + published + "]";
    }
}
  • @Entity annotation indicates that the class is a persistent Java class.
  • @Table annotation provides the table that maps this entity.
  • @Id annotation is for the primary key.
  • @GeneratedValue annotation is used to define generation strategy for the primary key. GenerationType.AUTO means Auto Increment field.
  • @Column annotation is used to define the column in database that maps annotated field.

Create Repository Interface

Let’s create a repository to interact with Tutorials from the database.

  • TutorialRepository.java
package com.example.springbootjpah2.repository;

import com.example.springbootjpah2.model.Tutorial;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;

public interface TutorialRepository extends JpaRepository<Tutorial, Long> {
    List<Tutorial> findByPublished(boolean published);
    List<Tutorial> findByTitleContaining(String title);
}

Now we can use JpaRepository’s methods: save(), findOne(), findById(), findAll(), count(), delete(), deleteById(),… without implementing these methods.

We also define custom finder methods: – findByPublished(): returns all Tutorials with published having value as input published. – findByTitleContaining(): returns all Tutorials which title contains input title.

Create Spring Rest APIs Controller

Finally, we create a controller that provides APIs for creating, retrieving, updating, deleting and finding Tutorials.

package com.example.springbootjpah2.controller;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

import com.example.springbootjpah2.model.Tutorial;
import com.example.springbootjpah2.repository.TutorialRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;


@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class TutorialController {

    @Autowired
    TutorialRepository tutorialRepository;

    @GetMapping("/tutorials")
    public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) {
        try {
            List<Tutorial> tutorials = new ArrayList<Tutorial>();

            if (title == null)
                tutorialRepository.findAll().forEach(tutorials::add);
            else
                tutorialRepository.findByTitleContaining(title).forEach(tutorials::add);

            if (tutorials.isEmpty()) {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }

            return new ResponseEntity<>(tutorials, HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @GetMapping("/tutorials/{id}")
    public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
        Optional<Tutorial> tutorialData = tutorialRepository.findById(id);

        if (tutorialData.isPresent()) {
            return new ResponseEntity<>(tutorialData.get(), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @PostMapping("/tutorials")
    public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
        try {
            Tutorial _tutorial = tutorialRepository
                    .save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), false));
            return new ResponseEntity<>(_tutorial, HttpStatus.CREATED);
        } catch (Exception e) {
            return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @PutMapping("/tutorials/{id}")
    public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
        Optional<Tutorial> tutorialData = tutorialRepository.findById(id);

        if (tutorialData.isPresent()) {
            Tutorial _tutorial = tutorialData.get();
            _tutorial.setTitle(tutorial.getTitle());
            _tutorial.setDescription(tutorial.getDescription());
            _tutorial.setPublished(tutorial.isPublished());
            return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(HttpStatus.NOT_FOUND);
        }
    }

    @DeleteMapping("/tutorials/{id}")
    public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) {
        try {
            tutorialRepository.deleteById(id);
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    @DeleteMapping("/tutorials")
    public ResponseEntity<HttpStatus> deleteAllTutorials() {
        try {
            tutorialRepository.deleteAll();
            return new ResponseEntity<>(HttpStatus.NO_CONTENT);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }

    }

    @GetMapping("/tutorials/published")
    public ResponseEntity<List<Tutorial>> findByPublished() {
        try {
            List<Tutorial> tutorials = tutorialRepository.findByPublished(true);

            if (tutorials.isEmpty()) {
                return new ResponseEntity<>(HttpStatus.NO_CONTENT);
            }
            return new ResponseEntity<>(tutorials, HttpStatus.OK);
        } catch (Exception e) {
            return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }
}
  • @CrossOrigin is for configuring allowed origins.
  • @RestController annotation is used to define a controller and to indicate that the return value of the methods should be be bound to the web response body.
  • @RequestMapping("/api") declares that all Apis’ url in the controller will start with /api.
  • We use @Autowired to inject TutorialRepository bean to local variable.

Run & Test

Click to main function to run application

Let’s open H2 console with url: http://localhost:8080/h2-ui

  • For In-memory database: You can get JBDC path in file application.properties
  • For on Disk database: Click on Connect button, then check H2 database, you can see things like this:

Create some Tutorials: H2 database tutorials table after that: Update some Tutorials:

The table data is changed:

Retrieve all Tutorials: Retrieve a Tutorial by Id:

Find all published Tutorials: Find all Tutorials which title contains string 'Hunger': Delete a Tutorial: Delete all Tutorials:

Source code

https://github.com/java-cake/spring-boot/tree/main/spring-boot-jpa-h2

Top comments (0)

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more