จาก 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>
- JPA / Hibernate
มาลุยกันเลยครับ
- ถ้าเพิ่มข้อมูลเกี่ยวกับ 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); } }
- Interface กำหนด Spec ที่ไฟล์ PersonService.java ครับ
- แก้ไข 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"; } }
- ไฟล์ IndexController.java เอาไว้จัดการกับทุก Request ที่เข้ามาครับ โดยใช้งานคู่กับ ไฟล์ index.html ครับ
- หลังจากได้ 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>
- การอ้างอิงไฟล์ CSS ใน Tag <link> ให้มีการระบุ Path ผ่าน th:href="@{/css/bootstrap.min.css}" ครับ โดยมันจะไปอ้างอิงตาม Default Path ของ Thymeleaf ครับ
- ตัวอย่าง 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>
- ไฟล์ index.html ครับ
- สำหรับ Default Path ของ Thymeleaf จะถูก Map พวกไฟล์ View และผองเพื่อน (พวก CSS - ผมใช้ Bootstrap 4, /JS/ Image) ไว้ที่ [CLASS PATH]/resources/static/ ครับ โดยมีโครงสร้างคร่าวๆ ดังนี้
ทดสอบกันครับ
- เข้าเว็บเล่นกันครับ ผ่าน 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.