Spring Boot、PostgreSQL、JPA、Hibernate RESTful CRUD API 示例

x33g5p2x  于2021-10-20 转载在 Spring  
字(10.3k)|赞(0)|评价(0)|浏览(299)

在本文中,您将学习如何配置 Spring Boot 以使用 PostgreSQL 数据库并从头开始构建 RESTful CRUD API。

您还将了解 Spring Data JPA 和 Hibernate 如何与 PostgreSQL 一起使用。

我们将为 Quora 等问答应用程序编写 REST API。 Q&A 应用程序将有两个域模型 - QuestionAnswer。由于一个问题可以有多个答案,我们将定义 QuestionAnswer 实体之间的一对多关系。

我们将首先引导项目并配置 PostgreSQL 数据库。之后,我们将定义用于从 PostgreSQL 访问数据的域模型和存储库。最后,我们将编写 REST API 并使用 Postman 测试这些 API。

酷,让我们开始吧!

引导项目

您可以使用 Spring CLI 工具通过在终端中键入以下命令来引导 Spring Boot 项目。

$ spring init --name=postgres-demo --dependencies=web,jpa,postgresql,validation postgres-demo

或者,您可以按照以下说明使用 Spring Initializr Web 工具生成项目 -

  • 前往http://start.spring.io
  • 在工件字段中输入 postgres-demo
  • 在包字段中输入 com.example.postgresdemo
  • 在依赖项部分添加 WebJPAPostgreSQLValidation
  • 点击生成下载项目。


就是这样!您现在可以将项目导入您喜欢的 IDE 并开始工作。

###项目的目录结构

我在这里包含了项目的完整目录结构供您参考。我们将在接下来的部分中一一创建所需的包和类。

配置 PostgreSQL

第一件事 第一。让我们配置 Spring Boot 以使用 PostgreSQL 作为我们的数据源。你可以简单地通过在 src/main/resources/application.properties 文件中添加 PostgreSQL 数据库 url、用户名和密码来做到这一点 -

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres_demo
spring.datasource.username= rajeevkumarsingh
spring.datasource.password=

# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

上述文件中的最后两个属性用于 Hibernate。它是 spring-data-jpa 附带的默认 JPA 供应商。

尽管 Hibernate 与数据库无关,但我们可以指定当前的数据库方言,让它为该数据库生成更好的 SQL 查询。

ddl-auto 属性用于根据应用程序中的实体类自动创建表。

定义域模型

域模型是映射到数据库中相应表的类。我们的应用程序中有两个主要的域模型 - QuestionAnswer。这两个领域模型都有一些常见的审计相关字段,如 createdAtupdatedAt

最好在一个单独的基类中抽象出这些公共字段,这就是我们在这篇文章中要做的。我们将创建一个名为 AuditModel 的抽象类来保存这些字段。

此外,我们将使用 Spring Boot 的 JPA 审计功能在数据库中插入/更新特定实体时自动填充 createdAtupdatedAt 值。

1. 审计模型

以下 AuditModel 类将被其他实体扩展。它包含 @EntityListeners(AuditingEntityListener.class) 注释,当实体被持久化时,它会自动填充 createdAtupdatedAt 值。

package com.example.postgresdemo.model;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@JsonIgnoreProperties(
        value = {"createdAt", "updatedAt"},
        allowGetters = true
)
public abstract class AuditModel implements Serializable {
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at", nullable = false, updatable = false)
    @CreatedDate
    private Date createdAt;

    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "updated_at", nullable = false)
    @LastModifiedDate
    private Date updatedAt;

    // Getters and Setters (Omitted for brevity)
}
启用 JPA 审计

请注意,要启用 JPA 审计,您需要将 @EnableJpaAuditing 注释添加到配置类之一。所以打开主类 PostgresDemoApplication.java 并像这样添加 @EnableJpaAuditing 注释 -

package com.example.postgresdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
@EnableJpaAuditing
public class PostgresDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(PostgresDemoApplication.class, args);
    }
}

2. 问题模型

以下是 Question 实体。它映射到数据库中名为 questions 的表。

package com.example.postgresdemo.model;

import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;

@Entity
@Table(name = "questions")
public class Question extends AuditModel {
    @Id
    @GeneratedValue(generator = "question_generator")
    @SequenceGenerator(
            name = "question_generator",
            sequenceName = "question_sequence",
            initialValue = 1000
    )
    private Long id;

    @NotBlank
    @Size(min = 3, max = 100)
    private String title;

    @Column(columnDefinition = "text")
    private String description;

    // Getters and Setters (Omitted for brevity)
}

请注意,我使用 @SequenceGenerator 来生成问题的 id。您还可以通过指定 @GeneratedValue(strategy=GenerationType.IDENTITY) 注释来使用 PostgreSQL SERIAL 列。但是 SequenceGenerator 在这种情况下表现更好。

3. 答案模型

以下是 Answer 类的定义。它包含一个 @ManyToOne 注释,以声明它与 Question 实体具有多对一的关系。

package com.example.postgresdemo.model;

import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;

@Entity
@Table(name = "answers")
public class Answer extends AuditModel {
    @Id
    @GeneratedValue(generator = "answer_generator")
    @SequenceGenerator(
            name = "answer_generator",
            sequenceName = "answer_sequence",
            initialValue = 1000
    )
    private Long id;

    @Column(columnDefinition = "text")
    private String text;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "question_id", nullable = false)
    @OnDelete(action = OnDeleteAction.CASCADE)
    @JsonIgnore
    private Question question;

    // Getters and Setters (Omitted for brevity)
}

定义存储库

以下存储库将用于访问数据库中的问题和答案。

1. QuestionRepository

package com.example.postgresdemo.repository;

import com.example.postgresdemo.model.Question;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface QuestionRepository extends JpaRepository<Question, Long> {
}

2.AnswerRepository

package com.example.postgresdemo.repository;

import com.example.postgresdemo.model.Answer;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

@Repository
public interface AnswerRepository extends JpaRepository<Answer, Long> {
    List<Answer> findByQuestionId(Long questionId);
}

构建 REST API

最后,让我们在控制器中编写 REST API,用于对问题和答案执行 CRUD 操作。

1. QuestionController

package com.example.postgresdemo.controller;

import com.example.postgresdemo.exception.ResourceNotFoundException;
import com.example.postgresdemo.model.Question;
import com.example.postgresdemo.repository.QuestionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;

@RestController
public class QuestionController {

    @Autowired
    private QuestionRepository questionRepository;

    @GetMapping("/questions")
    public Page<Question> getQuestions(Pageable pageable) {
        return questionRepository.findAll(pageable);
    }


    @PostMapping("/questions")
    public Question createQuestion(@Valid @RequestBody Question question) {
        return questionRepository.save(question);
    }

    @PutMapping("/questions/{questionId}")
    public Question updateQuestion(@PathVariable Long questionId,
                                   @Valid @RequestBody Question questionRequest) {
        return questionRepository.findById(questionId)
                .map(question -> {
                    question.setTitle(questionRequest.getTitle());
                    question.setDescription(questionRequest.getDescription());
                    return questionRepository.save(question);
                }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId));
    }


    @DeleteMapping("/questions/{questionId}")
    public ResponseEntity<?> deleteQuestion(@PathVariable Long questionId) {
        return questionRepository.findById(questionId)
                .map(question -> {
                    questionRepository.delete(question);
                    return ResponseEntity.ok().build();
                }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId));
    }
}

2. AnswerController

package com.example.postgresdemo.controller;

import com.example.postgresdemo.exception.ResourceNotFoundException;
import com.example.postgresdemo.model.Answer;
import com.example.postgresdemo.repository.AnswerRepository;
import com.example.postgresdemo.repository.QuestionRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.List;

@RestController
public class AnswerController {

    @Autowired
    private AnswerRepository answerRepository;

    @Autowired
    private QuestionRepository questionRepository;

    @GetMapping("/questions/{questionId}/answers")
    public List<Answer> getAnswersByQuestionId(@PathVariable Long questionId) {
        return answerRepository.findByQuestionId(questionId);
    }

    @PostMapping("/questions/{questionId}/answers")
    public Answer addAnswer(@PathVariable Long questionId,
                            @Valid @RequestBody Answer answer) {
        return questionRepository.findById(questionId)
                .map(question -> {
                    answer.setQuestion(question);
                    return answerRepository.save(answer);
                }).orElseThrow(() -> new ResourceNotFoundException("Question not found with id " + questionId));
    }

    @PutMapping("/questions/{questionId}/answers/{answerId}")
    public Answer updateAnswer(@PathVariable Long questionId,
                               @PathVariable Long answerId,
                               @Valid @RequestBody Answer answerRequest) {
        if(!questionRepository.existsById(questionId)) {
            throw new ResourceNotFoundException("Question not found with id " + questionId);
        }

        return answerRepository.findById(answerId)
                .map(answer -> {
                    answer.setText(answerRequest.getText());
                    return answerRepository.save(answer);
                }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId));
    }

    @DeleteMapping("/questions/{questionId}/answers/{answerId}")
    public ResponseEntity<?> deleteAnswer(@PathVariable Long questionId,
                                          @PathVariable Long answerId) {
        if(!questionRepository.existsById(questionId)) {
            throw new ResourceNotFoundException("Question not found with id " + questionId);
        }

        return answerRepository.findById(answerId)
                .map(answer -> {
                    answerRepository.delete(answer);
                    return ResponseEntity.ok().build();
                }).orElseThrow(() -> new ResourceNotFoundException("Answer not found with id " + answerId));

    }
}

自定义 ResourceNotFoundException 类

当在数据库中找不到问题或答案时,问答 REST API 会抛出 ResourceNotFoundException。以下是 ResourceNotFoundException 类的定义 -

package com.example.postgresdemo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String message, Throwable cause) {
        super(message, cause);
    }
}

异常类包含一个 @ResponseStatus(HttpStatus.NOT_FOUND) 注释,用于告诉 Spring Boot 在抛出此异常时以 404 NOT FOUND 状态进行响应。

通过 Postman 运行应用程序并测试 API

我们已经完成了 REST API 的构建。是时候运行应用程序并测试这些 API 了。

启动终端并从应用程序的根目录键入以下命令以运行它 -

mvn spring-boot:run

应用程序将在默认端口 8080 上启动。

以下为creenshots 向您展示了如何使用 Postman 测试 API。

1. 创建问题 POST /questions

2. 获取分页问题 GET /questions?page=0&size=2&sort=createdAt,desc

3. 创建答案 POST /questions/{questionId}/answers

4. 获取问题的所有答案 GET /questions/{questionId}/answers

您可以类似地测试其他 API。

相关文章