JavaRush /Blog Java /Random-VI /Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Sp...
Artur
Mức độ
Tallinn

Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Spring Boot. Phần 2

Xuất bản trong nhóm
Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Spring Boot. Phần 1 Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Spring Boot.  Phần 2 - 1 Vì vậy, trong phần trước chúng ta đã học cách cài đặt cơ sở dữ liệu PostgresSQL trên máy tính, tạo cơ sở dữ liệu trong pgAdmin, đồng thời tạo và xóa các bảng trong đó theo cách thủ công và theo chương trình. Trong phần này, chúng ta sẽ viết lại chương trình của mình để nó học cách làm việc với cơ sở dữ liệu và bảng này. Tại sao là chúng tôi? Vì bản thân tôi cũng đang học cùng các bạn từ tài liệu này. Và khi đó, chúng tôi sẽ không chỉ giải quyết được nhiệm vụ trước mắt mà còn sửa các lỗi phát sinh khi đang di chuyển với sự trợ giúp của lời khuyên từ các lập trình viên giàu kinh nghiệm hơn. Có thể nói, chúng ta sẽ học cách làm việc theo nhóm;) Trước tiên, hãy tạo com.javarush.lectures.rest_examplemột gói mới trong một thư mục và gọi nó là repository. Trong gói này chúng ta sẽ tạo một giao diện mới 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>  {
}
Giao diện này sẽ tương tác “một cách kỳ diệu” với cơ sở dữ liệu và bảng của chúng ta. Tại sao lại kỳ diệu? Bởi vì chúng ta sẽ không cần phải viết phần triển khai của nó và Spring framework sẽ cung cấp cho chúng ta. Bạn chỉ cần tạo một giao diện như vậy là bạn đã có thể sử dụng được “ma thuật” này. Bước tiếp theo là chỉnh sửa lớp Clientnhư thế này:
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
}
Tất cả những gì chúng tôi làm trong lớp này chỉ là thêm một số chú thích. Chúng ta hãy đi qua chúng:
  • @Entity - chỉ ra rằng Bean (lớp) này là một thực thể.
  • @Table - cho biết tên của bảng sẽ được hiển thị trong thực thể này.
  • @Id - id cột (khóa chính - giá trị sẽ được sử dụng để đảm bảo tính duy nhất của dữ liệu trong bảng hiện tại. Lưu ý: Andrei )
  • @Column - cho biết tên của cột được ánh xạ tới thuộc tính thực thể.
  • @GeneratedValue - chỉ ra rằng thuộc tính này sẽ được tạo theo chiến lược đã chỉ định.
Tên của các trường trong bảng không nhất thiết phải khớp với tên của các biến trong lớp. Ví dụ: nếu chúng ta có một biến firstNamethì chúng ta sẽ đặt tên cho trường trong bảng first_name. Những chú thích này có thể được đặt trực tiếp trên các trường và trên getters của chúng. Nhưng nếu bạn chọn một trong những phương pháp này thì hãy cố gắng duy trì phong cách này trong suốt chương trình của mình. Tôi đã sử dụng phương pháp đầu tiên chỉ để rút ngắn danh sách. Bạn có thể tìm thấy danh sách đầy đủ hơn các chú thích để làm việc với cơ sở dữ liệu tại đây . Bây giờ chúng ta vào lớp ClientServiceImplvà viết lại như sau:
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;
    }
}
Như bạn có thể thấy trong danh sách, tất cả những gì chúng tôi đã làm là xóa những dòng chúng tôi không còn cần nữa:
// Хранorще клиентов
private static final Map<Integer, Client>  CLIENT_REPOSITORY_MAP = new HashMap<>();

// Переменная для генерации ID клиента
private static final AtomicInteger CLIENT_ID_HOLDER = new AtomicInteger();
Thay vào đó, chúng tôi đã khai báo giao diện của mình ClientRepositoryvà cũng đặt chú thích @Autowired phía trên giao diện đó để Spring tự động thêm phần phụ thuộc này vào lớp của chúng tôi. Chúng tôi cũng đã ủy quyền tất cả công việc cho giao diện này, hay đúng hơn là việc triển khai nó mà Spring sẽ bổ sung. Hãy chuyển sang giai đoạn cuối cùng và thú vị nhất - thử nghiệm ứng dụng của chúng tôi. Hãy mở chương trình Postman (xem cách sử dụng tại đây ) và gửi yêu cầu GET đến địa chỉ này: http://localhost:8080/clients. Chúng tôi nhận được câu trả lời này:
[
    {
        "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)"
    }
]
Chúng tôi gửi yêu cầu POST:
{
  "name" : "Amigo",
  "email" : "amigo@jr.com",
  "phone" : "+7 (191) 746-43-23"
}
Và... chúng tôi gặp phải lỗi đầu tiên trong chương trình:
{
    "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"
}
Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Spring Boot.  Phần 2 - 2 Chúng tôi xem nhật ký và tìm thấy lỗi sau:

org.postgresql.util.PSQLException: ОШИБКА: повторяющееся meaning ключа нарушает ограничение уникальности "clients_pkey"
  Detail: Ключ "(id)=(1)" уже существует.
Chúng tôi gửi lại cùng một yêu cầu POST, kết quả vẫn như cũ, nhưng có điểm khác biệt: Ключ "(id)=(2)" уже существует. Chúng tôi gửi cùng một yêu cầu lần thứ ba và chúng tôi nhận được Trạng thái: 201 Đã tạo. Chúng ta gửi lại yêu cầu GET và nhận được phản hồi:
[
    {
        "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"
    }
]
Điều này cho thấy rằng chương trình của chúng tôi đang bỏ qua thực tế là bảng này đã được điền trước và đang gán lại id bắt đầu từ một. Chà, lỗi là một khoảnh khắc đang hoạt động, đừng tuyệt vọng, điều này xảy ra thường xuyên. Vì vậy, tôi sẽ nhờ các đồng nghiệp có kinh nghiệm hơn giúp đỡ: “Các đồng nghiệp thân mến, vui lòng tư vấn trong phần bình luận cách khắc phục để chương trình hoạt động bình thường”. Sự trợ giúp không mất nhiều thời gian để đến nơi và Stas Pasinkov đã nói với tôi trong phần nhận xét về hướng tôi cần nhìn. Đặc biệt cảm ơn anh ấy vì điều này! Nhưng vấn đề là trong lớp Clienttôi đã chỉ định không chính xác chiến lược chú thích @GeneratedValue(strategy = GenerationType.IDENTITY)cho trường này id. Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Spring Boot.  Phần 2 - 3 Chiến lược này phù hợp với MySQL. Nếu chúng tôi làm việc với Oracle hoặc PostrgeSQL, thì chúng tôi cần đặt ra một chiến lược khác. Bạn có thể đọc thêm về chiến lược cho khóa chính tại đây . Tôi đã chọn chiến lược GenerationType.SEQUENCE. Để triển khai nó, chúng ta sẽ cần viết lại một chút tệp initDB.sql và tất nhiên cả các chú thích của trường id của lớp Client. Viết lại initDB.sql:
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;
Điều gì đã thay đổi: loại cột id của bảng của chúng tôi đã thay đổi, nhưng sau này sẽ nói thêm về điều đó. Chúng tôi đã thêm một dòng bên dưới để tạo một chuỗi mới client_id_seq, cho biết rằng nó phải bắt đầu bằng số ba (vì id cuối cùng trong tệp populateDB.sql là 2) và cho biết rằng mức tăng sẽ xảy ra theo một. Hãy quay lại loại cột id. Ở đây chúng ta đã chỉ định INTEGER, vì nếu chúng ta rời khỏi SERIAL, chuỗi sẽ được tạo tự động, có cùng tên client_id_seq, nhưng sẽ bắt đầu từ một (dẫn đến lỗi chương trình). Tuy nhiên, bây giờ nếu bạn muốn xóa một bảng, bạn sẽ cần phải xóa thêm chuỗi này theo cách thủ công thông qua giao diện pgAdmin hoặc thông qua tệp .sql bằng các lệnh sau:
DROP TABLE IF EXISTS clients;
DROP SEQUENCE IF EXISTS clients_id_seq
Nhưng nếu bạn không sử dụng tệp như populateDB.sql để điền bảng ban đầu thì bạn có thể sử dụng loại SERIAL hoặc BIGSERIAL cho khóa chính và bạn không phải tạo chuỗi theo cách thủ công và do đó không phải xóa nó một cách riêng biệt. Bạn có thể đọc thêm về trình tự trên trang web của. Tài liệu PostgreSQL . Hãy chuyển sang các chú thích trường idlớp Clientvà định dạng chúng như sau:
@Id
@Column(name = "id")
@SequenceGenerator(name = "clientsIdSeq", sequenceName = "clients_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "clientsIdSeq")
private Integer id;
Những gì chúng tôi đã làm: chúng tôi đã cài đặt một chú thích mới @SequenceGeneratorđể tạo trình tạo chuỗi, gán tên cho nó clientsIdSeq, chỉ ra rằng đây là trình tạo cho chuỗi clients_id_seqvà thêm một thuộc tính. allocationSize = 1 Đây là thuộc tính tùy chọn, nhưng nếu chúng tôi không làm điều này, chúng ta sẽ gặp lỗi sau khi chạy chương trình:

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]
Đây là những gì người dùng Andrei viết về điều này trong phần nhận xét: alocationSize chủ yếu nhằm mục đích giảm thời gian ngủ đông đến cơ sở dữ liệu cho một “id mới”. Nếu giá trị == 1, ngủ đông cho mỗi thực thể mới, trước khi lưu nó vào cơ sở dữ liệu, hãy “chạy” vào cơ sở dữ liệu cho id. Nếu giá trị > 1 (ví dụ: 5), hibernate sẽ liên hệ với cơ sở dữ liệu để lấy id “mới” ít thường xuyên hơn (ví dụ: 5 lần) và khi liên hệ, hibernate sẽ yêu cầu cơ sở dữ liệu dự trữ số này (trong trường hợp, 5) giá trị. Lỗi mà bạn mô tả cho thấy rằng chế độ ngủ đông muốn nhận 50 id mặc định, nhưng trong cơ sở dữ liệu, bạn đã chỉ ra rằng bạn sẵn sàng chỉ cấp id cho thực thể này theo id đầu tiên . Một lỗi khác đã được người dùng Nikolya Kudryashov bắt gặp : Nếu bạn chạy một yêu cầu từ bài viết gốc http://localhost:8080/clients/1, lỗi sẽ được trả về:
{
    "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"
}
Lỗi này liên quan đến việc khởi tạo lười biếng của Hibernate và để loại bỏ nó, chúng ta cần thêm một chú thích bổ sung vào lớp Client:
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
Bằng cách này:
@Entity
@Table(name = "clients")
@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"})
public class Client {.....}
Bây giờ, hãy chạy chương trình của chúng ta (sau khi xóa bảng client khỏi cơ sở dữ liệu nếu nó vẫn còn ở đó từ lần trước) và nhận xét 3 dòng từ tệp application.properties:
#spring.datasource.initialization-mode=ALWAYS
#spring.datasource.schema=classpath*:database/initDB.sql
#spring.datasource.data=classpath*:database/populateDB.sql
Lần trước chúng tôi chỉ bình luận dòng cuối cùng, nhưng... Vì chúng tôi đã tạo và điền vào bảng nên điều này đối với tôi vào lúc này có vẻ hợp lý hơn. Hãy chuyển sang thử nghiệm, thực hiện các yêu cầu GET, POST, PUT và DELETE thông qua Postman, chúng ta sẽ thấy các lỗi đã biến mất và mọi thứ đều hoạt động tốt. Thế là xong, công việc đã hoàn thành. Bây giờ chúng ta có thể tóm tắt ngắn gọn và xem xét những gì chúng ta đã học được:
  • Cài đặt PostgreSQL trên máy tính của bạn
  • Tạo cơ sở dữ liệu trong pgAdmin
  • Tạo và xóa bảng theo cách thủ công và lập trình
  • Điền vào bảng thông qua tệp .sql
  • Chúng ta đã tìm hiểu đôi chút về giao diện JpaRepository “thần kỳ” của Spring framework
  • Chúng tôi đã tìm hiểu về một số lỗi có thể phát sinh khi tạo một chương trình như vậy
  • Chúng tôi nhận ra rằng mình không nên xấu hổ khi tìm kiếm lời khuyên từ đồng nghiệp
  • Chúng tôi đã xác nhận rằng cộng đồng JavaRush là một lực lượng sẽ luôn sẵn sàng giải cứu;)
Chúng ta có thể kết thúc ở đây bây giờ. Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Spring Boot.  Phần 2 - 4Cảm ơn mọi người đã dành thời gian đọc tài liệu này. Tôi sẽ rất vui khi thấy những nhận xét, quan sát, bổ sung và phê bình mang tính xây dựng của bạn. Có lẽ bạn sẽ đưa ra các giải pháp tinh tế hơn cho vấn đề này, mà tôi hứa sẽ bổ sung vào bài viết này thông qua “từ khóa” CẬP NHẬT, tất nhiên có đề cập đến bạn với tư cách là tác giả. Nói chung, hãy viết nếu bạn thích bài viết này và phong cách trình bày tài liệu này, và nói chung là liệu tôi có nên tiếp tục viết bài về JR hay không. Dưới đây là các bổ sung: CẬP NHẬT1: người dùng Justinian thực sự khuyên tôi nên đổi tên gói com.javarush.lectures.rest_examplethành com.javarush.lectures.rest.example, và tên của dự án, để không vi phạm các quy ước đặt tên trong Java. Người dùng UPD2 Alexander Pyanov gợi ý rằng để khởi tạo một trường ClientRepositorytrong một lớp ClientServiceImpl, tốt hơn nên sử dụng hàm tạo thay vì chú thích @Autowired. Điều này được giải thích bởi thực tế là trong những trường hợp hiếm hoi bạn có thể nhận được NullPointerException, và nói chung, đây là cách thực hành tốt nhất và tôi đồng ý với điều đó. Về mặt logic, nếu một trường được yêu cầu cho chức năng ban đầu của một đối tượng thì tốt hơn là khởi tạo nó trong hàm tạo, vì một lớp không có hàm tạo sẽ không được tập hợp thành một đối tượng, do đó, trường này sẽ được khởi tạo ở giai đoạn của việc tạo đối tượng. Tôi sẽ thêm một đoạn mã có chỉnh sửa (cái gì cần thay thế bằng cái gì):
@Autowired
private ClientRepository clientRepository;

private final ClientRepository clientRepository;

public ClientServiceImpl(ClientRepository clientRepository) {
   this.clientRepository = clientRepository;
}
Liên kết đến phần đầu tiên: Thêm cơ sở dữ liệu PostgreSQL vào dịch vụ RESTful trên Spring Boot. Phần 1 PS Nếu bất kỳ ai trong số các bạn muốn tiếp tục phát triển ứng dụng giáo dục này thì tôi rất vui được thêm liên kết đến hướng dẫn của bạn trong bài viết này. Có lẽ một ngày nào đó chương trình này sẽ phát triển thành một thứ gì đó giống như một ứng dụng kinh doanh thực sự mà bạn có thể thêm công việc vào danh mục đầu tư của mình. PPS Về bài viết khiêm tốn này, tôi quyết định dành tặng bài kiểm tra cây bút này cho các cô gái, quý bà và các quý cô thân yêu của chúng ta. Biết đâu bây giờ sẽ không có Java, không có JavaRush, không có bản chất lập trình nếu không có người phụ nữ này . Chúc mừng kỳ nghỉ của bạn, những người thông minh thân yêu của chúng tôi! Chúc mừng ngày 8 tháng 3! Hãy hạnh phúc và xinh đẹp!
Bình luận
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION