JavaRush /Java 博客 /Random-ZH /将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。第2部分
Artur
第 40 级
Tallinn

将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。第2部分

已在 Random-ZH 群组中发布
将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。第 1 部分 将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。 第 2 - 1 部分因此,在上一部分中,我们学习了如何在计算机上安装 PostgresSQL 数据库,在 pgAdmin 中创建数据库,以及如何手动和以编程方式在其中创建和删除表。在这一部分中,我们将重写我们的程序,以便它学习如何使用该数据库和表。为什么是我们?因为我自己正在和你们一起从这个材料中学习。然后,我们不仅会解决手头的任务,还会在更有经验的程序员的建议的帮助下纠正路上出现的错误。可以这么说,我们将学习如何在团队中工作;)首先,让我们com.javarush.lectures.rest_example在文件夹中创建一个新包并将其命名为repository. 在此包中,我们将创建一个新界面ClientRepository
package com.javarush.lectures.rest_example.repository;

import com.javarush.lectures.rest_example.model.Client;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ClientRepository extends JpaRepository<Client, Integer>  {
}
这个界面将“神奇地”与我们的数据库和表格进行交互。为什么神奇?因为我们不需要编写它的实现,Spring框架会提供给我们。你只需要创建这样一个界面,你就已经可以使用这个“魔法”了。下一步是Client像这样编辑类:
package com.javarush.lectures.rest_example.model;

import javax.persistence.*;

@Entity
@Table(name = "clients")
public class Client {
    @Id
    @Column(name = "id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

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

    @Column(name = "email")
    private String email;

    @Column(name = "phone")
    private String phone;

    //… getters and setters
}
我们在这个类中所做的只是添加一些注释。让我们来看看它们:
  • @Entity - 表明这个bean(类)是一个实体。
  • @Table - 指示将在该实体中显示的表的名称。
  • @Id - 列id(主键 - 将用于确保当前表中数据的唯一性的值。注:Andrei
  • @Column - 指示映射到实体属性的列的名称。
  • @GenerateValue - 表示该属性将根据指定的策略生成。
表字段的名称不必与类中变量的名称匹配。例如,如果我们有一个变量firstName,那么我们将在表中命名该字段first_name。这些注释可以直接在字段及其 getter 上设置。但如果您选择其中一种方法,请尝试在整个程序中保持这种风格。我使用第一种方法只是为了缩短列表。可以在此处找到更完整的数据库注释列表。现在我们去类中ClientServiceImpl重写一下,如下:
package com.javarush.lectures.rest_example.service;

import com.javarush.lectures.rest_example.model.Client;
import com.javarush.lectures.rest_example.repository.ClientRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class ClientServiceImpl implements ClientService {

    @Autowired
    private ClientRepository clientRepository;

    @Override
    public void create(Client client) {
        clientRepository.save(client);
    }

    @Override
    public List<Client>  readAll() {
        return clientRepository.findAll();
    }

    @Override
    public Client read(int id) {
        return clientRepository.getOne(id);
    }

    @Override
    public boolean update(Client client, int id) {
        if (clientRepository.existsById(id)) {
            client.setId(id);
            clientRepository.save(client);
            return true;
        }

        return false;
    }

    @Override
    public boolean delete(int id) {
        if (clientRepository.existsById(id)) {
            clientRepository.deleteById(id);
            return true;
        }
        return false;
    }
}
正如您从列表中看到的,我们所做的就是删除不再需要的行:
// Хранorще клиентов
private static final Map<Integer, Client>  CLIENT_REPOSITORY_MAP = new HashMap<>();

// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
相反,我们声明了我们的接口ClientRepository,并在其上方放置了@Autowired注释,以便 Spring 会自动将此依赖项添加到我们的类中。我们还将所有工作委托给这个接口,或者更确切地说是它的实现,Spring 将添加它。让我们进入最后也是最有趣的阶段 - 测试我们的应用程序。让我们打开 Postman 程序(请参阅此处如何使用它)并向此地址发送 GET 请求:http://localhost:8080/clients。我们得到这样的答案:
[
    {
        "id": 1,
        "name": "Vassily Petrov",
        "email": "vpetrov@jr.com",
        "phone": "+7 (191) 322-22-33)"
    },
    {
        "id": 2,
        "name": "Pjotr Vasechkin",
        "email": "pvasechkin@jr.com",
        "phone": "+7 (191) 223-33-22)"
    }
]
我们发送一个 POST 请求:
{
  "name" : "Amigo",
  "email" : "amigo@jr.com",
  "phone" : "+7 (191) 746-43-23"
}
而且......我们发现了程序中的第一个错误:
{
    "timestamp": "2020-03-06T13:21:12.180+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "could not execute statement; SQL [n/a]; constraint [null]; nested exception is org.hibernate.exception.ConstraintViolationException: could not execute statement",
    "path": "/clients"
}
将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。 第 2 - 2 部分 我们查看日志,发现如下错误:

org.postgresql.util.PSQLException: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
  Detail: Ключ "(id)=(1)" уже существует.
我们再次发送相同的 POST 请求,结果是相同的,但有一个区别:Ключ "(id)=(2)" уже существует. 我们第三次发送相同的请求,我们得到 Status: 201 Created。我们再次发送 GET 请求并收到响应:
[
    {
        "id": 1,
        "name": "Vassily Petrov",
        "email": "vpetrov@jr.com",
        "phone": "+7 (191) 322-22-33)"
    },
    {
        "id": 2,
        "name": "Pjotr Vasechkin",
        "email": "pvasechkin@jr.com",
        "phone": "+7 (191) 223-33-22)"
    },
    {
        "id": 3,
        "name": "Amigo",
        "email": "amigo@jr.com",
        "phone": "+7 (191) 746-43-23"
    }
]
这表明我们的程序忽略了这个表已经被预先填充的事实,并再次从 1 开始分配一个 id。好吧,错误是一个工作时刻,不要绝望,这种情况经常发生。因此,我会向更有经验的同事寻求帮助:“亲爱的同事,请在评论中指出如何解决这个问题,以便程序正常运行。” 帮助很快就到达了,斯塔斯·帕辛科夫在评论中告诉我需要朝哪个方向看。在此特别感谢他!但问题是,在课堂上我错误地指定了字段Client注释的策略。 这种策略适合MySQL。如果我们使用 Oracle 或 PostrgeSQL,那么我们需要设置不同的策略。您可以在此处阅读有关主键策略的更多信息。我选择了 GenerationType.SEQUENCE 策略。为了实现它,我们需要稍微重写 initDB.sql 文件,当然还有 Client 类的 id 字段的注释。重写initDB.sql: @GeneratedValue(strategy = GenerationType.IDENTITY)id将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。 第 2 - 3 部分
CREATE TABLE IF NOT EXISTS clients
(
    id    INTEGER PRIMARY KEY ,
    name  VARCHAR(200) NOT NULL ,
    email VARCHAR(254) NOT NULL ,
    phone VARCHAR(50)  NOT NULL
);
CREATE SEQUENCE clients_id_seq START WITH 3 INCREMENT BY 1;
发生了什么变化:我们表的 id 列的类型发生了变化,稍后会详细介绍。我们在下面添加了一行,在其中创建了一个新序列clients_id_seq,指示它应以 3 开头(因为 populateDB.sql 文件中的最后一个 id 是 2),并指示增量应为 1。让我们回到 id 列类型。这里我们指定了INTEGER,因为如果我们离开SERIAL,序列将自动创建,具有相同的名称clients_id_seq,但将从1开始(这导致了程序错误)。但是,现在如果要删除表,则需要通过 pgAdmin 界面手动或使用以下命令通过 .sql 文件另外删除此序列:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
但是,如果您不使用像 populateDB.sql 这样的文件来初始填充表,那么您可以使用 SERIAL 或 BIGSERIAL 类型作为主键,并且您不必手动创建序列,因此不必删除分开它。您可以在 的网站上阅读有关序列的更多信息。PostgreSQL 文档。让我们继续讨论id类字段注释Client并将其格式化如下:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
我们做了什么:我们安装了一个新的注释@SequenceGenerator来创建一个序列生成器,为其分配一个名称clientsIdSeq,表明这是一个序列生成器clients_id_seq,并添加了一个属性。allocationSize = 1 这是一个可选属性,但如果我们不这样做,当我们运行程序时,会出现以下错误:

org.hibernate.MappingException: The increment size of the [clients_id_seq] sequence is set to [50] in the entity mapping while the associated database sequence increment size is [1]
以下是用户Andrei在评论中对此的评论: allocationSize 主要是为了减少 hibernate 为“新 id”而访问数据库的次数。如果值 == 1,则对每个新实体进行休眠,在将其保存到数据库之前,“运行”到数据库中查找 id。如果该值> 1(例如5),hibernate将较少频率地联系数据库获取“新”id(例如5次),并且在联系时,hibernate将要求数据库保留这个号码(在我们的案例,5)值。您描述的错误表明 hibernate 希望接收 50 个默认 id,但在数据库中,您表示您已准备好仅根据第一个 id 为该实体颁发 id用户Nikolya Kudryashov发现了另一个错误:如果您从原始文章 http://localhost:8080/clients/1 运行请求,将返回错误:
{
    "timestamp": "2020-04-02T19:20:16.073+0000",
    "status": 500,
    "error": "Internal Server Error",
    "message": "Type definition error: [simple type, class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor]; nested exception is com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: com.javarush.lectures.rest_example.model.Client$HibernateProxy$ZA2m7agZ[\"hibernateLazyInitializer\"])",
    "path": "/clients/1"
}
这个错误与Hibernate的延迟初始化有关,为了消除它,我们需要在Client类中添加一个额外的注释:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
这样:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
现在让我们运行我们的程序(从数据库中删除客户端表(如果上次还存在的话)),并从 application.properties 文件中注释掉 3 行:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
上次我们只评论了最后一行,但是...... 由于我们已经创建并填写了表格,目前这对我来说似乎更合乎逻辑。让我们继续测试,通过 Postman 执行 GET、POST、PUT 和 DELETE 请求,我们将看到错误已经消失,一切正常。就这样,工作完成了。现在我们可以简要总结并思考我们所学到的内容:
  • 在您的计算机上安装 PostgreSQL
  • 在 pgAdmin 中创建数据库
  • 手动和编程创建和删除表
  • 通过 .sql 文件填充表
  • 我们了解了 Spring 框架的“神奇”JpaRepository 接口
  • 我们了解了创建此类程序时可能出现的一些错误
  • 我们意识到向同事寻求建议不应该感到尴尬
  • 我们已经确认 JavaRush 社区是一支始终能够提供救援的力量;)
我们现在就可以到这里结束了。将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。 第 2 - 4 部分感谢所有花时间阅读本材料的人。我很高兴看到您的评论、观察、补充和建设性批评。也许您会为这个问题提供更优雅的解决方案,我承诺通过 UPD“关键字”将其添加到本文中,当然,还会提及您作为作者。好吧,总的来说,写下你是否喜欢这篇文章和这种呈现材料的风格,以及总的来说,我是否应该继续写关于 JR 的文章。以下是补充内容: UPD1:用户Justinian强烈建议我将包重命名com.javarush.lectures.rest_examplecom.javarush.lectures.rest.example,以及项目的名称,以免违反 Java 中的命名约定。UPD2 用户Alexander PyanovClientRepository建议,要初始化类中的字段,ClientServiceImpl最好使用构造函数而不是注释@Autowired。这是因为在极少数情况下您可以获得NullPointerException,并且一般来说,这是最佳实践,我同意这一点。从逻辑上讲,如果一个对象的初始功能需要一个字段,那么最好在构造函数中初始化它,因为没有构造函数的类不会被组装成对象,因此,该字段将在阶段初始化的对象创建。我将添加一个带有更正的代码片段(需要用什么替换):
@Autowired
private ClientRepository clientRepository;

private final ClientRepository clientRepository;

public ClientServiceImpl(ClientRepository clientRepository) {
   this.clientRepository = clientRepository;
}
链接到第一部分:将 PostgreSQL 数据库添加到 Spring Boot 上的 RESTful 服务。第 1 部分 PS 如果你们中的任何人想继续开发此教育应用程序,那么我将很高兴在本文中添加指向您的指南的链接。也许有一天,这个程序将发展成为类似于真正的业务应用程序的东西,您可以将工作添加到您的投资组合中。PPS 关于这篇谦虚的文章,我决定将这次笔试献给我们亲爱的女孩、妇女和女士们。谁知道,如果没有这个女人,也许现在就没有 Java,没有 JavaRush,就没有编程本质。祝贺您节日快乐,我们亲爱的聪明人!3 月 8 日快乐!快乐又美丽!
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION