Giới thiệu

Đầu xuân, tôi muốn viết bài này để chia sẻ với tập thể giảng viên Techmaster cũng như các sinh viên hay lập trình viên về việc chúng ta nên tích cực tận dụng AI, ChatGPT để viết ra phần mềm sáng tạo hơn, chất lượng hơn trong khoảng thời gian ngắn hơn, chi phí rẻ hơn. Nếu bạn chưa nắm vững Java hay Spring Boot thì việc ra lệnh cho ChatGPT không có nhiều ý nghĩa vì bạn chả hiểu code ChatGPT sinh ra như thế nào. Tuy nhiên nếu bạn đã từng lập trình một web framework khác như PHP Laravel, Node.js, ASP.net MVC muốn chuyển qua Java Spring Boot thì ChatGPT cực kỳ hữu ích.

Đề bài

Xây dựng một ứng dụng REST API bằng web frame work Spring Boot, ngôn ngữ lập trình Java thực hiện nghiệp vụ quản lý sách (Book) và người mượn sách (Borrower). Mình quyết định ra lệnh cho ChatGPT thực hiện viết mã.

Tổng kết nhanh

Thời gian viết yêu cầu để ChatGPT viết mã, sau đó kiểm tra lại xem nó viết có đúng không là khoảng .5 tiếng. Nếu tự viết bằng tay với kinh nghiệm lập trình Spring Boot đã có, thì mất khoảng 3-4 tiếng. Ưu điểm của tự viết code là hiểu rõ từng dòng code mình viết ra, sau này tinh chỉnh dễ dàng. Tuy nhiên nếu tôi hoàn toàn chưa có kinh nghiệm hoặc có rất ít kinh nghiệm với Spring Boot thì có thể tăng lên 4-6 tiếng chủ yếu là Google tìm hướng dẫn.

Sau khi ChatGPT sinh mã, nếu không hiểu rõ được những mã này thì thực sự bạn khó tiếp tục phát triển tiếp ứng dụng. Tạm thời tôi có thể dùng ChatGPT như là một boiler plate code generator.

1. Tạo 2 model BookBorrowerquan hệ nhiều - nhiều

Tôi : Write a Spring Boot app that has Book, Borrower models. Relationship between Book and Borrower is Many to Many. Book model has following properties: unique ID with fixed length 12 characters, title as string, authors as array of string, published year as integer. Borrower model has following properties: unique ID with fixed length 12 characters, full name as string, email and mobile phone.

ChatGPT
Here’s an example of a Spring Boot app that has Book and Borrower models with a many-to-many relationship:

@Entity
@Table(name = "books")
public class Book {
    @Id
    @Column(length = 12)
    private String id;
    
    private String title;
    
    @ElementCollection
    private List<String> authors;
    
    private int publishedYear;
    
    @ManyToMany(mappedBy = "books")
    private List<Borrower> borrowers;
    
    // Getters and setters
}
Book.java
@Entity
@Table(name = "borrowers")
public class Borrower {
    @Id
    @Column(length = 12)
    private String id;
    
    private String fullName;
    
    private String email;
    
    private String mobilePhone;
    
    @ManyToMany
    @JoinTable(
        name = "borrowers_books",
        joinColumns = @JoinColumn(name = "borrower_id"),
        inverseJoinColumns = @JoinColumn(name = "book_id")
    )
    private List<Book> books;
    
    // Getters and setters
}
Borrower.java

In this example, the Book model has a unique ID with a fixed length of 12 characters, a title as a string, authors as an array of strings, and a published year as an integer. The Borrower model has a unique ID with a fixed length of 12 characters, a full name as a string, an email, and a mobile phone. The relationship between the Book and Borrower models is many-to-many, and is defined by the @ManyToMany annotation on the borrowers and books properties. The @JoinTable annotation is used to specify the name of the join table and the names of the join columns.

2. Viết automation unit test để kiểm thử 2 model trên

Tôi : Great. Using JUnit5 and AssertJ please write unit test to verify above two models: Book and Borrower

ChatGPT
Here’s an example of unit tests using JUnit 5 and AssertJ that test the Book and Borrower models:

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.List;

import org.junit.jupiter.api.Test;

public class BookAndBorrowerTests {

    @Test
    void testBookProperties() {
        Book book = new Book();
        book.setId("1234567890AB");
        book.setTitle("The Great Gatsby");
        book.setAuthors(Arrays.asList("F. Scott Fitzgerald"));
        book.setPublishedYear(1925);

        assertThat(book.getId()).isEqualTo("1234567890AB");
        assertThat(book.getTitle()).isEqualTo("The Great Gatsby");
        assertThat(book.getAuthors()).containsExactly("F. Scott Fitzgerald");
        assertThat(book.getPublishedYear()).isEqualTo(1925);
    }

    @Test
    void testBorrowerProperties() {
        Borrower borrower = new Borrower();
        borrower.setId("0987654321ZA");
        borrower.setFullName("John Doe");
        borrower.setEmail("johndoe@example.com");
        borrower.setMobilePhone("+1234567890");

        assertThat(borrower.getId()).isEqualTo("0987654321ZA");
        assertThat(borrower.getFullName()).isEqualTo("John Doe");
        assertThat(borrower.getEmail()).isEqualTo("johndoe@example.com");
        assertThat(borrower.getMobilePhone()).isEqualTo("+1234567890");
    }

    @Test
    void testManyToManyRelation() {
        Book book1 = new Book();
        book1.setId("1234567890AB");
        book1.setTitle("The Great Gatsby");
        book1.setAuthors(Arrays.asList("F. Scott Fitzgerald"));
        book1.setPublishedYear(1925);

        Book book2 = new Book();
        book2.setId("0987654321ZA");
        book2.setTitle("The Catcher in the Rye");
        book2.setAuthors(Arrays.asList("J.D. Salinger"));
        book2.setPublishedYear(1951);

        Borrower borrower1 = new Borrower();
        borrower1.setId("1111111111AA");
        borrower1.setFullName("Jane Smith");
        borrower1.setEmail("janesmith@example.com");
        borrower1.setMobilePhone("+1234567890");

        Borrower borrower2 = new Borrower();
        borrower2.setId("2222222222BB");
        borrower2.setFullName("Bob Johnson");
        borrower2.setEmail("bobjohnson@example.com");
        borrower2.setMobilePhone("+0987654321");

        borrower1.setBooks(Arrays.asList(book1, book2));
        borrower2.setBooks(Arrays.asList(book1, book2));

        assertThat(book1.getBorrowers()).containsExactlyInAnyOrder(borrower1, borrower2);
        assertThat(book2.getBorrowers()).containsExactlyInAnyOrder(borrower1, borrower2);
        assertThat(borrower1.getBooks()).containsExactlyInAnyOrder(book1, book2);
        assertThat(borrower2.getBooks()).containsExactlyInAnyOrder(book1, book2);
    }
}
BookAndBorrowerTests.java

In this test case, it creates two books, two borrowers and assign these books to the borrowers and vice versa, and then it assert that the books and borrowers are equal to the expected values.
It is also important to note that since it is a many-to-many relationship, the order of the elements in the list can be different, so the containsExactlyInAnyOrder is used to check the elements in any order.
Nếu code quá dài, ChatGPT có thể ngưng giữa chừng, các bạn gõ tiếp “Please continue”, ChatGPT sẽ tiếp tục viết tiếp.

3. Viết 2 JPA repository để thao tác 2 model Book và Borrower xuống CSDL

Tôi : Great job! Please write two JPA repositories that manipulate Book and Borrower models. In this project just use H2 as database

ChatGPT

import org.springframework.data.jpa.repository.JpaRepository;

public interface BookRepository extends JpaRepository<Book, String> { }
BookRepository.java
import org.springframework.data.jpa.repository.JpaRepository;

public interface BorrowerRepository extends JpaRepository<Borrower, String> { }
BorrowerRepository.java

In this example, the BookRepository extends JpaRepository and uses the Book model and its ID type, String, as its generic types. The BorrowerRepository is similar, but uses the Borrower model instead.

By extending JpaRepository, these repositories will have a number of built-in methods for performing CRUD operations on the corresponding model, such as findAll(), save(), delete(), etc.

To use these repositories, you will need to enable JPA and H2 in your Spring Boot application. You can do this by adding the following dependencies to your pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>
pom.xml

You also need to configure the H2 database by adding the following properties to your application.properties file:

spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
application.properties

These properties configure the H2 database to run in-memory with the name testdb, and configure the JPA to use the H2 dialect.

Once you have these repositories and configurations set up, you can use them in your application to perform CRUD operations on theBook and Borrower models. For example, you can use the save() method to add a new book to the database, or the findAll() method to retrieve a list of all books in the database.

It’s also worth noting that you should annotate your entities with @Entity and @Table and your fields with @Id and @Column to specify the table name and the column name respectively.

ChatGPT quá chi tiết và tỷ mỷ. Nó còn gợi ý luôn để dùng H2 database thì cần bổ xung dependencies vào pom.xml

4. Viết 2 services sử dụng Book và Borrower repositories

Tôi : Great, write 2 service components that consume Book and Borrower repositories

ChatGPT
Here’s an example of two service components, BookService and BorrowerService, that consume the BookRepository and BorrowerRepository respectively:

@Service
public class BookService {

    private final BookRepository bookRepository;

    @Autowired
    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public List<Book> getAllBooks() {
        return bookRepository.findAll();
    }

    public Book saveBook(Book book) {
        return bookRepository.save(book);
    }

    public void deleteBook(String id) {
        bookRepository.deleteById(id);
    }

    public Optional<Book> findBookById(String id) {
        return bookRepository.findById(id);
    }
}
BookService.java
@Service
public class BorrowerService {

    private final BorrowerRepository borrowerRepository;

    @Autowired
    public BorrowerService(BorrowerRepository borrowerRepository) {
        this.borrowerRepository = borrowerRepository;
    }

    public List<Borrower> getAllBorrowers() {
        return borrowerRepository.findAll();
    }

    public Borrower saveBorrower(Borrower borrower) {
        return borrowerRepository.save(borrower);
    }

    public void deleteBorrower(String id) {
        borrowerRepository.deleteById(id);
    }

    public Optional<Borrower> findBorrowerById(String id) {
        return borrowerRepository.findById(id);
    }
}
BorrowerService.java

Both of these services are annotated with @Service to make them Spring components, and they both have a constructor that accepts the corresponding repository as a parameter and @Autowired to automatically initialize the repository.

Both services have basic CRUD operations like save(), findAll(), delete() and findById() which can be used to perform CRUD operations on the Book and Borrower models.

It’s also worth noting that in the saveBook or saveBorrower method, it’s returning the saved object that has the id generated by the database. In the delete methods, it’s also using the deleteById which is more secure than delete because you don’t need to fetch the object first, you can directly delete it by its id.

5. Viết 2 REST Controller để xử lý các CRUD request

Tôi : Write 2 REST Controllers: BookController and BorrowerController that handle CRUD requests. These REST Controllers will use BookService and BorrowerService

ChatGPT:
Here’s an example of two REST controllers, BookController and BorrowerController, that handle CRUD requests and use the BookService and BorrowerService respectively:

@RestController
@RequestMapping("/books")
public class BookController {

    private final BookService bookService;

    @Autowired
    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @GetMapping
    public List<Book> getAllBooks() {
        return bookService.getAllBooks();
    }

    @PostMapping
    public Book saveBook(@RequestBody Book book) {
        return bookService.saveBook(book);
    }

    @DeleteMapping("/{id}")
    public void deleteBook(@PathVariable String id) {
        bookService.deleteBook(id);
    }

    @GetMapping("/{id}")
    public Optional<Book> findBookById(@PathVariable String id) {
        return bookService.findBookById(id);
    }
}
BookController.java
@RestController
@RequestMapping("/borrowers")
public class BorrowerController {
private final BorrowerService borrowerService;
    @Autowired
    public BorrowerController(BorrowerService borrowerService) {
        this.borrowerService = borrowerService;
    }

    @GetMapping
    public List<Borrower> getAllBorrowers() {
        return borrowerService.getAllBorrowers();
    }

    @PostMapping
    public Borrower saveBorrower(@RequestBody Borrower borrower) {
        return borrowerService.saveBorrower(borrower);
    }

    @DeleteMapping("/{id}")
    public void deleteBorrower(@PathVariable String id) {
        borrowerService.deleteBorrower(id);
    }

    @GetMapping("/{id}")
    public Optional<Borrower> findBorrowerById(@PathVariable String id) {
        return borrowerService.findBorrowerById(id);
    }
}
BorrowerController.java

Both of these controllers are annotated with @RestController to make them Spring REST controllers, and they both have a constructor that accepts the corresponding service as a parameter and @Autowired to automatically initialize the service.

Both controllers have basic CRUD operations like GET, POST, DELETE and GET/{id} which can be used to perform CRUD operations on the Book and Borrower models via REST endpoints.

In the saveBook and saveBorrower methods, it uses @RequestBody to get the request payload, and in the deleteBook and deleteBorrower methods, it uses @PathVariable to get the id from the url.

In the GET methods, it’s returning the list of Books or Borrowers, in the POST methods, it’s returning the saved object that has the id generated by the database, and in the GET/{id} methods, it’s returning the object of the corresponding id.

6. Viết lệnh curl để kiểm tra 2 REST controller phía trên

Tôi : Write curl command to test two REST Controllers

ChatGPT
Here are some examples of curl commands you can use to test the BookController and BorrowerController:

To get a list of all books:

curl -X GET http://localhost:8080/books

To save a book

curl -X POST -H "Content-Type: application/json" -d '{"id":"123456789012","title":"The Lord of the Rings","authors":["J.R.R. Tolkien"],"publishedYear":1954}' http://localhost:8080/books

To delete a book by id:

curl -X DELETE http://localhost:8080/books/123456789012

To find a book by id:

curl -X GET http://localhost:8080/books/123456789012

To get a list of all borrowers:

curl -X GET http://localhost:8080/borrowers

To save a borrower:

curl -X POST -H "Content-Type: application/json" -d '{"id":"098765432109","fullName":"John Doe","email":"johndoe@example.com","mobilePhone":"555-555-5555"}' http://localhost:8080/borrowers

To delete a borrower by id:

curl -X DELETE http://localhost:8080/borrowers/098765432109

To find a borrower by id:

curl -X GET http://localhost:8080/borrowers/098765432109

Note that in these examples, http://localhost:8080 is the base URL of your Spring Boot app, and you may need to adjust it to match your app’s configuration. Also, these examples assume that you’re running your app on your local machine, so you may need to replace localhost with the IP address or hostname of the machine where your app is running.

Please also note that when you are running these commands, you should have your Spring Boot app running, otherwise you will get an error message indicating that it cannot connect to the specified address.

Also, when you are running the curl command in your local machine, the headers and payload in the command should match with the headers and payload of the request that the endpoint is expecting.

Bổ xung phương thức mượn sách

Tôi : Write me a method in BorrowerService that one borrower borrows several books
ChatGPT :
Here is an example of a method in the BorrowerService class that allows a borrower to borrow several books:

@Transactional
public Borrower borrowBooks(String borrowerId, List<String> bookIds) {
    Borrower borrower = borrowerRepository.findById(borrowerId)
            .orElseThrow(() -> new EntityNotFoundException("Borrower not found"));

    List<Book> books = bookRepository.findAllById(bookIds);
    if (books.size() != bookIds.size()) {
        throw new EntitiesNotFoundException("Some books not found");
    }

    books.forEach(book -> book.getBorrowers().add(borrower));
    borrower.getBooks().addAll(books);

    return borrowerRepository.save(borrower);
}
java

This method takes two arguments, the ID of the borrower and a list of book IDs. It first finds the borrower by ID using the BorrowerRepository, and throws an exception if the borrower is not found. Then it finds all the books by their IDs using the BookRepository, and throws an exception if some of the books are not found.
It then iterates through the books and add the borrower to the list of borrower of each book and then it add all the books to the borrower. Finally, it saves the borrower using the BorrowerRepository.

Please note that this method uses JPA to manage the many-to-many relationship between the Book and Borrower entities, if you are using other ORM like Hibernate, the implementation might be different.

Yêu cầu vẽ Mermaid diagram

Tôi : Can you generate mermaid diagrams describes Book, Borrower, BookRepository, BorrowerRepository, BookService, BorrowerService, BookController and BorrowerController?

ChatGPT:
You can copy and paste this code into the editor of website like https://mermaid-js.github.io/mermaid-live-editor/ or use any other tool that supports Mermaid diagrams.
Please note that, this is just a basic diagram representation, you can always add or remove methods and fields as per your requirement and implement the relations that you want to show.

Book+String id+String title+List authors+int publishedYearBorrower+String id+String fullName+String email+String mobilePhoneBookRepository+save(Book)+deleteById(String)+findById(String)+findAll()BorrowerRepository+save(Borrower)+deleteById(String)+findById(String)+findAll()BookService+saveBook(Book)+deleteBook(String)+findBookById(String)+getAllBooks()BorrowerService+saveBorrower(Borrower)+deleteBorrower(String)+findBorrowerById(String)+getAllBorrowers()BookController+saveBook(Book)+deleteBook(String)+findBookById(String)+getAllBooks()BorrowerController+saveBorrower(Borrower)+deleteBorrower(String)+findBorrowerById(String)+getAllBorrowers()

Tôi : give me an example of sequence diagram using Mermaid

Sequence Diagram

Yêu cầu đóng gói dự án vào Docker

Tôi : Write a Docker file that compile above SpringBoot project and build into a Alpine Linux with JDK 17

ChatGPT
Here’s an example of a Dockerfile that can be used to compile and build your Spring Boot project using Alpine Linux with JDK 17:

# Use the official Alpine Linux image as the base image
FROM alpine:3.13

# Update the package manager and install JDK 17
RUN apk update && apk add openjdk17

# Set JAVA_HOME environment variable
ENV JAVA_HOME /usr/lib/jvm/java-17-openjdk
ENV PATH ${PATH}:${JAVA_HOME}/bin

# Create a directory for the application
RUN mkdir /app

# Set the working directory to the application directory
WORKDIR /app

# Copy the application jar and dependencies to the container
COPY target/*.jar app.jar

# Expose the application's port
EXPOSE 8080

# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]
Dockerfile

This Dockerfile can be used to build an image of your application using the following command:

docker build -t my-spring-app .

The -t flag specifies the name and optionally a tag to the name of the image in the name:tag format.
This command will build an image of your application using the Dockerfile in the current directory. Once the image is built, you can start a container using the following command:

docker run -p 8080:8080 my-spring-app

The -p flag maps the host port to the container port, in this case 8080.

This command will start a container from the image and run the application inside it. The application will be accessible on port 8080 of the host machine.
Please note that you need to have your jar package ready and in the same folder where you are running the build command. Also, you might need to adjust some of the settings based on your project

Đánh giá những gì ChatGPT vừa viết

  • Là một lập trình viên và giảng viên dạy Java, Spring Boot nhiều năm kinh nghiệm, tôi đánh giá những đoạn code mà ChatGPT sinh ra có độ chính xác rất cao. Lập trình viên chỉ cần copy và paste vào dự án SpringBoot là có thể chạy được ngay. Code những đoạn code rất dài, ChatGPT viết nửa chừng rồi tạm ngừng. Bạn hãy gõ “Please continue”, chatGPT sẽ viết nốt.

  • ChatGPT có khả năng nhớ hoàn cảnh, ngữ cảnh trao đổi trước đó với con người. Ví dụ tôi yêu cầu id của model Book phải chứa đúng 12 ký tự. Trong những đoạn code ChatGPT sinh ra đều tuân thủ yêu cầu này. Book id dài đúng 12 ký tự trong lệnh curl 123456789012

curl -X GET http://localhost:8080/books/123456789012
  • ChatGPT tỷ mỷ đến mức khi yêu cầu kết nối H2, nó sẽ gợi ý các dependencies cần bổ xung vào pom.xml và cấu hình cho file application.properties để chạy được H2.,

  • Khi dùng code ChatGPT sinh ra, nếu có lỗi, hãy copy thông báo lỗi rồi yêu cầu ChatGPT gỡ lỗi. Trong nhiều trường hợp nó sẽ đưa ra đề xuất sửa lỗi,

ChatGPT có ảnh hưởng gì đến công việc của lập trình viên?

Đây là một câu hỏi mà rất nhiều lập trình băn khoăn lo lắng khi ChatGPT code quá nhanh, chính xác và hầu như không có lỗi. Tôi xin trả lời theo hiểu biết và dự đoán của cá nhân tôi:

  1. Khi có ChatGPT thì chi phí lập trình trên mỗi lập trình viên cần phải giảm xuống khoảng 60-70% cho cùng 1 đầu việc (task). Hoặc lập trình viên đó cần phải tăng năng suất lên gấp 2-3 lần. Cụ thể code phải có Unit Test, thời gian hoàn thành phải ngắn lại.
  2. Một lập trình viên cần phải dùng ChatGPT để chuyển đổi nhanh giữa các ngôn ngữ lập trình khác nhau, web frameworks, mobile frameworks khác nhau thích nghi nhanh với mọi yêu cầu công việc.
  3. Thay vì phải mất 5-8 năm mới trở thành một senior developer hay solution architect giờ với sự hỗ trợ của ChatGPT bạn chỉ nên mất khoảng 1-3 năm nếu kết hợp với các dịch vụ , pattern mà điện toán đám mây.
  4. Việc học lập trình căn bản, nắm vững kiến thức nền tảng khoa học máy tính, mạng, hệ điều hành và tự code thành thạo ít nhất một framework vẫn cần thiết. Việc ra lệnh cho ChatGPT chỉ có thể thành công khi bạn hiểu bạn cần gì và có khả năng kiểm tra những gì ChatGPT sinh ra.
  5. Lập trình viên đầu tư học tiếng Anh để viết câu chữ thật rõ ràng, chính xác ngữ nghĩa, cú pháp để ChatGPT phục vụ bạn chính xác tận tình hơn.
  6. Thời của cách học cũ đã hết, bạn phải học và làm việc với AI, dùng nó chứ không chối bỏ nó.