So far, data has only been handled with the in-memory Map. Now we use Spring Data JPA and H2 In-Memory DB to create a persistence layer that behaves the same across restarts. JPA is an ORM standard that automates the mapping between Java objects and SQL.
Learning Objectives
- Map the class to the table with
@Entity - Only define the
JpaRepositoryinterface and use it without implementation. - View actual SQL in H2 console
- Complete a small notepad API that operates CRUD
Key concepts
1) @Entity
@Entity
@Table(name = "memos")
public class Memo {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Column(columnDefinition = "TEXT")
private String body;
protected Memo() {} //JPA uses
public Memo(String title, String body) { this.title = title; this.body = body; }
// getters/setters ...
}
- Default constructor (protected/public) required
@Idis PK- For
@GeneratedValue(IDENTITY), DB automatically creates PK
2) JpaRepository
public interface MemoRepository extends JpaRepository<Memo, Long> {
List<Memo> findByTitleContaining(String keyword);
}
save, findAll, findById, delete, count, etc. are automatically implemented.
With a naming convention (findByfield name), a simple lookup method only needs to declare a signature.
3) H2 in-memory DB
spring.datasource.url=jdbc:h2:mem:lecture26
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=update
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.show-sql=true
You can view the table by accessing the console using the browser http://localhost:8087/h2-console and entering the above URL as is in the JDBC URL.
4) Transaction
@Service
public class MemoService {
@Transactional
public Memo create(...) { ... }
}
All DB operations that occur within the @Transactional method must succeed before being committed, and if even one fails, they are automatically rolled back.
See as example
Example 1 — Memo.java: Entity
@Entity
@Table(name = "memos")
public class Memo {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@Column(columnDefinition = "TEXT")
private String body;
// ...
}
Example 2 — MemoRepository.java: Spring Data JPA
public interface MemoRepository extends JpaRepository<Memo, Long> {
List<Memo> findByTitleContaining(String keyword);
}
Just define this interface and Spring will create and inject the implementation at runtime.
Example 3 — MemoService.java: Business Logic
@Service
@Transactional
public class MemoService {
private final MemoRepository repository;
public MemoService(MemoRepository repository) { this.repository = repository; }
public Memo create(String title, String body) { ... }
public Memo update(Long id, String title, String body) { ... }
public void delete(Long id) { ... }
public Memo get(Long id) { ... }
public List<Memo> search(String q) { ... }
}
Example 4 — MemoController.java: REST API
@RestController
@RequestMapping("/api/memos")
public class MemoController {
private final MemoService service;
// ...
@GetMapping public List<MemoResponse> list(...) { ... }
@GetMapping("/{id}") public MemoResponse one(@PathVariable Long id) { ... }
@PostMapping public MemoResponse create(@RequestBody MemoRequest req) { ... }
@PutMapping("/{id}") public MemoResponse update(...) { ... }
@DeleteMapping("/{id}") public ResponseEntity<Void> delete(...) { ... }
}
Run + curl scenario
cd 06_Spring_Boot/26_Data_Save
mvn spring-boot:run
#create
curl -X POST localhost:8087/api/memos -H "Content-Type: application/json" \
-d '{"title":"Spring","body":"Good"}'
#{"id":1,"title":"Spring","body":"Good"}
#View
curl localhost:8087/api/memos
# [{"id":1, ...}]
curl localhost:8087/api/memos/1
#search
curl "localhost:8087/api/memos?q=Spr"
#Edit
curl -X PUT localhost:8087/api/memos/1 \
-H "Content-Type: application/json" \
-d '{"title":"Spring Boot","body":"Very good"}'
#Delete
curl -X DELETE localhost:8087/api/memos/1
Common Mistakes
- Startup fails because the entity does not have a default constructor
- Missing
@Transactional→ Exception in lazy loading - Confusion about whether
saveis an update or an insert (if there is an id, it is an update) - Apply DDL automatically (
ddl-auto=update) to the operating environment as is - Use
findById(id).get()→ Immediate exception if not present,orElseThrowis safe
Cleanup
- 80% of CRUD is completed with only
@Entity+JpaRepository - Business logic is service layer +
@Transactional - Actual SQL and data can be checked with H2 console
- When replacing with an external DB, only
application.propertiesneeds to be replaced.
Try it yourself
cd 06_Spring_Boot/26_Data_Save
mvn spring-boot:run
#Running the above curl scenario in another terminal