[SPRING] ลองต่อฐานข้อมูลกันหน่อย

จาก Blog ที่แล้วไปลองใช้ Thymeleaf ไปแล้วกับการแสดงผลให้สวยงานครับ แต่อันนั้นข้อมูลที่ได้เกิดจากการ Mock จากชั้น Service ครับ สำหรับ Blog ตอนนี้มีเป้าหมาย

  • CRUD ได้
  • ต่อ Database ได้ โดยในที่นี้ผมใช้ MySQL ครับ

สิ่งที่ต้องเตรียม

  • ต่อ Database ต้องมีการเพิ่ม Dependency เพิ่มครับ
    • JPA / Hibernate
      <dependency>
         <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter-data-jpa</artifactId>
      </dependency>
    • MySQL - เรื่อง Version ยังไงต้องตรวจดูด้วยนะครับ
      <dependency>
         <groupId>mysql</groupId>
         <artifactId>mysql-connector-java</artifactId>
         <version>5.1.6</version>
      </dependency>

มาลุยกันเลยครับ

  • ถ้าเพิ่มข้อมูลเกี่ยวกับ Person ก็ต้องมีหน้าจอ 2 จอ
    • หน้าจอ Main
    • หน้าจอสำหรับ List รายการ
    • หน้าจอสำหรับจัดการข้อมูล Person ครับ
    • หน้าจอดู Owner ครับ (เอามาจาก Blog ตอนก่อน 55)
  • แก้ไขไฟล์ application.properties  สำหรับการเชื่อมต่อฐานข้อมูล persondb ครับ
    #Database
    spring.datasource.driverClassName=com.mysql.jdbc.Driver
    spring.datasource.url=jdbc:mysql://localhost/persondb?createDatabaseIfNotExist=true
    spring.datasource.username=root
    spring.datasource.password=
    spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
    spring.jpa.properties.hibernate.generate_statistics=true
    spring.jpa.hibernate.ddl-auto=update
    • spring.datasource.driverClassName อันนี้ก็ใช้ของ MySQL ครับ
    • spring.jpa.database-platform  ก็เช่นกันครับ กำหนดให้ถูกนะครับ ถ้าลืม อาจจะเจอ Exception แบบนี้

      org.hibernate.HibernateException: Access to DialectResolutionInfo cannot be null when 'hibernate.dialect' not set

    • createDatabaseIfNotExist=true  สำหรับ เอาไว้กำหนดว่า ถ้าไม่เจอ Database ก็ให้สร้างใหม่เลยครับ
    • ตอน Run อย่าลืม เปิด Service ของ MySQL ตาม Config ที่กำหนดไว้นะครับ ไม่งั้นจะเจอ Error

      com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure

  • อย่าลืมไปเอา exclude = {DataSourceAutoConfiguration.class}  ออกจาก @SpringBootApplication  ที่ไฟล์ [ชื่อ Project]Application.java  ด้วยนะครับ เดี๋ยวจะต่อ DB ไม่ได้เอา บางที่ Spring Intializer มัน Generate มาให้ครับ
  • แก้ไข Entity ครับ ให้เหมาะกับ Table ครับ โดยผมเพิ่ม Column Id เข้าไปครับ และใส่ Annotation ของ Hibernate เข้าไป ซึ่งมีทั้งพวก Validate ข้อมูล และระบุ Column ครับ
  • เพิ่มชั้น Repositories เข้ามาครับ โดยตัวนี้จะช่วยให้การติดต่อฐานข้อมูลง่ายขึ้นครับ
    • สร้าง Folder Repositories ครับ
    • สร้างไฟล์ PersonRepository.java  ครับ โดยผมใช้ JPARepository ครับ เนื่องจาก Feature เยอะว่าครับ ถ้าลอง Search หลายๆเว็บจะใช้ CrudRepository ครับ JPARepository  มาจาก CrudRepository, PagingAndSortingRepository, QueryByExampleExecutor และ Repository ครับ
      	        
               Repository
                   |
                   V
             CrudRepository
          	     |
          	     V
      PagingAndSortingRepository     QueryByExampleExecutor
          			         | 	   |
          			         V 	   V
      		                      JpaRepository
      
    • มาดูไฟล์ PersonRepository.java  สมบูรณ์ครับ เนื่องจากไม่มี Cusotmize อะไรเลย ยุ่งกับ Table 1 ต่อ 1 ครับ
      package com.cu.thesis.WeMuBPMN.repository;
      
      import com.cu.thesis.WeMuBPMN.entity.Person;
      
      import org.springframework.data.jpa.repository.JpaRepository;
      import org.springframework.stereotype.Repository;
      
      @Repository
      public interface PersonRepository extends JpaRepository<Person, Integer>
      {
      
      }
  • แก้ไข Service  โดยหลักจะเป็นการเตรียม Method สำหรับ List รายการ / เพิ่ม / ลบ / แก้ไข / เรียกดูข้อมูล ครับ แต่ในส่วนของ Service อันนี้ผมจะแยกเป็น Interface กับ Implement ครับ
    • Interface กำหนด Spec ที่ไฟล์ PersonService.java ครับ
      package com.cu.thesis.WeMuBPMN.service
      import com.cu.thesis.WeMuBPMN.entity.Person;
      
      public interface PersonService
      {
         Person viewOwner();
         Iterable<Person> listAllPersons();
         Person getPersonById(Integer id);
         Person savePerson(Person product);
         void deletePerson(Integer id);
      }
      
    • Implement ใส่การทำงานจริงครับ ที่ไฟล์ PersonServiceImpl.java ครับ
      package com.cu.thesis.WeMuBPMN.service;
      
      import com.cu.thesis.WeMuBPMN.entity.Person;
      import com.cu.thesis.WeMuBPMN.repository.PersonRepository;
      
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Service;
      
      @Service
      public class PersonServiceImpl implements PersonService
      {
       
          private PersonRepository personRepository;
      
          @Autowired
          public void setPersonRepository(PersonRepository personRepository)
          {
              this.personRepository = personRepository;
          }
      
          @Override
          public Person viewOwner()
          {
              return new Person("Chatri", "Ngambenchawong");
          }
      
          @Override
          public Iterable<Person> listAllPersons() {
              return personRepository.findAll();
          }
      
          @Override
          public Person getPersonById(Integer id) {
              return personRepository.findById(id).get();
          }
      
          @Override
          public Person savePerson(Person product) {
              return personRepository.save(product);
          }
      
          @Override
          public void deletePerson(Integer id) {
              personRepository.deleteById(id);
          }
      }
  • แก้ไข Controller โดยต้องเพิ่ม 2 ไฟล์
    • ไฟล์ IndexController.java  เอาไว้จัดการกับทุก Request ที่เข้ามาครับ โดยใช้งานคู่กับ ไฟล์ index.html  ครับ
      package com.cu.thesis.WeMuBPMN.controller;
      
      import org.springframework.stereotype.Controller;
      import org.springframework.web.bind.annotation.RequestMapping;
      
      @Controller
      public class IndexController
      {
          @RequestMapping(value = "/")
          public String index() {
              return "index";
          }
      }
    • ไฟล์ PersonController.java เอาไว้จัดการกับทุก Request ที่เกี่ยวกับ [basepath]/persons (หน้า List) และ [basepath]/person  ครับ
      package com.cu.thesis.WeMuBPMN.controller;
      
      import com.cu.thesis.WeMuBPMN.entity.Person;
      import com.cu.thesis.WeMuBPMN.service.PersonService;
      
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.stereotype.Controller;
      import org.springframework.ui.Model;
      import org.springframework.ui.ModelMap;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.PathVariable;
      import org.springframework.web.bind.annotation.RequestMapping;
      import org.springframework.web.bind.annotation.RequestMethod;
      
      @Controller
      public class PersonController
      {
          @Autowired
          private PersonService personService;
          
          @GetMapping(value ="/person/owner")
          public String index(ModelMap model){
              Person person = personService.viewOwner();
              //person - parameter แรก ถูกเอาไปใช้ที่หน้า UI
              model.addAttribute("person",person);
              //บอกว่าไฟล์เราอยู่ที่ Folder person ไฟล์ index.html
              return "person/owner";
          }
      
          /**
           * List all products.
           *
           * @param model
           * @return
           */
          @RequestMapping(value = "/persons", method = RequestMethod.GET)
          public String listPerson(Model model) {
              model.addAttribute("persons", personService.listAllPersons());
              System.out.println("Returning persons:");
              return "person/persons";
          }
      
          /**
           * View a specific person by its id.
           *
           * @param id
           * @param model
           * @return
           */
          @RequestMapping(value = "person/{id}")
          public String showPerson(@PathVariable Integer id, Model model) {
              model.addAttribute("person", personService.getPersonById(id));
              return "person/viewPerson";
          }
      
          // Afficher le formulaire de modification du Product
          @RequestMapping(value = "person/edit/{id}")
          public String editPerson(@PathVariable Integer id, Model model) {
              model.addAttribute("person", personService.getPersonById(id));
              return "person/editPerson";
          }
      
          /**
           * New person.
           *
           * @param model
           * @return
           */
          @RequestMapping(value = "person/new")
          public String newPerson(Model model) {
              model.addAttribute("person", new Person());
              return "person/editPerson";
          }
      
          /**
           * Save person to database.
           *
           * @param person
           * @return
           */
          @RequestMapping(value = "person", method = RequestMethod.POST)
          public String savePerson(Person person) {
              personService.savePerson(person);
              return "redirect:/persons";
              //return "redirect:/person/" + person.getId();
          }
      
          /**
           * Delete person by its id.
           *
           * @param id
           * @return
           */
          @RequestMapping(value = "person/delete/{id}")
          public String deletePerson(@PathVariable Integer id) {
              personService.deletePerson(id);
              return "redirect:/persons";
          }
      }
  • หลังจากได้ Controller แล้วมาดูหน้าจอ View ครับ (อันนี้สำหรับผมแล้วเสียเวลาไปวันเต็มๆเลย)
    • สำหรับ Default Path ของ Thymeleaf จะถูก Map พวกไฟล์ View และผองเพื่อน (พวก CSS - ผมใช้ Bootstrap 4, /JS/ Image) ไว้ที่ [CLASS PATH]/resources/static/ ครับ โดยมีโครงสร้างคร่าวๆ ดังนี้
      resources
      -> static 
         -> css - พวก CSS จะเก็บในนี้
            -> bootstrap.xxx.css ใส่ไฟล์เกี่ยวกับ bootstrap
            -> person.css ใช้คู่กับ owner.html ครับ
         -> js - พวก javascript จะเก็บไว้ในนี้
            -> bootstrap.xxx.js ใส่ไฟล์เกี่ยวกับ bootstrap
      -> templates
         -> fragments - เอาไว้เก็บไฟล์ที่สามารถ Reuse ได้
            -> header.html - ในที่นี้ทำเป็นเมนูครับ
         -> person - เก็บไฟล์ที่ต้องใช้คู่กับ PersonController ทั้งหมดเลย โดยตอน Return ขึ้นต้นด้วยว่า person/ชื่อไฟล์
            -> persons.html สำหรับหน้าจอ List
            -> editPerson.html สำหรับ Model New/Edit
            -> viewPerson.html สำหรับ Model View
            -> owner.html
         -> index.html สำหรับหน้าจอ Main ครับ
    • สำหรับ View มีข้อควรระวังของ Thymeleaf
      • การอ้างอิงไฟล์  CSS ใน Tag <link>  ให้มีการระบุ Path ผ่าน th:href="@{/css/bootstrap.min.css}"  ครับ โดยมันจะไปอ้างอิงตาม Default Path ของ Thymeleaf ครับ
        <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}" href="../../static/css/bootstrap.min.css"  media="screen"/>
      • การอ้างอิงไฟล์ JS ใน Tag <script>  ให้มีการระบุ Path ผ่าน th:src="@{/js/bootstrap.min.js}"  ครับ โดยมันจะไปอ้างอิงตาม Default Path ของ Thymeleaf ครับ
         <script th:src="@{/js/bootstrap.min.js}" src="../../js/bootstrap.min.js"></script>
      • สำหรับการ Include ไฟล์อื่นเข้าไป อย่างเช่น ผมต้องการ Include ไฟล์ header.html  เข้าไปครับ โดยใช้ <th:block th:include=  ครับ
        <th:block th:include="fragments/header :: header"></th:block>
    • ตัวอย่าง Code ของแต่ละไฟล์ เอาแค่บางส่วนนะครับ เดี๋ยว Blog จะยาวไป
      • ไฟล์ index.html  ครับ
        <!DOCTYPE html>
        <html>
        <head lang="en">
            <title>Spring Boot Example</title>
            <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}" href="../../static/css/bootstrap.min.css"  media="screen"/>
            <script th:src="@{/js/bootstrap.min.js}" src="../../js/bootstrap.min.js"></script> 
            <th:block th:include="fragments/header :: header"></th:block>
        </head>
        <body>
        
        <div class="container">
        
        </div>
        </body>
        </html>
      • ไฟล์ persons.html ครับ แสดงรายการของ Person ทั้งหมดครับ
        <!DOCTYPE html>
        <html xmlns:th="http://www.thymeleaf.org">
        <head lang="en">
        
            <title>Spring Boot Exemple</title>
            <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}" href="../../static/css/bootstrap.min.css"  media="screen"/>
            <script th:src="@{/js/bootstrap.min.js}" src="../../js/bootstrap.min.js"></script> 
            <th:block th:include="fragments/header :: header"></th:block>
        
        </head>
        <body>
            <div class="container">
                <div th:if="${not #lists.isEmpty(persons)}">
                    <h2>Person List</h2>
                    <table class="table table-striped">
                        <tr>
                            <th>Id</th>
                            <th>Firstname</th>
                            <th>Lastname</th>
                            <th>View</th>
                            <th>Edit</th>
                            <th>Delete</th>
                        </tr>
                        <tr th:each="person : ${persons}">
                            <td th:text="${person.id}"><a href="/person/${person.id}">Id</a></td>
                            <td th:text="${person.Firstname}">Product Id</td>
                            <td th:text="${person.Lastname}">descirption</td>
                            <td><a th:href="${ '/person/' + person.id}">View</a></td>
                            <td><a th:href="${'/person/edit/' + person.id}">Edit</a></td>
                            <td><a th:href="${'/person/delete/' + person.id}">Delete</a></td>
                        </tr>
                    </table>
        
                </div>
            </div>
        </body>
        </html>
      • ไฟล์ editPerson.html  ครับ สำหรับ New / Edit Person ครับ
        <!DOCTYPE html>
        <html xmlns:th="http://www.thymeleaf.org">
        <head lang="en">
            <title>Spring Boot Example</title>
            <link rel="stylesheet" type="text/css" th:href="@{/css/bootstrap.min.css}" href="../../static/css/bootstrap.min.css"  media="screen"/>
            <script th:src="@{/js/bootstrap.min.js}" src="../../js/bootstrap.min.js"></script> 
            <th:block th:include="fragments/header :: header"></th:block>
        </head>
        <body>
            <div class="container">
                <h2>Person Details</h2>
                <div>
                    <form class="form-horizontal" th:object="${person}" th:action="@{/person}" method="post">
                        <input type="hidden" th:field="*{id}"/>
                        <!-- <input type="hidden" th:field="*{version}"/> -->
                        <div class="form-group">
                            <label class="col-sm-2 control-label">Firstname:</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control" th:field="*{Firstname}"/>
                            </div>
                        </div>
                        <div class="form-group">
                            <label class="col-sm-2 control-label">Lastname:</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control" th:field="*{Lastname}"/>
                            </div>
                        </div>
                        <div class="form-group">
                            <button type="submit" class="btn btn-default">Submit</button>
                        </div>
                    </form>
                </div>
            </div>
        </body>
        </html>

ทดสอบกันครับ

  • เข้าเว็บเล่นกันครับ ผ่าน http://localhost:8080  ครับ
  • ลองสร้าง Person
  • เข้ามาดูหน้า List Person
  • View รายการ 1 รายการ

Code ที่เสร็จแล้ว

  • Download ไปลองเล่นกันได้ครับ

สำหรับ Blog ตอนแรกคิดว่าใช้เวลาทำ 2 ชั่วโมง แต่ติดปัญหาด้าน UI จนยาวมาวันครึ่งแทนครับ ใน Blog ตอนนี้ ผมคง Focus เรื่องขา Views (UI) เพราะยังได้กลิ่นของ Code ที่ซ้ำอยู่ครับ และยังขาดในส่วนของการ Validate ข้อมูลครับ


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts to your email.