ตรวจสอบ Lib ที่ใช้ว่ามีช่องโหว่หรือไม่ด้วย Dependency check

ตรวจสอบ Lib Java ที่ใช้ว่ามีช่องโหว่หรือไม่ด้วย Dependency check

Dependency Check

ในปัจจุบันเรื่อง Security ของ Software นั้นเริ่มถูกสนใจมากยิ่งขึ้น นั่นก็เพราะ Software นั้นถูกใช้กันมากขึ้น และถูกใช้ในงานสำคัญเรื่อยๆ เช่น ธุรกรรมทางการเงิน ดังนั้นถ้าตัว Software มีช่องโหว่ที่ให้ผู้ไม่หวังเข้ามาโจมตีได้ก็จะสร้างความเสียหายให้กับตัวผู้ใช้งานและบริษัทที่พัฒนา Software

ช่องโหว่หนึ่งที่ Developer นั้นต้องรับผิดชอบคือช่องโหว่ที่มาจาก Lib ที่เรานำมาใช้ ซึ่งเป็นเรื่องปกติในการพัฒนา Software ที่เราต้องนำ Lib มาใช้เพื่อลดเวลาในการพัฒนา แต่ Lib มันก็เป็น Code ซึ่งมันก็มีโอกาสจะมีช่องโหว่ ซึ่งเมื่อมันมีช่องโหว่ Software ที่เราเขียนก็จะมีช่องโหว่ตามทันที ซึ่งวิธีการแก้ไขสำหรับการที่ Lib มีช่องโหว่นั้นก็ไม่ยากครับ ก็แค่เข้าไป Update ตัว Lib ให้เป็น version ที่เขาแก้ไขเรื่องช่องโหว่นั้นแล้ว ซึ่ง Lib ที่เป็นที่นิยมมากๆ และมีผู้พัฒนาคอยดูแลจะรีบเข้ามาแก้ไขอย่างรวดเร็ว ดังนั้นถ้าเลือกจะใช้ Lib ตัวไหน จำนวนคนที่ใช้งานและจำนวนผู้พัฒนานั้นเป็นตัวเลือกที่สำคัญในการพัฒนา

แต่ปัญหานี้ยังถูกแก้ไม่หมดเสียทีเดียวเพราะ Developer เวลาพัฒนา Software นั้นก็ไม่ทราบว่า Lib ตัวไหนนั้นมีช่องโหว่อะไร และส่วนใหญ่ก็อาจจะค้นใน Internet ว่าเขาเขียนยังไง ใช้ Lib อะไรแล้วก็ copy ตัว Lib จากตัวอย่างใน Inernet มาใช้เลย ซึ่ง Version lib ในตัวอย่างนั้นอาจเก่าแล้วและมีช่องโหว่ ซึ่งจากปัญหาที่เกิดขึ้นแบบนี้เหล่า Developer ก็เลยสร้างเครื่องมือขึ้นมาเพื่อ Scan ตัว Lib ใน Version ที่ใช้ในการพัฒนาว่ามีช่องโหว่หรือไม่ ซึ่งนั้นก็คือ Dependency Check

Dependency check with MAVEN

ท่านสามารถดูตัวอย่าง Source code ได้ที่ https://github.com/surapong-taotiamton/test-dependency-check

ตัว Dependecncy check นั้นสามารถนำไปได้ใช้ได้หลายรูปแบบทั้งแบบ command line หรือเป็น Jenkins Plugin แต่ตัวอย่างนี้จะแสดงวิธีการใช้ด้วย MAVEN (พอดีผมเขียน Project java โดยใช้ MAVEN เป็นตัว Build และ จัดการ Dependency )

โดยวิธีการใช้นั้นง่ายมากเพียงท่าน copy ส่วนที่เป็น profile ไปวางในไฟล์ pom.xml ของ project

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<project>
<dependencies>
...
</dependencies>

<!-- >
เพิ่มตรงส่วน profile เข้าไป
-->
<profiles>
<profile>
<id>scan-vulnerability</id>
<build>
<plugins>
<plugin>
<groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId>
<version>6.5.0</version>
<configuration>
<failBuildOnCVSS>1</failBuildOnCVSS>
</configuration>

<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>

</project>

จากนั้นสั่ง run ด้วย command

1
mvn -P scan-vulnerability dependency-check:check

ซึ่งในการสั่งครั้งแรกอาจจะกินเวลานานหน่อยเพราะตัว Dependency check จะทำการ Download ตัวฐานข้อมูลช่องโหว่ที่มีอยู่ใน NATIONAL VULNERABILITY DATABASE มาไว้ที่เครื่องเพื่อใช้ในการ Scan ซึ่งผลลัพธ์ที่ได้ประมาณนี้

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[ERROR] Failed to execute goal org.owasp:dependency-check-maven:6.5.0:check (default-cli) on project testdependencycheck:
[ERROR]
[ERROR] One or more dependencies were identified with vulnerabilities that have a CVSS score greater than or equal to '1.0':
[ERROR]
[ERROR] log4j-core-2.14.1.jar: CVE-2021-44228, CVE-2021-45105, CVE-2021-45046
[ERROR]
[ERROR] See the dependency-check report for more details.
[ERROR]
[ERROR]
[ERROR] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

ซึ่งการดูอย่างนี้อาจไม่ละเอียดตัว Dependency check จะมีการออก Report ให้ดูแบบละเอียดได้ โดยเข้าไปที่ folder : target ที่อยู่ใน folder project

Path to Dependency check report

โดยตัวอย่าง Report จะหน้าตาแบบนี้ โดยในกรอบสีแดงจะเป็น Lib ที่มีช่องโหว่

Dependency check report

จะรู้ได้ไงว่ามาจากไหน

จากตัวอย่างจะเห็นว่ามีช่องโหว่จาก Lib : log4j 2.14.1 ซึ่งถ้าเราลองไปหาใน pom.xml จะไม่มีตรงไหนเรียกใช้ตัว log4j แบบตรงๆเลย ซึ่งจะทำให้เรางงว่าจะต้องแก้ตรงไหน ดังนั้นจึงต้องสั่งด้วยคำสั่ง

1
mvn dependency:tree

ซึ่งจะได้ผลลัพธ์ดังนี้

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
[INFO] --- maven-dependency-plugin:3.2.0:tree (default-cli) @ testdependencycheck ---
[INFO] blog.surapong.example:testdependencycheck:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.6.1:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.6.1:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:2.6.1:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.6.1:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.6.1:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.7:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.7:compile
[INFO] | | | \- org.apache.logging.log4j:log4j-to-slf4j:jar:2.14.1:compile
[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.29:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.6.1:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.0:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.0:compile
[INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.13.0:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.0:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.0:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.0:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.6.1:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.55:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.55:compile
[INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.55:compile
[INFO] | +- org.springframework:spring-web:jar:5.3.13:compile
[INFO] | | \- org.springframework:spring-beans:jar:5.3.13:compile
[INFO] | \- org.springframework:spring-webmvc:jar:5.3.13:compile
[INFO] | +- org.springframework:spring-aop:jar:5.3.13:compile
[INFO] | +- org.springframework:spring-context:jar:5.3.13:compile
[INFO] | \- org.springframework:spring-expression:jar:5.3.13:compile
[INFO] +- org.springframework.boot:spring-boot-starter-log4j2:jar:2.6.1:compile
[INFO] | +- org.apache.logging.log4j:log4j-slf4j-impl:jar:2.14.1:compile
[INFO] | | +- org.slf4j:slf4j-api:jar:1.7.32:compile
[INFO] | | \- org.apache.logging.log4j:log4j-api:jar:2.14.1:compile
[INFO] | +- org.apache.logging.log4j:log4j-core:jar:2.14.1:compile
[INFO] | +- org.apache.logging.log4j:log4j-jul:jar:2.14.1:compile
[INFO] | \- org.slf4j:jul-to-slf4j:jar:1.7.32:compile
[INFO] +- org.projectlombok:lombok:jar:1.18.22:compile (optional)
[INFO] \- org.springframework.boot:spring-boot-starter-test:jar:2.6.1:test
[INFO] +- org.springframework.boot:spring-boot-test:jar:2.6.1:test
[INFO] +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.6.1:test
[INFO] +- com.jayway.jsonpath:json-path:jar:2.6.0:test
[INFO] | \- net.minidev:json-smart:jar:2.4.7:test
[INFO] | \- net.minidev:accessors-smart:jar:2.4.7:test
[INFO] | \- org.ow2.asm:asm:jar:9.1:test
[INFO] +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:test
[INFO] | \- jakarta.activation:jakarta.activation-api:jar:1.2.2:test
[INFO] +- org.assertj:assertj-core:jar:3.21.0:test
[INFO] +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] +- org.junit.jupiter:junit-jupiter:jar:5.8.1:test
[INFO] | +- org.junit.jupiter:junit-jupiter-api:jar:5.8.1:test
[INFO] | | +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] | | +- org.junit.platform:junit-platform-commons:jar:1.8.1:test
[INFO] | | \- org.apiguardian:apiguardian-api:jar:1.1.2:test
[INFO] | +- org.junit.jupiter:junit-jupiter-params:jar:5.8.1:test
[INFO] | \- org.junit.jupiter:junit-jupiter-engine:jar:5.8.1:test
[INFO] | \- org.junit.platform:junit-platform-engine:jar:1.8.1:test
[INFO] +- org.mockito:mockito-core:jar:4.0.0:test
[INFO] | +- net.bytebuddy:byte-buddy:jar:1.11.22:test
[INFO] | +- net.bytebuddy:byte-buddy-agent:jar:1.11.22:test
[INFO] | \- org.objenesis:objenesis:jar:3.2:test
[INFO] +- org.mockito:mockito-junit-jupiter:jar:4.0.0:test
[INFO] +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] +- org.springframework:spring-core:jar:5.3.13:compile
[INFO] | \- org.springframework:spring-jcl:jar:5.3.13:compile
[INFO] +- org.springframework:spring-test:jar:5.3.13:test
[INFO] \- org.xmlunit:xmlunit-core:jar:2.8.3:test

ซึ่งจากผลลัพธ์เราจะเห็นว่า log4j นั้นมาจาก org.springframework.boot:spring-boot-starter-log4j2 ซึ่งพอเรารู้ว่ามาจาก spring-boot ที่ใช้ log4j ซึ่งวิธีแก้ของแต่ละ lib ที่ใช้ก็จะแตกต่างกันไป แต่โดยส่วนใหญ่จะคือการอัพ Version ตัว Lib เป็นหลัก โดยของ spring-boot-starter-log4j2 อันนี้จะสามารถแก้ได้โดยทำตาม Link นี้ https://spring.io/blog/2021/12/10/log4j2-vulnerability-and-spring-boot ซึ่งคือการเพิ่ม properties ในไฟล์ pom.xml

1
2
3
4
<properties>
<java.version>11</java.version>
<log4j2.version>2.17.0</log4j2.version>
</properties>

ซึ่งเมื่อเพิ่มแล้วจะเห็นว่าไม่มีช่องโหว่แล้ว แต่สำหรับ Lib อื่นนั้นอาจจะมีวิธีแก้ต่างกันไป อาจจะอัพเดทตัว lib นั้นเป็น version ที่ใหม่กว่าเลยเป็นต้น

สรุป

สำหรับบทความนี้ก็ได้สอนวิธีการใช้ Dependecny check เพื่อหา Lib ที่มีช่องโหว่นะครับว่าทำยังไง และได้สาธิตการหาว่า Lib ที่มีช่องโหว่นั้นมาจากการประกาศตัวไหน และวิธีแก้ไขเบื้องต้น

MISBEHAVING - เศรษฐศาสตร์พฤติกรรม

MISBEHAVING - เศรษฐศาสตร์พฤติกรรม

ผมเห็นหนังสือเล่มนี้ครั้งแรกผ่านการ Review ของเพื่อนใน Facebook คนหนึ่ง โดยส่วนตัวผมไม่ค่อยชอบอ่าน Review ของคนที่ได้รับ Sponsor หรือสำนักพิมพ์เพราะเขาต้องการจะขายของอยู่แล้ว ดังนั้นเขาก็ต้องบอกว่าหนังสือเล่มนี้ดีอยู่แล้ว ดังนั้นผมจึงชอบอ่าน Review ของคนที่บุคคลธรรมดาทั่วไปมากกว่า เพราะเขาไม่มีส่วนได้ส่วนเสีย ซึ่งเพื่อนผมคนนี้ก็บอกว่าหนังสือเล่มนี้ดีในระดับที่ประทับใจ ผมก็เลยกดซื้อในงานหนังสือออนไลน์ที่ผ่านมา ซึ่งอันนี้คือเล่มหลักที่ซื้อเลย (พวก A Little History นี่เพื่อให้ได้ส่วนลด) ซึ่งพอได้เริ่มอ่านก็รู้สึกว่าดีจริงเหมือนที่เพื่อน Review

MISBEHAVING - เศรษฐศาสตร์พฤติกรรม

มนุษย์อีค่อน

มนุษย์อีค่อนคือคำที่หนังสือเล่มนี้ใช้บ่อยๆ มนุษย์อีค่อนคือมนุษย์ในโมเดลเศรษฐศาสตร์โดยลักษณะของมนุษย์อีค่อนคือใช้หลักการเหตุผล ทางคณิตศาสตร์หาตัวเลือกที่ดีที่สุด เหมาะสมที่สุด ในการตัดสินใจเรื่องใดเรื่องหนึ่ง ซึ่งมนุษย์อีค่อนนี่คือมนุษย์ที่ผมอยากจะเป็นมากๆ เพราะเขาเป็นคนใช้เหตุผล และคิดรอบด้านจริงๆ พออ่านมาถึงตรงนี้แล้วก็แบบว่า อ้าวเห้ยแล้วมันไม่ดีตรงไหน มันไม่ดีตรงที่มนุษย์อีค่อนไม่ใช่มนุษย์ธรรมดาแบบเรา ซึ่งมันตลกตรงที่ในสาขาเศรษฐศาสตร์ในสมัยก่อนปี ค.ศ. 1970 เนี่ย เขาใช้โมเดลเศรษฐศาสตร์โดยการคิดว่ามนุษย์ในโมเดลนั้นคือมนุษย์อีค่อน ตอนผมอ่านนี่แบบเฮ้ยเอาจริงดิ มันได้เหรอวะ คือผมคนนอกสายยังแบบเหวอเลย แล้วคนในสายเขาไม่เหวอบ้างเหรอวะ ซึ่งจริงๆคนในสายจำนวนหนึ่งก็รู้สึกไม่ปกติแหละ แต่ในเมื่อกระแสหลักของสายนี้เป็นแบบนั้นการมาออกมาว่ามันแปลกนะเป็นเรื่องที่ทำไม่ได้และไม่มีคนสนใจ เทียบง่ายๆก็ตอนที่ทุกคนในสายวิทยาศาสตร์ยึดหลักทฤษฎีว่าโลกเป็นศูนย์กลางจักรวาล ใครที่ออกมาพยายามพิสูจน์ว่าทฤษฎีนี้ไม่จริงนั้นจะถูกมองว่า เฮ้ย คุณปกติดีรึเปล่า ทฤษฎีคุณมีอะไรผิดปกติรึเปล่าประมาณนั้น ซึ่งหนังสือตัวนี้จะเป็นเรื่องของการที่ผู้เขียนพยายามพิสูจน์ว่าโมเดลที่ใช้มนุษย์อีค่อนนั้นมันไม่สมจริง มันมีความคลาดเคลื่อนจากความเป็นจริง เราควรจะเปลี่ยนตัวโมเดลมาเป็นการใช้มนุษย์แบบธรรมดาในการคิดคำนวณนะ

เหตุการณ์แปลกๆของมนุษย์ธรรมดา (การขาดสติ)

ผลลัพธ์เท่ากันแต่เลือกไม่เหมือนกัน

มีผู้ป่วย 600 คนป่วยด้วยโรคชนิดหนึ่ง มีนโยบาย 2 นโยบายให้คุณเลือกเพื่อแก้ปัญหาโรคระบาดนี้

  • A : ช่วยชีวิตคนได้แน่นอน 200 คน
  • B : มีโอกาส 1 ใน 3 (33 %) ที่ทุกคนจะรอด และ 2 ใน 3 (67%) คือทุกคนตาย

ในตัวนี้ตรงนี้คนส่วนใหญ่จะเลือกข้อ A กันครับ แต่ปัญหามันอยู่ที่พอเราเปลี่ยนคำเป็น

  • A : มีคนตายแน่นอน 400 คน
  • B : มีโอกาส 1 ใน 3 (33 %) ที่ทุกคนจะรอด และ 2 ใน 3 (67%) คือทุกคนตาย

คนกลับเลือกนโยบาย B กันครับ คำถามคือถ้าเราเป็นมนุษย์อีค่อนทำไมเราไม่เลือกเหมือนกันทั้งที่ผลลัพธ์ทุกอย่างเหมือนกัน คำตอบมันมีครับคือการตัดสินของมนุษย์ธรรมดานั้นมีเหตุผลเข้ามาเกี่ยว ถ้าพูดถึงการช่วยชีวิตเราอยากช่วยแบบให้ได้แน่นอน 100% เราจะไม่เสี่ยงกับการมีชีวิตรอดแค่ 1 ใน 3 แต่กลับกันถ้าเป็นเราพูดด้วยความตาย เราจะไม่อยากให้ใครตายแน่นอนเราเลยไปเลือกข้อที่มีโอกาสเสี่ยงที่จะมีโอกาสรอดโดยมองข้ามว่าจริงๆมีคนรอดแน่นอน 200 คน อันนี้เราอาจจะรู้ลึกๆว่าจริงๆ แต่ในทางเศรษฐศาสตร์นั้นต้องการหลักฐานที่ยืนยันว่าชัดเจนกว่านี้ และเขายังมีข้้ออ้างมากมายเช่น ผู้ที่ทำการทดสอบไม่ได้ใช้เหตุผลมากพอ ตอบส่งๆ ต่างๆนาๆ

อีกตัวอย่างนึง

ถ้าคุณจ่ายเงินด้วยบัตรเครดิตคุณจะเสียเงินเพิ่ม 3 % (ว่าง่ายๆคือถ้าของ 100 จ่าย 103) คุณจะจ่ายไหม ถ้าคุณคิดแบบผมคุณก็คงจะไม่จ่ายเพราะมันเหมือนเราโดนจ่ายเพิ่มจากราคาปกติ

แต่ถ้าลองเปลี่ยนเป็นจ่ายด้วยบัตรเครดิตคุณจะต้องจ่ายในราคา 103 บาท แต่ถ้าจ่ายด้วยเงินสดคุณจะได้ส่วนลดเหลือเพียง 100 บาท คุณจะรู้สึกว่าเออจ่ายด้วยบัตรเครดิตก็ได้ มันแปลกไหม ทั้งที่ผลลัพธ์เหมือนกันแต่คุณกลับเลือกแบบนึงแต่ไม่เลือกอีกแบบนึง ในส่วนนี้มันมีเหตุผลรองรับพฤติกรรมอยู่ซึ่งนั่นก็คือในกรณีนี้การจ่าย 103 บาทเหมือนการจ่ายปกติ การจ่ายเงินแบบ 100 เป็นแค่ตัวเลือก (ซึ่งยากกว่าปกติ) ที่คุณสามารถจะไม่เลือกก็ได้

การให้คุณค่า

ในปกติมนุษย์อีค่อนนั้นเงิน 100 นั้นมีค่าเท่ากันไม่ว่าจะเสียไปหรือได้รับมา แต่ถ้าลองมองในชีวิตจริงสิครับ ถ้าคุณได้รับเงิน 100 นึงกับเสียเงิน 100 ความรู้สึกของเราจะเป็นดังภาพเลย โดยเหตุการณ์นี้มันมีทฤษฎีรองรับอยู่นะครับ คือถ้าคุณได้เงินมากขึ้นไประดับนึงแล้วความสุขที่คุณได้รับมันจะได้มากขึ้นขนาดนั้น แต่ถ้าเป็นการสูญเสียคุณจะรู้สึกเสียใจเป็นอย่างมาก อีกทั้งคุณยังรู้สึกว่ามันเป็นของของคุณแล้ว คุณไม่อยากจะเสียไป

ทุนจม

อาการนี้พวกเราหลายคนก็จะเจอ เหมือนคุณไปซื้อตั๋วหนังล่วงหน้า (หรือได้ตั๋วฟรีมา) หรือสมัครฟิสเนต คุณจะมีความรู้สึกว่าคุณจะต้องไปทำดูหนังเรื่องนั้นให้ได้ ไปเล่นฟิสเนตให้ได้ อาการแบบนี้เรียกอาการทุนจมครับ คือถ้าเป็นมนุษย์อีค่อนเนี่ย เรื่องที่เสียเงินไปแล้วไม่ควรมีผลตัดสินใจ

นำสิ่งแปลกๆนี้มาประยุกต์ใช้งานจริง

ในหนังสือได้มีการเล่าว่าผู้เขียนเอาพฤติกรรมที่เล่ามาด้านบน (จริงๆมีเยอะกว่านี้มาก) ไปประยุกต์ใช้ในงานจริงกับรีสอร์ทสกี คือเขาจะขายเป็นแพ็คเกจตั๋ว ซื้อแล้วสามารถเอามาใช้ได้ในปีนั้นซึ่งขายเป็นเซ็ต 10 ใบ ซึ่งจากการทำแบบนี้ทำให้ลูกค้าจะพยายามมาที่รีสอร์ทบ่อยขึ้นซึ่งตัวรีสอร์ทอยากให้มาบ่อยเพราะจะได้ค่าอาหารและค่าเบียร์ซึ่งทำกำไรให้กับรีสอร์ทได้มาก

สรุป

สำหรับหนังสือเล่มนี้นั้นชี้เห็นให้เกี่ยวกับพฤติกรรมของมนุษย์ที่ดูจะไม่เป็นเหตุเป็นผล ในหนังจะเรียกว่าการ “ขาดสติ” ซึ่งหลายๆอาการที่ผมอ่านเจอแล้วแบบเออว่ะ ใช่เลย เราก็รู้สึกแบบนั้น ซึ่งหนังสือทำการยกตัวอย่างง่ายๆที่เข้าใจได้ง่ายมาก แบบคุณไม่ต้องเก่งคณิตศาสตร์ก็สามารถเข้าใจได้ จากนั้นผู้เขียนจะเล่าการต่อสู้เพื่อจะทำให้วงการวิชาการตระหนักรู้และเข้าใจว่าควรเอาเรื่องการขาดสติไปใช้คาดการณ์ในโมเดลทางเศรษฐศาสตร์เพื่อความแม่นจำของโมเดล หรือควรจะเอาพฤติกรรมเหล่านี้ไปปรับใช้กับนโยบายของรัฐ หรือ การบริหารจัดการต่างๆเพื่อให้เกิดประสิทธิภาพที่ดีขึ้น ผู้เขียนทดลองกับการเรียกคืนภาษีโดยเปลี่ยนคำบางคำในจดหมายเรียกคืนภาษีก็ทำให้ผู้ที่ต้องคืนภาษีคืนภาษีไวขึ้นและลดภาระค่าใช้จ่ายในการทวงภาษีของรัฐได้อย่างมาก (แค่เปลี่ยนคำแค่ไม่กี่คำเท่านั้น)
สำหรับผมการอ่านหนังสือเล่มนี้มันช่วยให้ผมตระหนักรู้เกี่ยวกับพฤติกรรมขาดสติ และพยายามกลับมามีสติในการตัดสินใจในบางเรื่อง เช่น เรื่อง “ทุนจม” การใช้คำเพื่อเล่นกับความรู้สึก พยายามใช้เหตุผลมากกว่าความรู้สึกแรกในการตัดสินใจ

ก็สำหรับใครที่อยากหาหนังสือดีๆสักเล่มมาอ่านเพิ่มความรู้ ผมก็แนะนำหนังสือเล่มนี้ให้อ่านเลยครับ รับรองว่าคุ้มกับเงินที่เสียไปแน่นอน

A Little History Of Science - วิทยาศาสตร์ ประวัติศาสตร์ การไขความจริงแห่งสรรพสิ่ง

A Little History Of Science - วิทยาศาสตร์ ประวัติศาสตร์ การไขความจริงแห่งสรรพสิ่ง

A Little History Of Science - วิทยาศาสตร์ ประวัติศาสตร์ การไขความจริงแห่งสรรพสิ่ง

ผมชอบหนังสือในชุด A Little History Of X เมื่อ X คือเรื่องอะไรก็ได้ ผมเคยอ่าน A Little History of Philosophy แล้วเหมือนเปิดโลกเกี่ยวกับปรัชญาในวงกว้างให้เห็น แนวคิดเกี่ยวกับโลกในมุมมองที่ไม่เคยคิดว่ามันมีคนคิดแบบนี้ด้วยเหรอวะ แถมเขาเล่าตั้งแต่อดีตไล่มาให้เห็นการเปลี่ยนแปลงและพัฒนา พอเห็นเรื่องเกี่ยวกับวิทยาศาสตร์ก็เลยสั่งซื้อมา ถึงผมจะเป็นคนที่เรียนสายวิทยาศาสตร์แต่ก็ไม่ค่อยรู้ว่าวิทยาศาสตร์นั้นพัฒนามายังไง เกิดมาก็อยู่ในช่วงที่วิทยาศาสตร์เจริญรุ่งเรืองในระดับที่ไม่เอามาปนกับศาสนาและความเชื่อแล้ว

วิทยาศาสตร์ยุคแรกคือการมองฟ้าและนับ

อันนี้ผมก็ประหลาดใจเหมือนกันว่าทำไมเป็นการมองฟ้ากับการนับ แต่พอมาอ่านก็เข้าใจเพราะสมัยก่อนมนุษย์ยังไม่มีความรู้อะไรเกี่ยวกับวิทยาศาสตร์เลย สิ่งแรกที่มนุษย์เราทำก็เลยเป็นการมองฟ้าดูดวงดาว ดูพระอาทิตย์ พระจันทร์ ดูว่ามันขึ้นลงยังไง จากนั้นก็เริ่มดูว่า 1 วันมันนานเท่าไหร่ จนชาวบาโบโลนสร้างรูปแบบการนับวันและเวลาแบบ 24 ชั่วโมงที่มาที่เราใช้กันในปัจจุบัน รู้ว่า 1 ปีมี 365 วัน โดยใช้การเฝ้ามองว่าพระอาทิตย์กลับมาอยู่ที่ตำแหน่งเดิมนั้นใช้เวลานานเท่าไหร่ พวกเขาเริ่มคำนวณข้างขึ้นข้างแรมได้แม่นยำ เริ่มเอาไปใช้คาดการณ์ฤดูกาลได้ในการปลูกพืชผลได้ จริงๆไม่ได้มีแค่บาบิโลนนะครับ ที่จีน (จีนนี่แม่นมากถึงขนาดรู้ว่า 1 ปีมี 365 1 / 4 วัน) อียิปต์ ก็มีการนับเวลาแบบนี้เหมือนกัน นี่เลยทำให้เห็นว่ามนุษย์ทุกที่ก็พยายามทำในเรื่องคล้ายๆกัน

การแพทย์ในยุคกรีก

การแพทย์ในสมัยเราเป็นอะไรที่แบบดูเป็นเรื่องที่เข้าใจได้ไม่ยาก เรารู้ระบบการย่อยอาหาร ระบบการไหลเวียนโลหิต รู้ว่าเราสามารถติดเชื้อโรคได้จากไวรัส แต่ถ้าลองย้อนกลับไปเมื่อ 2000 - 3000 ปีที่แล้วล่ะ ในยุคที่เราไม่รู้อะไรเกี่ยวกับร่างกาย เราไม่รู้ด้วยซ้ำว่าเราป่วยเพราะอะไร ในสภาพแบบนั้นมันเป็นยังไง หนังสือเล่มนี้เล่าตั้งแต่มนุษย์คิดว่าโรคเกิดจากธาตุในร่างกายไม่เท่ากัน บ้างก็ว่ามาจากเทพ (ซึ่งดูเหมือนคนจะเชื่อว่ามาจากเทพมากกว่า) แต่ก็มีเหล่าคนที่ไม่คิดเหล่านั้น “ฮิปโปคราตีส” ผู้ได้ชือว่าเป็นบิดาของการแพทย์คือหนึ่งในกลุ่มคนเหล่านั้น ฮิปโปคราตีสเชื่อว่าโรคภัยไข้เจ็บทุกชนิดเกิดจากธรรมชาติ ไม่ได้มาจากเทพ โรคอันโด่งดังสมัยก่อนคือโรคลมชัก โรคนี้ถูกมองว่าเป็นอาการของเจ้าประทับทรง ฮิปโปคราตีสบอกว่ามันคือโรคที่เกิดขึ้นตามธรรมชาติซึ่งโคตรจะขัดกับความเชื่อกระแสหลักในสมัยนั้นที่เชื่อว่ามาจากเทพเจ้า อีกทั้งฮิปโปคราตีสยังคิดค้นวิธีการรักษาโรคโดยเขาคิดว่าโรคนั้นเกิดจากการที่ของเหลวในร่างกายไม่สมดุล ซึ่งเขาสังเกตจากการอาการของโรคที่เขาพบเจอเช่น เป็นหวัดมีน้ำมูก มีน้ำเหลืองจากแผล เขาจึงคิดวิธีรักษาโดยการทำให้ของเหลวในร่างกายสมดุล โดยมีวิธีรักษาโดยการดูอาการบ่งบอกโรค และเริ่มรักษา โดยการรักษานั้นเป็นการรักษาแบบติดตามโรคไปเรื่อยๆด้วย ดูว่าดีขึ้นไหมในแต่ละบุคคล (เหมือนหมอปัจจุบันเลย เลยไม่แปลกใจว่าแกเลยเป็นบิดาทางการแพทย์) อีกทั้งแกยังเป็นคนคิดจรรยาบรรณของแพทย์ด้วยว่า จะต้องรักษาผู้ป่วยด้วยสติปัญญาและความสามารถสูงสุด และจะไม่ใช้ความรู้ทางการแพทย์ในการทำร้ายผู้ป่วย

วิทยาศาสตร์คือการเรียนรู้สิ่งที่มีค้นพบไปหมดแล้ว

ดินแดนตะวันตกดินแดนที่เรามักเห็นว่านักวิทยาศาสตร์เก่งๆดังๆจะอยู่แถวนั้น เช่น นิวตัน (ที่หลายคนเกลียดแกเพราะการคิดค้น calculus และกลายมาเป็นข้อสอบยากๆ) จริงๆแล้ววิทยาศาสตร์นั้นถูกแช่แข็งที่ตะวันตกเป็นเวลาหลายร้อยปี ซึ่งเกิดจากเมื่อศาสนาเอาวิทยาศาสตร์มาใช้อธิบายหลายๆเรื่อง ไม่ว่าจะเป็นโลกเป็นศูนย์กลางจักรวาล (คิดค้นโดย ปโตเลมี) และอีกหลายๆเรื่อง พอเอาความรู้ตอนนั้นมาใช้อธิบายศาสนาความรู้เหล่านั้นก็จะถูกจารึกว่ามันคือคำพูดของพระเจ้า พระเจ้าทรงทำแบบนั้นแบบนี้ คราวนี้พอมันเป็นแบบนั้นศาสนาก็พยายามควบคุมไม่ให้วิทยาศาสตร์ออกนอกกรอบ ถึงขนาดมีการพูดเลยว่า “วิทยาศาสตร์คือการเรียนรู้สิ่งที่มีค้นพบไปหมดแล้ว” พูดโดยนักบุญออกัสติน แต่ไม่ใช่ว่าศาสนาจะสร้างแต่เรื่องไม่ดี จริงๆพวกเขาก็ที่ต้องการให้วิทยาศาสตร์นั้นมีแบบแผนเดียวกัน เขาเลยสร้างสิ่งที่เรียกมหาวิทยาลัย (ที่เราเรียนกันในปัจจุบันเนี่ยแหละ) เพื่อให้การศึกษา ความรู้ที่สอนจะได้เหมือนกัน ไม่แปลกประหลาด แล้วก็กลายเป็นรากฐานการศึกษามาจนถึงปัจจุบัน ซึ่งเนื่องจากปัญหาที่ศาสนาเข้ามาควบคุมความรู้ทางวิทยาศาสตร์เหล่านี้ ทุกคนก็เลยเชื่อตามกันไปหมด ขนาด “โคเปอร์นิคัส” ทำการสังเกตและทดลองคำนวณจนสามารถตั้งทฤษฎีที่ว่าโลกไม่ใช่ศูนย์กลางจักรวาลที่ ดาวเคราะห์ ดวงอาทิตย์ พระจันทร์ไม่ได้หมุนรอบโลก แต่สิ่งที่ว่ามาหมุนรอบดวงอาทิตย์ต่างหาก เขาถึงขนาดทำการคำนวณให้เห็นว่า ถ้าเราคำนวณถ้าเปลี่ยนไปให้ทุกอย่างหมุนรอบดวงอาทิตย์เนี่ย การคำนวณจะเป็นเหตุเป็นผลง่ายขึ้น ไม่ต้องทำการเพิ่มการทดรอบการหมุน (เพื่อให้สมการการหมุนรอบโลกนั้นถูกต้อง) แต่สุดท้ายเมื่อมันขัดกับความรู้เก่าที่ศาสนาเอามาบอกแล้วว่าพระเจ้าสร้างโลกให้เป็นศูนย์กลางของทุกสิ่ง ทฤษฏีนี้ก็ถูกต่อต้านและเป็นความคิดที่อันตราย ใครไปเห็นด้วยนี่โอกาสตายสูงกันเลยทีเดียวจนต้องใช้เวลาเกือบ 100 ปี กว่าจะมีนักวิทยาศาสตร์หลายๆคนได้ทำการศึกษาและบอกว่ามันคือความจริง งัดข้อกับศาสนาจนทำให้สุดท้ายแนวคิดว่าโลกและดาวเคราะห์หมุนรอบดวงอาทิตย์จะกลายเป็นแนวคิดหลักแบบในปัจจุบัน

ไม่ใช่แค่เรื่องวิทยาศาสตร์ที่เกี่ยวกับดาราศาสตร์และการคำนวณที่ถูกแช่แข็ง วิทยาศาสตร์การแพทย์ก็ถูกแช่แข็ง พวกเขาใช้ตำราของ เกเลน แพทย์ในสมัยโรมัน ประมาณปี ศ.ศ. 100 กว่าๆมาตลอด พวกเขาเชื่อตามที่เกเลนเขียนไว้ทั้งหมด ไม่ว่าจะสรีรวิทยา การรักษาโรค ยังเชื่อว่า ร่างกายประกอบด้วยธาตุ การรักษาเป็นไปตามที่เกเลนเขียนไว้ จนสุดท้ายเมื่อศาสนายอมให้มีการผ่าศพได้ (นักวิทยาศาสตร์พยายามขอร้องกับญาติ หรือ คนใกล้ตายให้บริจาคร่างกายเพื่อการศึกษาร่างกาย) พวกเขาก็ได้รู้ว่าสิ่งที่เกเลนให้ความรู้ไว้ไม่ถูกต้องทั้งหมด สรีรวิทยาที่เกเลนเขียนไว้นั้นมาจากการผ่าดูลิง หมู แล้วเอามาเทียบกับมนุษย์เพราะสมัยโรมันนั้นการผ่าศพเป็นเรื่องผิดกฏหมาย เมื่อมาเทียบกับความกับการผ่าศพคนจริงๆแล้วเห็นภายในจริงๆทำให้แตกต่างกันอย่างมาก จนสุดท้ายแนวคิดที่ว่า “วิทยาศาสตร์คือการเรียนรู้สิ่งที่มีค้นพบไปหมดแล้ว” ก็ได้ถูกทำให้เห็นว่าไม่จริง นักวิทยาศาสตร์เริ่มกลับมาศึกษาและพิสูจน์ความรู้ในอดีตว่าถูกต้องหรือไม่ มีอะไรใหม่กว่าที่เขียนไว้หรือไม่

สรุป

จริงๆมีอีกหลายเรื่องที่น่าสนใจมากๆที่ผมไม่ได้เล่า (ที่ผมเล่ายังไม่ถึงครึงเล่มเลย) ไม่ว่าจะเป็นการค้นพบทฤษฎีวิวัฒนาการที่สามารถใช้ปฏิเสธการมีอยู่ของพระเจ้าได้ หรือเรื่องการค้นพบแนวคิดที่ว่าโรคนั้นสามารถติดจากภายนอกได้ไม่ใช่เกิดจากร่างกายขาดความสมดุลเพียงอย่างเดียว อีกทั้งจะเห็นว่าความรู้จากวิทยาศาสตร์นำไปพัฒนาเป็นเทคโนโลยีและก็เอาเทคโนโลยีมาพัฒนาวิทยาศาสตร์ต่อ เช่น การใช้ความรู้จากวิทยาศาสตร์เรื่องการใช้กระจกในการขยายภาพ จากนั้นก็เกิดเทคโนโลยีการขยายภาพ พอขยายภาพได้มากขึ้นเราก็ค้นพบจุลินทรีย์ เชื้อโรค และเซลล์ แล้วก็ต่อยอดกันไปเรื่อยๆ

สำหรับใครที่อยากรู้ว่าวิทยาศาสตร์ที่หลายคนคิดว่าทำให้มนุษย์พัฒนาและยึดครองโลกได้ทั้งใบนั้นมีเส้นทางยังไง ต้องผ่านอะไรมาบ้าง มันไม่ใช่เรื่องมหัศจรรย์แต่มันคือความพยายามของเหล่านักวิทยาศาสตร์ คนยุคก่อนหน้าเรา ถ้าคุณจะขอบคุณเทพเจ้า พระเจ้า หรืออะไร ผมว่าคุณลองขอบคุณ ฮิปโปคราตีส เกเลน โคเปอร์นิคัส หรือ ลองเอาวิธีการทางวิทยาศาสตร์มาใช้ในชีวิตประจำวันมากขึ้นดูบ้างก็น่าจะดี

What Caesar Did for My Salad - ตำนานอาหารโลก เบื้องหลังจานโปรดโดนใจคนทั่วโลก

What Caesar Did for My Salad - ตำนานอาหารโลก เบื้องหลังจานโปรดโดนใจคนทั่วโลก

What Caesar Did for My Salad - ตำนานอาหารโลก เบื้องหลังจานโปรดโดนใจคนทั่วโลก

หนังสือเล่มนี้ซื้อมาตอนงานหนังสือ(ออนไลน์) ซึ่งซื้อเพราะมันต้องซื้อให้ถึงราคานึงถึงจะได้โปรลด 15 - 20% จริงๆผมมีหนังสือที่อยากอ่านอยู่แล้วคือพวก Designing your work life หนังสือเล่มนี้เลยกลายเป็นของที่ซื้อไปเพื่อได้ส่วนลด แต่จริงๆก็กะซื้อมาอ่านเพิ่มความรู้เวลาไปคุยกับสาวด้วย คือผมมีปัญหาเกี่ยวกับคุยกับมนุษย์นอกสายคอมพิวเตอร์ คือเราไม่สามารถเอาหัวข้อที่เราทำงานไปคุยกับคนเหล่านั้นได้เลย (แต่ถ้าคุยกับคนสายงานเดียวกันนี่คุยกันเย็นก็คุยได้เรื่อยๆ) ก็เลยต้องหาหนังสือนอกสายมาอ่านบ้าง โอเคเรานอกเรื่องมาเยอะแล้วมาพูดถึงหนังสือเล่มนี้ดีกว่า หนังสือเล่มนี้พูดถึงที่มาของอาหารจานดังๆในโลกว่ามีที่มาอย่างไร ซึ่งต้องบอกว่าเขาใช้คำว่า “ตำนาน” ดังนั้นอาจจะไม่ตรงกับที่คุณได้ยินมาเพราะบางทีเรื่องเล่าที่คุณฟังมาอาจจะเป็นอีกเวอร์ชันนึงซึ่งหนังสือเล่มนี้เล่าแทบทุกเวอร์ชันของที่มาพร้อมบอกว่าอันไหนน่าเชื่อถือที่สุด (ส่วนใหญ่หนังสือให้ค่าความเชื่อถือมาจากการมีบันทึกเป็นตำราอาหาร หรือหนังสือพิมพ์เป็นหลัก)

แฮมเบอร์เกอร์ : อาหารเยอรมันที่กลายเป็นสัญลักษณ์ของอเมริกา

ตอนอ่านครั้งแรกนี่แบบใช่เหรอวะ มั่วรึเปล่าแต่พออ่านเขาก็มีหลักฐานมายืนยันว่ามันมาจากชาวเยอรมันที่อพยพจากเยอรมันมาอเมริกา โดยเดินทางผ่านบริษัท Hamburg America Line เข้ามา โดยชาวเยอรมันเอาอาหารประเภทเนื้อบดกินกับขนมปังมาของบ้านเกิดมาด้วย ซึ่งก็ได้รับความนิยมจากคนอเมริกามาก คราวนี้เนื่องจากไม่รู้ชื่อว่ามันคืออาหารอะไรก็เลยตั้งชื่อว่า Hamburgers (ผู้เดินทางมากับ Hamburg America Line) พอมารู้ที่มาของชื่อแบบนี้แล้วก็แบบเฮ้ยมึงตั้งชื่ออาหารกันง่ายๆแบบนี้เลยเหรอวะ

แซนด์วิช : แซนด์วิชคือชื่อเมือง ส่วนคนที่คิดคือผู้ปกครองเมืองนั้น

ตอนอ่านครั้งแรกแบบใช่เหรอวะ (อีกแล้ว) แต่พอมาอ่านมันมากจากคำว่า sand ที่แปลว่า ทราย wic แปลว่าหมู่บ้าน มันเลยเป็นหมู่บ้านทราย (หมู่บ้านนี้ติดทะเลนะครับ) ซึ่งการที่เป็นหมู่บ้านติดทะเลเลยกลายเป็นเมืองท่าแถมเป็นเมืองท่าที่มีความสำคัญในอังกฤษจนมีการตั้งคนมาปกครองเมืองนี้โดยเฉพาะ ซึ่งคนที่ครองเมืองนี้คนที่ 4 เป็นคนที่เอาแต่เล่นการพนันโดยเฉพาะไพ่เป็นชีวิตจิตใจจนขนาดที่ไม่ไปกินข้าวที่ไหนนั่งเล่นไพ่อย่างเดียว คราวนี้เวลากินอาหารจะใช้มือจับอาหารเข้าปากมือก็เลยเปื้อน พอมือเปื้อนก็เลยทำให้คนรู้ว่าไอหมอนี่มันถือไพ่อะไร หลังจากที่มีการคืนไพ่เข้าสำรับ พอรู้ก็พอเดาได้ว่าไอหมอนี่ชอบเล่นไพ่สไตล์ไหนซึ่งอาจทำให้แพ้ได้ แกก็เลยคิดวิธีกินอาหารโดยมือไม่เปื้อนเลยเอาขนมปังมาใช้จับแทน (แบบนี้ก็ได้เหรอวะ) ซึ่งวิธีการนี้เป็นการแพร่หลายเอามากๆจนเรียกเลยเรียกอาหารแบบนี้ว่า แซนด์วิช ตามชื่อการคนที่ครองเมืองแซนด์วิชที่ติดพนันท่านนั้น

พาย : เป็นชื่อนกที่ชอบสะสมอาหาร

ใช่ครับมันมาจากนกชื่อ แม็กพาย ซึ่งชอบเก็บสะสมอะไรหลายๆอย่าง โดยพายสมัยก่อนนั้นมีไส้ที่หลากหลายเนื้อ ผัก ผลไม้ ต่างๆนาๆใส่รวมเข้าไป โดยเขาจะไม่กินแป้งที่หุ้มพายไว้เขาจะกินเฉพาะไส้ข้างใน ซึ่งแป้งพายในยุคนั้นแข็งมากไม่ได้มีไว้กิน แต่มีไว้เพื่อรักษาอาหารข้างใน ว่าง่ายๆไม่ให้อากาศเข้าไปในไส้ดังนั้นแป้งจึงเป็นการถนอมอาหารไม่ได้มีไว้กิน จนมาถึงในปัจจุบันค่อยๆเปลี่ยนให้แป้งที่หุ้มพายกินได้

ฟิสแอนด์ชิปส์ : ปลาไม่ถือว่าเป็นเนื้อ

ตอนอ่านเรื่องนี้คือแบบได้เหรอวะ คือในอังกฤษมีการรณรงค์ให้ไม่คนในศาสนาคริสต์เนี่ยละเว้นการกินเนื้อสัตว์ (พึ่งรู้ว่าคนคริสต์ก็มีการละเว้นเนื้อสัตว์แบบนี้) แต่ด้วยความอยากกินของคน (แหม่ก็เนื้อมันอร่อยๆอะนะ) ก็เลยไปอ่านพระคัมภีร์ก็พบว่า ปลา ไม่จัดว่าเป็นเนื้อสัตว์ครับ เขาก็เลยปิ้งไอเดียเอาปลามาทอดขายกันในวันศุกร์ ซึ่งก็น่าจะรู้นะครับว่ามันจะขายดีขนาดไหน จนฟิสแอนด์ชิปส์เนี่ยเป็นอาหารยอดนิยมอันดับต้นๆของอังกฤษไปเลย

สรุป

หนังสือเล่มนี้เล่าที่มาของอาหารจานดังๆบนโลกว่ามีที่มาอย่างไร ซึ่งบางเรื่องก็ทำให้ช็อกเอามากๆ อย่างซีซาร์สลัดเนี่ยมันไม่ได้จาก จูเลียต ซีซาร์ แต่มาจากอะไรลองไปซื้อมาอ่านดู แถมเมื่อมันพูดถึงอดีตที่มาของอาหารย่อมมีเกร็ดเล็กเกร็ดน้อยทางประวัติศาสตร์ให้เราได้รู้ด้วย เช่น เฟรนช์ฟรายส์เคยถูกเปลี่ยนชื่อเป็นฟรีด้อมฟรายส์เพราะกระแสต่อต้านฝรั่งเศสในสหรัฐอเมริกา หรือ ราชวงศ์อังกฤษในปัจจุบันเนี่ยจริงๆนั้นแทบจะมาจากเยอรมันเลยทีเดียว ชื่อพระราชวังวินเซอร์เนี่ยสมัยก่อนมันมีชื่อเป็นภาษาเยอรมัน แต่เมื่อเกิดสงครามโลกชาวอังกฤษก็เกิดการต่อต้านอะไรที่เกี่ยวกับเยอรมันด้วย โดยเฉพาะราชวงศ์อังกฤษที่แน่นแฟ้นกับเยอรมันมาก ด้วยเหตุนี้ราชวง์อังกฤษจึงต้องแสดงความจริงใจโดยการทำอะไรหลายๆอย่างเพื่อแสดงว่าตนอยู่ข้างอังกฤษ ซึ่งหนึ่งในการกระทำนั้นคือการเปลี่ยนชื่อพระราชวังด้วย ส่วนข้อเสียในการอ่านหนังสือเล่มนี้คือถ้าคุณไม่รู้จักอาหารที่เขาเล่าให้ฟังเราจะงงเลย แบบผมไม่รู้จักพวก พุดดิ้งปลาไหลงี้ บลัดแมรี่งี้ กราแตงงี้ อ่านแล้วแบบเฮ้ยอาหารอะไรวะ ความอินกับมันก็จะลดลง แต่ถ้าเป็นอาหารที่รู้จักแบบที่ผมเล่าข้างบนเนี่ยเราจะอินกับมันทันที สำหรับใครที่ชอบกินอาหารต่างประเทศ ผมก็แนะนำหนังสือเล่มนี้ให้อ่านเลย เพราะคุณจะรู้สึกอินและสนุกกับที่มาชวนประหลาดใจในหลายๆเรื่อง ส่วนใครที่กะซื้อมาอ่านเพราะคิดว่าจะเกี่ยวกับวิธีการทำอาหารอันนี้ผมก็ไม่แนะนำให้ซื้อมาเลยเพราะไม่มีสูตรอาหารใดๆทั้งสิ้นในหนังสือเล่มนี้

Neurofitness - สมองฟิต-ฟิตสมอง เคล็ดลับเพิ่มศักยภาพและปลุกความคิดสร้างสรรค์

Neurofitness - สมองฟิต-ฟิตสมอง เคล็ดลับเพิ่มศักยภาพและปลุกความคิดสร้างสรรค์

Neurofitness - สมองฟิต-ฟิตสมอง เคล็ดลับเพิ่มศักยภาพและปลุกความคิดสร้างสรรค์

สำหรับหนังสือเล่มนี้ซื้อมาจากงานหนังสือออนไลน์ซื้อเพราะชื่อหนังสือที่มีคำว่า “ฟิตสมอง” ซึ่งน่าสนใจมากว่าเราจะสามารถทำให้สมองเรามีประสิทธิภาพดีตลอดยังไง ซึ่งพอได้อ่านก็รู้สึกว่าไม่ค่อยตรงกับที่คาดหวังเท่าไหร่ แต่กลับสนุกเพราะหนังสือเล่าถึงเรื่องต่างๆของสมองไม่ว่าจะเป็นโรคประหลาดที่ไม่น่ารักษาหาย (ที่บ้านเรามองว่าน่าจะโดนผีเข้า ผีสิง โรคเวรโรคกรรม) แต่สามารถรักษาหายได้โดยผ่าตัดรักษา ให้ยา หรือช็อตไฟฟ้า (คุณอ่านไม่ผิดครับช็อตไฟฟ้า) โดยคุณหมอที่เขียนหนังสือเล่มนี้เขียนให้เข้าใจง่าย (คนแปลก็แปลได้เก่งมาก) โดยทุกเรื่องที่หมอเล่านั้นเป็นกรณีที่หมอเป็นคนรักษาเองด้วย การเล่าจึงเห็นภาพตั้งแต่อาการก่อนการรักษา การวินิจฉัย ตรวจเจอโรคได้ยังไง แล้วจะรักษายังไง ทำให้อ่านแล้วเหมือนนั่งฟังคุณหมอเล่านิทานให้ฟัง ส่วนในเรื่องของการฟิตสมองนั้นในหนังสือก็มีโดยจะพูดถึงการทำให้สมองสุขภาพดี แบบที่มีงานวิจัยยืนยัน อีกทั้งในแต่ละบทมีพูดถึงเรื่องที่คนนอกสายเข้าใจผิดๆ (หรือข้อมูลที่ได้นั้นถูกพิสูจน์ว่าไม่จริง) เช่น คนถนัดซ้ายจะใช้สมองซีกขวามากกว่า คนทำงานศิลป์ สมองซีกหนึ่งจะทำงานหนักกว่า ซึ่งจากงานวิจัยใหม่ๆพบว่าคนเหล่านั้นใช้สมองแทบจะเท่ากันเลย

ตัดสมองออกซีกนึงแล้วไม่ตาย

ตอนอ่านเรื่องนี้ครั้งแรกนี่แบบเฮ้ยได้เหรอวะ โดยเรื่องนี้เป็นกรณีของเด็กหญิงคนหนึ่งที่มีอาการลมชักโดยมีสาเหตุมาจากสัญญาณไฟฟ้าในสมองผิดปกติ ซึ่งในการรักษาเบื้องต้นนั้นคือการใช้ยา แต่เมื่อรักษาไปเรื่อยๆยาเริ่มใช้ไม่ได้ผลจนสุดท้ายทางหมอและญาติจึงตัดสินใจใช้วิธีผ่าตัดสมองซีกขวาซึ่งเป็นที่ปล่อยกระแสไฟฟ้าผิดปกติทิ้งเพื่อรักษาที่ต้นเหตุ อ่านถึงตรงผมตกใจมากว่าคนเราจะเสียสมองซีกนึงไปได้เลยเหรอ แต่คุณหมอก็อธิบายว่าสมองของเรานั้นเก่งมาก คือถ้าเราสูญเสียสมองส่วนที่ควบคุมร่างกายส่วนใดส่วนหนึ่งไป สมองจะพยายามเอาส่วนอื่นที่เหลือมาทำงานแทนส่วนที่เหลือไป ในกณณีของเด็กหญิงคนนี้นั้นต้องผ่าตัดสมองซีกขวาทิ้งทั้งซีกซึ่งนั่นทำให้เด็กหญิงสูญเสียการควบคุมร่างกายซีกซ้ายทั้งซีกไป แต่หลังจากกายภาพบำบัดเป็นเวลา 6 เดือน เด็กหญิงสามารถกลับมาใช้งานร่างกายซีกซ้ายได้เกือบปกติ ซึ่งหมออธิบายว่านี่คือสิ่งที่เรียกว่า “ความยืดหยุ่นของสมอง” แต่ไม่ใช่ว่าเสียสมองไปแล้วจะยืดหยุ่นกลับมาได้ทั้งหมดนะครับ มันมีสมองบางส่วนที่สำคัญมากที่ไม่สามารถเสียได้อยู่เช่นกัน

รักษาด้วยการช็อตไฟฟ้า

อ่านแล้วก็ช็อกเหมือนกันครับสำหรับการรักษาด้วยการช็อตไฟฟ้าเพราะภาพจำของผมนั้น การรักษาด้วยการช็อตไฟฟ้านั้นมันไม่เหมือนการรักษาแต่มันเหมือนการทรมานเสียมากกว่า ซึ่งภาพจำมันมาจากหนังจากละคร โดยกรณีนี้คนไข้เป็นหญิงแก่คนหนึ่งมีอาการไบโพล่าที่รุนแรงมาก ซึ่งพยายามทำการรักษาด้วยยามาตลอดจนสุดท้ายยาไม่สามารถรักษาได้ จนญาติคนไข้ตัดสินใจใช้การรักษาด้วยการช็อตไฟฟ้า ซึ่งคุณหมอได้เล่าวิธีการรักษานั้นแตกต่างจากหนังที่เราได้ดู การช็อตไฟฟ้านั้นนั้นไม่ได้ใช้ไฟรุนแรงแต่อย่างใด แต่เป็นการใช้ไฟที่อ่อนมากๆกระตุ้นไปที่สมอง เพื่อปรับสภาพสัญญาณไฟฟ้าในสมอง ซึ่งการรักษานั้นใช้การช็อตไฟฟ้าหลายรอบมากซึ่งผลของการรักษาสามารถทำให้ไข้กลับมาใช้ชีวิตได้ตามปกติได้ ดังนั้นถ้าหมอด้านสมองแนะนำการรักษาด้วยการช็อตไฟฟ้าก็ขอให้ทำใจกลางๆพิจารณากันด้วยนะครับ

สรุป

หนังสือเล่มนี้เล่าเรื่องเกี่ยวกับสมองในแง่มุมที่เราไม่เคยรู้ ในเล่มยังมีกรณีที่น่าสนใจอีกหลายเรื่องที่ผมไม่ได้เล่า ไม่ว่าจะเป็นการกินแบบคีโตจินิกที่ดังๆนั้น จริงๆคิดค้นขึ้นมาเพื่อรักษาอาการของคนที่เป็นลมชัก การทดลองฝังเครื่องกระตุ้นไฟฟ้าในสมองของวัวกระทิงเพื่อควบคุมให้วัวกระทิงวิ่งวนซ้ายวนขวา หรือแม้กระทั่งให้มันหยุดวิ่งไล่ขวิดคน (หรือที่มาทาดอร์หยุดวัวกระทิงได้นี่อาจจะมาจากการใช้การควบคุมแบบนี้ก็ได้นะ) อีกทั้งยังช่วยทำลายความเชื่อผิดๆ ความเข้าใจผิดๆที่เราเข้าใจเกี่ยวกับสมอง สำหรับใครที่ว่างๆไม่มีอะไรทำ หรืออย่างรู้เกี่ยวกับสมองผมแนะนำหนังสือเล่มนี้เลย แต่ถ้าใครคาดหวังว่าหนังสือเล่มนี้จะบอกวิธีทำให้ฉลาดขึ้น คิดเลขเร็วขึ้น เป็นข้อๆเหมือนหนังสือ How to อันนี้ผมไม่แนะนำให้อ่านครับ

TALK LIKE TED - 9 เคล็ดลับการนำเสนอให้เปี่ยมพลัง ตรึงใจ และสร้างสรรค์

TALK LIKE TED - 9 เคล็ดลับการนำเสนอให้เปี่ยมพลัง ตรึงใจ และสร้างสรรค์

TALK LIKE TED - 9 เคล็ดลับการนำเสนอให้เปี่ยมพลัง ตรึงใจ และสร้างสรรค์

หนังสือเล่มนี้ก็เป็นหนังสืออีกเล่มที่ซื้อมาตอนงานหนังสือออนไลน์ โดยที่ซื้อหนังสือเล่มนี้เพราะอยากรู้ว่าคนที่เขาออกงาน TED เนี่ยเขามีเทคนิคอะไร หลายคนถึงกับยกย่องบูชาว่างาน TED เนี่ยคืองานพูดที่สร้างแรงบันดาลใจที่ดีที่สุดงานนึง ก็เลยลองซื้อมาเผื่อจะประยุกต์ใช้ในการพูดกับมนุษย์ได้ดีขึ้น หรือ เอาไปใช้เวลาอธิบายหลักการและแนวคิดในการเขียนโปรแกรม

หนังสือมีทฤษฏีและการทดลองทางวิทยาศาสตร์รองรับ

หนังสือที่สอนเกี่ยวกับเทคนิคอะไรต่างๆที่เคยอ่านส่วนใหญ่จะบอกว่าทำแล้วดี ทำแล้วแบบนั้นแบบนี้ โดยไม่ได้อ้างอิงกับอะไรเลย แต่สำหรับหนังสือเล่มนี้จะมีการใช้การวัดด้วยวิทยาศาสตร์ ไม่ว่าจะเป็นจับคลื่นสมองของผู้ฟังขณะฟังการนำเสนอของ TED มีการวิเคราะห์คำพูดทุกคำ แยกเป็นประเภทต่างๆ แล้วดูว่าการนำเสนอที่ได้รับความนิยมมากๆเนี่ยมีคำประเภทไหนมากที่สุด ซึ่งทำให้เทคนิค วิธีการที่เขานำเสนอในหนังสือนั้นดูน่าเชื่อถือมากยิ่งขึ้น

คนชอบเรื่องเล่า

บทนึงในหนังสือแนะนำเทคนิคว่าให้นำเสนอเป็นเรื่องเล่า โดยพอเป็นเรื่องเล่าคนที่ฟังจะเปิดใจรับฟังง่ายกว่า แล้วมันฟังง่ายกว่าการเปิดมาพูดด้วยทฤษฏีหรือเนื้อหาหนักๆเลย อีกทั้งบางคนนั้นมีอคติ ถ้าเปิดด้วยเนื้อหาหลักเลยก็จะไม่ยอมเปิดใจ แต่ถ้าเป็นเรื่องเล่าเขาสามารถคิดตามได้ และคิดได้ว่ามันสามารถเกิดขึ้นจริงได้ พอเล่าจบแล้วพาเข้าเนื้อเรื่องหลัก อคติที่มีอยู่ก็อาจจะลดลงจนยอมเปิดใจฟัง ซึ่งพอเราไปดู Video ของ TED ส่วนใหญ่ก็เป็นเรื่องเล่า ของไทยก็มีนะครับที่ผมเคยดูก็อันนี้

ฟังไปก็เพลินๆแถมสร้างแรงบันดาลใจได้ดี ถึงผมจะไม่ค่อยอินเท่าไหร่เพราะผมมองว่าเขาทำเพราะอยากสนองความต้องการตัวเองก็เท่านั้น แต่ก็ถือเป็นการเปิดมุมมองแนวคิดใหม่ๆ ซึ่งถ้าเขาไม่ได้เล่าเรื่องเล่าของเขาแล้วเปิดหัวมายัดเรื่องแบบนั้นล่ะก็ บอกเลยว่าผมไม่น่าจะฟังจนจบ โดยในหนังสือมีการบอกว่าเราสามารถหาเรื่องเล่าต่างๆมาเล่าประกอบการนำเสนอได้ โดยเรื่องเล่าที่เขาแนะนำมี 3 ประเภทคือ

  1. เรื่องราวส่วนตัว ก็ง่ายๆครับ มันคือเรื่องราวของตัวคุณเองซึ่งตัวอย่างก็เรื่องราวของ ดร.กฤตินี พงษ์ธนเลิศ ที่เล่าเรื่องของตัวเอง

  2. เรื่องของคนอื่น ก็ง่ายๆครับ เรื่องเล่าต่างๆของคนอื่น ไม่ว่าจะคนดัง หรือ อะไรก็ตาม

  3. เรื่องเล่าของแบรนค์ดังๆ อันนี้ก็ยกตัวอย่างเช่น Google ที่เริ่มจากบริษัทเล็กๆ วิศวกรไม่กี่คน แต่สามารถพัฒนาขึ้นมายิ่งใหญ่ในปัจจุบัน

ให้ข้อมูลใหม่กับผู้ฟัง

นี่เป็นอีกเทคนิคที่หนังสือแนะนำคือ ให้ข้อมูลใหม่ที่ผู้ฟังไม่เคยรู้ เช่น “ถ้าสหรัฐเปลี่ยนเงินที่ใช้ในการสำรวจอวกาศ 1 ปีไปใช้สำรวจมหาสมุทรนั้นจะสามารถใช้สำรวจได้นานถึง 1600 ปี” ซึ่งการทำแบบนี้นั้นทำให้ผู้ฟังเกิดความสนใจและตื่นเต้น โดยหนังสือก็ได้อธิบายให้ฟังถึงวิธีนี้ในหลายๆรูปแบบ

เวลาที่ใช้ในการนำเสนอ

หนังสือบอกว่าเวลาที่ใช้ในการนำเสนอของ TED คือไม่เกิน 18 นาที ซึ่งจากสถิติของ TED พบว่า Video ที่ผู้ชมใช้ความสนใจเยอะจะอยู่ในช่วงเวลาดังกล่าว อีกทั้งมีการศึกษาเรื่อง “การคั่งค้างในการรู้คิด” ของคน กล่าวคือถ้าคุณต้องฟังอะไรนานๆสมองจะค่อยๆล้าจนสุดท้ายจะไม่ค่อยสนใจ ซึ่งก็ไม่แปลกใจที่เวลาเรียน 3 ชั่วโมงตอนอุดมศึกษามันทรมานแถมไม่ค่อยรู้เรื่องเท่ากับเรียนตอนมัธยมที่เรียนแค่ 50 นาที โดยหนังสือแนะนำว่าควรนำเสนอแค่ 18 นาที ถ้านานกว่านั้นก็ควรหยุดจากเรื่องหลักไปเล่าเรื่องอื่น อาจจะเป็นเรื่องเล่า มุกตลก เพื่อให้สมองผู้ฟังได้พัก จากนั้นค่อยกลับเข้าเนื้อหาหลัก ซึ่งพอมาคิดดูแล้วอาจารย์ที่สอนตอนเรียนมหาวิทยาลัยท่านนึงก็ใช้เทคนิคประมาณนี้ อาจารย์จะสอนทฤษฎีพอจบเรื่องนั้น แกก็จะตัดไปเล่าเรื่องอื่น เช่น ตอนไปเรียนต่างประเทศแรกๆ นั่งเครื่องบินแล้วเจอคนนั่งข้างเป็นใคร อะไรประมาณนี้ แล้วค่อยกลับไปสอนต่อ ซึ่งน่าแปลกใจที่เราเข้าใจวิชานี้มากกว่าวิชาที่สอนลากยาว 3 ชั่วโมง

สรุป

ในหนังสือมีอีกหลายเทคนิคสำหรับการนำเสนอ ซึ่งถ้าใครอยากได้เทคนิคที่ทำให้การนำเสนอให้ไม่น่าเบื่อน่าจดจำอีกทั้งมีทฤษฎีรองรับ ก็ลองไปหาซื้อมาอ่านกันดูครับ

History Of Reading - โลกในมือนักอ่าน

History Of Reading - โลกในมือนักอ่าน

History Of Reading - โลกในมือนักอ่าน

History Of Reading โลกในมือนักอ่าน หนังสือเล่มนี้ซื้อมาตอนงานหนังสือคือปกติผมอ่านหนังสือแนวประวัติของเรื่องต่างๆ พอเห็นเรื่องประวัติศาสตร์เกี่ยวกับเรื่องการอ่านก็เลยกดซื้อมา พอได้หนังสือมานี่ช็อกเลย เพราะหนังสือเล่มหนาในระดับหนึ่งเลยแถมหนักใช่เล่นด้วยพกใส่กระเป๋าทีก็หนักโคตร ซึ่งด้วยเหตุผลนี้เลยทำให้ใช้เวลานานมากกว่าจะอ่านจบ

การอ่านในหลายๆแง่มุม

หนังสือเล่มนี้พูดถึงการอ่านหนังสือในหลายๆแง่มุม ตั้งแต่การอ่านนั้นต้องอ่านออกเสียงเท่านั้น การอ่านเป็นพิธีกรรม การอ่านให้คนอื่นฟัง เทคโนโลยีในการเขียนหนังสือ การทำหนังสือ การใช้หนังสือ การขโมยหนังสือ และอื่นๆอีกมากมาย ซึ่งหลายเรื่องนี่เล่นเอาประหลาดใจมากๆ

การอ่านต้องออกเสียง

ในยุคกลางยุคที่หนังสือยังมีไม่มาก คนที่อ่านหนังสือออกนั้นมีจำนวนน้อยมาก และหนังสือส่วนใหญ่นั้นก็เป็นหนังสือเกี่ยวกับศาสนา ไม่ว่าจะหลักศาสนา การวางตัว ดังนั้นจึงเป็นหน้าที่ของผู้อ่านที่ต้องอ่านออกเสียง ให้ผู้ที่อ่านไม่ออกฟัง และเป็นการสอนศาสนาให้กับคนทั่วไปด้วย และด้วยอีกเหตุผลนึงที่ต้องอ่านออกเสียงด้วยเนี่ยเพราะสมัยก่อนการเขียนหนังสือนั้นไม่ได้มีการกำหนดการเว้นวรรค สัญลักษณ์ ต่างๆนาๆ ดังนั้นการสอนการอ่านหนังสือต้องอ่านออกเสียงให้ฟัง รู้ว่าข้อความในหน้านี้ต้องอ่านจากไหนถึงไหน แล้วเว้นวรรคตรงไหน คนที่เรียนก็ต้องจดจำทั้งวิธีการอ่าน และจำด้วยว่าหนังสือเล่มนี้หน้านี้ต้องอ่านยังไง เว้นวรรคตรงไหน ซึ่งพออ่านถึงตรงนี้ก็รู้สึกเฮ้ย จริงดิ แต่พอคิดตามก็เออ น่าจะเป็นไปได้เพราะ สมัยก่อนต่างคนต่างเขียนไม่ได้มีมาตรฐานกลาง ดังนั้นก็ไม่แปลกที่อะไรแบบนี้จะเกิดขึ้น

เมื่ออ่านภาพ

ในยุคกลาง (อีกแล้ว) จากที่ได้เล่าไปในหัวข้อที่แล้วว่าในตอนนั้นคนอ่านหนังสือออกนั้นมีน้อย ทางศาสนจักรเองก็ต้องการเผยแผ่ศาสนาให้กับเหล่าคนที่อ่านหนังสือไม่ออก หรือ อาจจะเป็นเหล่าพ่อค้าที่ต้องการขายหนังสือให้กับลูกค้ากลุ่มที่อ่านหนังสือไม่ออก จึงเกิดการทำหนังสือที่มีภาพประกอบขึ้นมา โดยภาพเหล่านั้นจะวาดเกี่ยวกับเหตุการณ์ต่างๆในศาสนาไม่ว่าจะเป็นวันเกิด วันถูกตรึงไม้กางเขน หรือ ภาพ ที่เกี่ยวกับหลักคำสอนต่างๆในพระคัมภีร์ ซึ่งด้วยการอ่านภาพนี้ก็ทำให้เชื่อมโยงกับหลักคำสอนที่ได้ฟังจากบาทหลวง จากคนอื่นที่ไปฟังมา หรือตีความจากภาพเอาเอง ซึ่งด้วยวิธีนี้ทำให้ชาวคริสต์สามารถเข้าถึงหลักคำสอนได้มากขึ้น แต่ก็ใช่ว่าจะเป็นเรื่องดี เนื่องจากมันเป็นภาพ ดังนั้นความเข้าใจต่างๆมาจากการตีความผ่านภาพ (ถึงตัวหนังสือจะต้องตีความเหมือนกันแต่กรอบในการตีความมันแคบกว่า) ซึ่งแต่ล่ะคนมองภาพอาจจะตีความไม่เหมือนกัน ลองนึกถึงการมองภาพที่แผงนัยยะบางอย่างไว้ ซึ่งผู้วาดต้องการจะบอกอย่างหนึ่ง แต่คนดูภาพอาจไม่เข้าใจเหมือนกัน (เดี๋ยวผมลองยกตัวอย่างภาพภาพนึงด้านล่าง ผมก็ไม่รู้ว่ามันตีความว่าอะไร) ซึ่งนั่นทำให้เกิดการเข้าใจไม่เหมือนกัน ซึ่งอาจลุกลามกลายเป็นปัญหาต่างๆนาตามมา

ภาพจากหนังสือในเรื่อง ninth gate (เป็นเรื่องแต่ง ไม่ใช่เรื่องจริง แต่มันเข้ากับเรื่องการตีความดีเลยเอาทำให้เห็น)

เมื่อฟังคนอื่นอ่าน

เรื่องนี้ตอนอ่านครั้งแรกผมแทบไม่เชื่อว่ามีอยู่จริง แต่มันมีจริงๆที่คิวบา คือในยุคปี ศ.ศ. 1860 คิวบาเป็นประเทศขึ้นชื่อเรื่องการทำซิกการ์ แต่การทำซิกการ์นั้นเป็นงานที่น่าเบื่อมากๆ ดังนั้นจะหาอะไรมาให้เหล่าคนงานอยู่กับงานที่โคตรน่าเบื่อนี้ได้ ประจวบเหมาะกับตอนนั้นเหล่าคนก้าวหน้ามีความคิดที่ว่าจะทำให้ยังไงให้เหล่าคนงาน และผู้อ่านหนังสือไม่ออกนั้นสามารถเข้าถึงความรู้ในหนังสือหรือหนังสือพิมพ์ได้ ด้วยเหตุนี้จึงเกิดการจ้างนักอ่าน มาอ่านหนังสือให้คนงานฟัง ซึ่งผลตอบรับนั้นดีมาก เหล่าคนงานบางที่ถึงกับยอมรวมเงินจ้างนักอ่านมาอ่านหนังสือให้ฟัง บางกลุ่มถึงขนาดซื้อหนังสือ หรือ จดรายการที่ต้องการฟังให้กับนักอ่าน จนสุดท้ายโรงงานผลิตซิกการ์มากมายต้องจ้างนักอ่านเหล่านี้มาประจำที่โรงงานกันเลยทีเดียว (ประมาณว่าถ้าไม่มีพนักงานจะย้ายออก หนีไปโรงงานอื่น) ซึ่งด้วยเหตุนี้จึงการรูปแบบการอ่านให้ฟังและฟังคนอื่นอ่านที่โดดเด่นที่สุดบนโลกขึ้นมา แต่จริงๆแล้วการฟังคนอื่นอ่านหรืออ่านให้คนอื่นฟังนั้นก็มีมานานแล้ว ตั้งแต่ยุคกลาง (อีกแล้ว) ที่วณิพก กวี จะร่อนเร่ เปิดการแสดงแลกเงิน ซึ่งการแสดงของ วณิพก และ กวี ก็คือการอ่านงานเขียนของตนให้คนอื่นฟังนั่นเอง

จริงๆผมรู้จักการเรื่องฟังคนอื่นอ่านและอ่านให้คนอื่นฟังที่คิวบาผ่านละครเวทีเรื่องนึง แต่ตอนนั้นคิดว่ามันเป็นเรื่องแต่งที่มีการจ้างนักอ่านมาอ่านหนังสือให้คนอื่นฟัง แต่พอมาอ่านหนังสือเล่มนี้แล้วแบบ เฮ้ย แม่งเรื่องจริงนี่หว่า

อำนาจของหนังสือ

เนื่องด้วยในสมัยก่อนหนังสือเป็นสิ่งที่หาได้ยาก ราคาแพง ว่าง่ายๆมันกลายเป็นตัวบ่งบอกฐานะได้เลย คนรวยเท่านั้นจึงจะสามารถมีหนังสือไว้ครอบครอง ต่อมาเมื่อหนังสือเริ่มผลิตง่ายขึ้น ถูกลง หนังสือเลยกลายเป็นตัวบอกอย่างอื่นเช่น บ่งบอกการเป็นผู้ใฝ่รู้ บ่งบอกรสนิยม บางทีถึงกับบ่งบอกแนวคิดด้านการเมือง ซึ่งเราก็เห็นได้ในปัจจุบันที่คนจำนวนหนึ่ง เวลาดูรายการเปิดบ้านคนดังหรือสัมภาษณ์คนดัง มักจะมองหาชั้นหนังสือหรือคนดังเหล่านั้นเองก็ชอบจะให้สัมภาษณ์ให้เห็นชั้นหนังสือด้านหลัง แล้วคนเหล่านั้นก็มาวี๊ดว้ายวัดค่าคนดังเหล่านั้นเป็นคนแบบไหนผ่านหนังสือที่เขาแสดงให้เห็น ซึ่งมันไม่เป็นเรื่องใหม่อะไรหรอกครับแต่อยากให้รู้ว่า การทำแบบนี้มันปลอมกันได้ในยุคปี ศ.ศ. 1900 กว่าๆเนี่ยมีการทำปกหนังสือปลอมขายให้บ้านเหล่าผู้ดี หรือ ขุนนางมากมาย เพื่อเอาไปใส่ในชั้นหนังสือ เพื่อสร้างภาพให้ตัวเองเป็นคนมีความรู้ หรือแสดงรสนิยมของตนเองให้ตรงกับผู้มีอำนาจในขณะนั้นเช่น กษัตริย์ ผู้ครองเมือง เป็นต้น ดังนั้นครั้งต่อไปที่คุณเห็นการหนังสือในชั้นวางหนังสือของคนดัง ก็พึงระลึกไว้เถิดว่ามันอาจจะเป็นการจัดฉากเหมือนที่เหล่าผู้ดี หรือ ขุนนางในยุคปี ศ.ศ. 1900 ทำกัน

สรุป

หนังสือ History Of Reading - โลกในมือนักอ่าน นั้นได้บอกเล่าเรื่องในแง่มุมต่างๆของการอ่าน และการเขียน ไว้มากมาย บางมุมมองเราก็ไม่รู้ว่ามันมีอยู่ด้วย โดยที่ผมเล่าไปนั้นเป็นแค่ส่วนน้อยมากๆ ในเล่มมีพูดถึง การแปลหนังสือจากภาษานึงเป็นอีกภาษานึงนั้นมันมีผลกระทบมากมาย หรือจะเป็นเรื่องของหนังสือนั้นเป็นภัยต่อการปกครองถึงขนาดออกกฏหมายจำกัดการอ่าน หรือ การปฏิวัติรูปแบบการผลิตหนังสือจนทำให้หนังสือเป็นที่แพร่หลาย ความบ้าคลั่งในการอยากครอบครองหนังสือ การเขียนหนังสือเพื่ออ่านเอง และอื่นๆ ซึ่งด้วยจำนวนหน้าหนังสือถึง 500 กว่าหน้า (จริงๆมากกว่านี้แต่เป็นบรรณานุกรม) กับราคา 495 บาทก็ถือว่าคุ้มมากมายทีเดียว สำหรับใครที่อ่านหนังสือฆ่าเวลา เก็บเรื่องน่าสนใจไว้เป็นความรู้ ไว้คุยกับเพื่อน ผมก็ขอแนะนำเลย ส่วนข้อเสียของหนังสือเล่มนี้คือมักจะอ้างถึงหนังสือต่างประเทศ ซึ่งผมเองก็ไม่รู้ว่ามันคือเรื่องอะไร บางทีเป็นหนังสือเยอรมันแล้วอ้างมันขึ้นมาแบบไม่มีการปูพื้นมาก่อนว่ามันคือหนังสืออะไร ซึ่งอ่านแล้วเราก็จะงงๆ ต้องไป Search หาว่ามันคือหนังสืออะไร พูดถึงอะไร

ปล. สำหรับใครจะเอาหนังสือเล่มนี้ไปรองขาโต๊ะก็แนะนำนะครับ เล่มหนาดี ราคาประมาณ 495 บาท ก็ถือเป็นที่รองขาโต๊ะชั้นดี แถมทำให้คุณดูดีมีรสนิยมไปอีกแบบเหมือนยุคปี ค.ศ. 1900 เลย

Rest api คืออะไร

Rest api คืออะไร

ตอนทำงานใหม่เมื่อประมาณปี ค.ศ. 2015 (พ.ศ. 2558) ช่วงนั้นนี่วงการ IT นี่กำลังบูมมากมีเรื่อง Start up ที่กำลังมาใหม่ๆ ใครๆก็พากันไปสนใจ ตอนผมกำลังจะจบมีบริษัทใหญ่ๆออกมาหาคนเข้าไปทำงานบริษัท Start up มากมาย หนึ่งในสิ่งที่เขาเอามาขายคือบริษัทเราทำ Rest api ซึ่งก็สร้างความสงสัยให้ผมว่าไอ้ Rest api เนี่ยมันคืออะไรวะ ทำไมต้องเอามาเป็นจุดขายเรียกเด็กจบใหม่ไปทำงานด้วย พอเริ่มทำงานก็เริ่มทำการค้นหาว่าไอ Rest api มันคืออะไร มันดีตรงไหน ซึ่งก็โชคร้ายที่บริษัทที่ผมไปทำงานไม่ได้พัฒนา Application ที่เป็นแบบ Rest api ทำให้ไม่มีคนสอน คราวนี้พอเราไปหาอ่านว่า Rest api ก็เจอข้อมูลมากมาย ซึ่งพาเราหลงไปกับการ Implementation แบบ Search “Rest api” ก็ไปเจอแต่วิธี Implement จนเราหลงว่ามันคือการทำอะไรแบบนั้น จนไป Search เจอคนที่พูดแค่เรื่องทฤษฎีอย่างเดียว

สำหรับใครอยากอ่าน best pracetice สามารถอ่านได้ที่ : https://blog.2my.xyz/2021/01/24/guidelines-best-practices-for-design-restful-api/ เขาเขียนได้ละเอียดมาก แต่สำหรับใครอยากอ่านแบบบ้านๆเล่าเรื่องไปเรื่อยๆก็อ่านต่อด้านล่าง

ยกตัวอย่างเขียน App ร้านขายของ

ถ้าเราเขียน Web application แบบธรรมดาเราจะใช้ HTTP POST ยิง JSON ไป Backend โดย URL ที่ยิงไปก็แล้วแต่จะตั้งกัน

Create Product

URL ( Http method : POST )

1
/api/create-product

Request Body

1
2
3
4
{
productName : string,
price : double
}

Response Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

Update Product

URL ( Http method : POST )

1
/api/update-product

Request Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

Response Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

Delete product

URL ( Http method : POST )

1
/api/delete-product/{productId}

Response Body

ไม่มี Body ดูแค่ http status ว่าเป็นตระกูล 200 ไหม

Get product

URL ( Http method : POST )

1
/api/get-product

Request Body

1
2
3
{
productId : string
}

Response Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

Search Product

URL ( Http method : POST )

1
/api/search-product

Request boyd

1
2
3
4
5
{
productName : string,
startPrice : double,
stopPrice : double
}

Response Body

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
productId : string,
productName : string,
price : double
},
{
productId : string,
productName : string,
price : double
},
....
]

เรา Design API ของ Web application เท่านี้ก็สามารถทำงานได้แล้วใช่มะ คราวนี้มันก็ไม่มีแนวทางอะไรกันใช่ไหมครับ ใครอยากจะตั้งชื่ออะไรแบบไหนก็ได้

แต่คราวนี้ถ้าเราสังเกตุดีๆจะเห็นว่า

  1. ส่วนใหญ่ที่เราสั่งงานกับ Backend เนี่ยส่วนใหญ่มันเป็น Resource นะ ถ้าดูจากตัวอย่างเรายุ่งกับ Product ซึ่งเป็น Resource ของระบบ
  2. ตัว Http method เนี่ยมันมีอย่างอื่นนอกจาก POST เช่น GET , PUT , PATCH , DELETE ซึ่งจะเห็นว่ามี GET, DELETE อยู่แล้ว ถ้าหา Http method มาให้ CREATE, UPDATE, SEARCH ได้ก็จะครบทุกการกระทำที่ต้องการ

เฮ้ยงั้นเราทำ URL ที่สื่อถึง Resource และจะทำอะไรเกี่ยวกับ Resource ก็ให้ดูผ่าน Http Method ด้วยแนวคิดนี้จึงเกิดตัวแนวทางการสร้าง API แบบใหม่

เปลี่ยนไปเป็น API แบบ REST API

Rest api ก็แค่เพียงการกำหนด API รูปแบบหนึ่งเท่านั้นเอง เรามาลองดูว่าถ้าเปลี่ยนจากแบบเก่าเป็น Rest API จะมีอะไรเปลี่ยนแปลงไปบ้าง

Create Product

URL ( Http method : POST )

1
/api/products

Request Body

1
2
3
4
{
productName : string,
price : double
}

Response Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

ในส่วนของ Create product จะมีการเปลี่ยนแปลงตรงส่วน URL ให้เป็น /api/products จะเห็นว่าตัว url จะสื่อว่าตอนนี้เรากำลังจะจัดการกับ product

Update Product

URL ( Http method : PUT )

1
/api/products/{productId}

Url path

productId คือ productId ของ Product

Request Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

Response Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

สิ่งที่เปลี่ยนไปของการ Update product คือ

  1. เปลี่ยน URL ไปเป็น /api/products/{productId}
  2. เปลี่ยนไปใช้ Http method : PUT

จะเห็นว่ามีการกำหนดว่าใน url จะต้องมี productId อยู่ใน url ด้วย ซึ่งพอมาอ่าน url เราจะเข้าใจว่าเรากำลังจะทำอะไรเกี่ยวกับ product ที่มี productId นี้อยู่ ซึ่งถ้าเราบอกว่า PUT คือการ Update ก็จะกลายเป็น เราทำการ Update product ที่มี productId นี้อยู่

Delete product

URL ( Http method : DELETE )

1
/api/products/{productId}

Url path

productId คือ productId ของ Product

Response Body

ไม่มี Body ดูแค่ http status ว่าเป็นตระกูล 200 ไหม

สิ่งที่เปลี่ยนไปของการ Delete product คือ

  1. เปลี่ยน URL ไปเป็น /api/products/{productId}
  2. เปลี่ยนไปใช้ Http method : DELETE

อันนี้ก็จะคล้ายๆกับ Update เลยแต่เปลี่ยนเป็น Http method เป็น DELETE พอเราอ่าน Http method และ URL เราก็จะเข้าใจว่ามันคือ การ Delete product ที่มี productId นี้

Get product

URL ( Http method : GET )

1
/api/products/{productId}

Url path

productId คือ productId ของ Product

Response Body

1
2
3
4
5
{
productId : string,
productName : string,
price : double
}

สิ่งที่เปลี่ยนไปของการ Get product คือเปลี่ยนไปใช้ Http method : GET พอเราอ่านโดยใช้ URL และ Http method รวมกันจะได้เป็น ต้องการดึงข้อมูล Product ที่มี productId นี้

Search Product

URL ( Http method : GET )

1
/api/products/{productId}?productName={productNameValue}&startPrice={startPriceValue}&stopPrice={stopPriceValue}

Url path

productId คือ productId ของ Product

Query parameter

  • productName = ใช้ Search หา Product ที่มีค่าตรงกับที่ส่งไป
  • startPriceValue = ใช้ Search หาค่า Product ที่มีค่า price มากกว่าหรือเท่ากับค่าที่ส่งไป
  • stopPriceValue = ใช้ Search หาค่า Product ที่มีค่า price น้อยกว่าหรือเท่ากับค่าที่ส่งไป

Response Body

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
productId : string,
productName : string,
price : double
},
{
productId : string,
productName : string,
price : double
},
....
]

สิ่งที่เปลี่ยนไปของการ Search Product คือเปลี่ยนไปใช้ Http method : GET และย้ายพวกเงื่อนไขการค้นหาที่อยู่ใน Request body ย้ายไปอยู่ที่ Query parameter แทน ซึ่งพอเราอ่าน url, http method, query parameter ก็จะเข้าใจว่าต้องการ search ข้อมูล product ตามเงื่อนไขใน Query parameter

มันก็แค่เปลี่ยนรูปแบบการกำหนด API

ใช่ครับทั้งหมดที่กล่าวมาของ Rest api นั้นก็แค่การเปลี่ยนรูปแบบการกำหนด api เท่านั้นเอง โดยผมทำตารางการเปลี่ยนแปลงไว้ด้านล่างลองไปดูครับ

Http method

Action Old style api Rest api
Get Resource POST GET
Create Resource POST POST
Update Resource POST PUT
Delete Resource POST DELETE
Search Resource POST GET

URL

Action Old style api Rest api
Get Resource /api/get-product /api/products/{productId}
Create Resource /api/create-product /api/products
Update Resource /api/update-product /api/products/{productId}
Delete Resource /api/delete-product /api/products/{productId}
Search Resource /api/search-product /api/products?query-parameter={value}

ถ้าถามว่าการเปลี่ยนตรงนี้มีผลกระทบอะไรกับ Code ที่เขียนไปแล้วบ้างก็ต้องบอกว่ากระทบแหละครับ แต่มันก็ไม่ได้กระทบอะไรมากเลยมันแค่กระทบตรงส่วนที่เป็น Code ตรง Controller เท่านั้นเอง

ตัวอย่าง Code เปรียบเทียบระหว่าง Get แบบ Old และ Rest Api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Controller
public class ProductController {

@Autowired
private ProductService productService;

// REST API STYLE

@GetMapping("/api/products/{productId}")
private @ResponseBody ProductDto restStyleGet(
@RequestPart("productId") String productId
) {
return productService.getProduct(productId);
}

// OLD API STYLE

@PostMapping("/api/get-product")
private @ResponseBody ProductDto oldStyleGet(
@RequestBody GetProductDto request
) {
return productService.getProduct(request.getProductId());
}

}

ซึ่งจะเห็นว่ามีความแตกต่างอยู่ตรงวิธีที่จะเอาค่า productId มาใช้กับ productService โดยแบบ Rest api นั้นจะเอาค่าจาก URL path ส่วนแบบเก่านั้นจะใช้จาก Request body แทน

สรุป

Rest api ก็คือรูปแบบการกำหนด API รูปแบบหนึ่งโดยจะใช้ Http method และ URL มาใช้ประโยชน์ ซึ่งประโยชน์อย่างหนึ่งที่ผมเห็นจากการลองทำ Rest api คือมันทำให้เข้าใจว่าเรากำลังจะทำอะไรกับ Resource นี้ผ่าน URL ที่เราได้อ่านเลย ข้อดีอีกข้อของการทำ Rest Api คือความสามารถเรื่องทำการ Cache data ผ่าน Http method GET ส่วนมันมหัศจรรย์อะไรไหม โดยส่วนตัวผมไม่เห็นว่ามันจะมหัศจรรย์หรืออะไรขนาดที่จะมาโม้ว่าใช้ Rest api แล้วเราต้องว้าวตามอะไรขนาดนั้น ส่วนมันมีผลกับการเขียนโปรแกรมไหมนั้นในส่วนนี้นั้นไม่แตกต่างกันมากครับ แค่เปลี่ยน Code ตรงส่วน Controller ให้ไปเอาค่าจากพวก Url path, query parameter แทนการดึงค่าจาก Request body

ถ้าใครอ่านมาถึงตรงนี้อาจจะไม่ค่อยว้าวกับ Rest api เหมือนที่ผมไม่ว้าวแหละครับ แต่ๆๆๆๆ จริงๆที่ผมอธิบายมาเนี่ยมันเป็น Rest api ระดับ 2 มันมี Rest api ที่สูงกว่านี้ ซึ่งพออ่านแล้วรู้สึกว่าน่าสนใจมากมันเรียกว่า HATEOAS ถ้าอยากอ่านแบบจริงจังผมแนะนำ Link นี้อธิบายเข้าใจง่ายมาก แถมตลกด้วย

"สิ่งที่ผมเขียนขึ้นเป็นเพียงความรู้และความเข้าใจของบุคคลเพียงบุคคลเดียว ดังนั้นอย่าเพิ่งเชื่อในสิ่งที่ผมเขียนและอธิบาย ลองทำความเข้าใจว่ามันเป็นจริงอย่างนั้นไหมและลองหาแหล่งอ้างอิงอื่นๆว่าเขามีแนวคิดอย่างไร เรื่องการ Design และวิธีการใช้งานไม่มีถูกไม่มีผิดมีแต่เหมาะสมกับงานนั้นไหม"

Ref

เพลงประกอบการเขียน Blog

เสียงพี่แอนนี่เหมาะกับเพลงเศร้าจริงๆ

น้องหมูปิ้ง

น้องหมูปิ้งน่ารักมากๆไปติดตามน้องกันได้ที่ : https://www.facebook.com/donteatthismooping/

น้องหมูปิ้งกินไม่ได้นะ

Spring Jpa connect multiple database at runtime

Spring Jpa connect multiple database at runtime

สำหรับตอนนี้เราจะมาทดลองทำอะไรแปลกๆที่ไม่ค่อยมี Usecase ใช้งานจริงมากเท่าไหร่ แต่ก็เป็น Case ที่น่าสนใจที่พอลองทำก็สนุกดี เลยลองเอามาเขียนตอนนี้เผื่อว่าจะมีคนเจอ Case ที่ต้องใช้จะได้ไปใช้บ้าง ซึ่งกรณีที่ว่าคือการต่อหลาย Database ในขณะ Runtime นั่นเอง

มาดูว่าเราจะทำอะไรกัน

จากภาพเราจะทำการต่อ Database 2 จำพวกหลักๆ โดยพวกแรกผมเรียกว่าพวก DatabaseInfo shcema ตัว จำพวกนี้จะมี database แค่ลูกเดียว อันนี้ไม่มีปัญหาอะไร ปัญหาอยู่ที่จำพวกที่ 2 ซึ่งก็คือ Account อันนี้ผมสร้างสถานการณ์ว่าเราแยก Account ของสาขา Thailand และ สาขา England เป็นคนละ Database แต่ทั้งสอง Database มี Schema เหมือนกัน ตอนนี้อาจจะมีคำถามแล้วว่า ทำไมต้องทำแบบนี้ คือในกรณีจริงๆอาจจะมีก็ได้คือ เราทำการแยก database ตามลูกค้าแต่ล่ะเจ้าออกจากกันเพื่อให้ข้อมูลแยกจากกันโดยสมบูรณ์ ประมาณนั้น คราวนี้มีประเด็นคือเราอยากเขียน App บางตัวไปจัดากร Database เหล่านั้น เช่น Summary data หรือไป Get ข้อมูล และด้วยอยากแยกมันจาก core หลัก App ว่าง่ายๆเราไม่อยากแตะ Code เก่า เราอยากเริ่มใหม่จะได้ใช้เทคโนโลยีที่ทันสมัย แต่จริงๆวิธีนี้ก็ไม่ค่อยถูกต้องเพราะคุณไปจิ้ม Database คนอื่นตรงๆ แต่วิธีนี้ก็ง่ายสุดในการ Maintainace แล้ว

มาดู Code กัน

สำหรับใครอยากดู Code เต็มไปดูได้ที่ https://github.com/surapong-taotiamton/multiple-db-at-runtime ตรงส่วน application.properties ที่เป็นรายละเอียดเกี่ยวกับค่า database ก็เปลี่ยนให้ถูกต้องตามที่ต้องการ

sql สำหรับสร้าง Table : DatabaseInfo สำหรับ database : center

1
2
3
4
5
6
7
8
9
10

CREATE TABLE `DatabaseInfo` (
`databaseInfoId` varchar(255) NOT NULL,
`host` varchar(255) DEFAULT NULL,
`password` varchar(255) DEFAULT NULL,
`port` int(11) DEFAULT NULL,
`username` varchar(255) DEFAULT NULL,
`name` varchar(255) DEFAULT NULL,
PRIMARY KEY (`databaseInfoId`)
) ENGINE=InnoDB

sql สำหรับสร้าง Table : account สำหรับ database : branch ต่างๆ

1
2
3
4
5
CREATE TABLE  `account` (
`accountId` varchar(255) NOT NULL,
`accountName` varchar(255) DEFAULT NULL,
PRIMARY KEY (`accountId`)
) ENGINE=InnoDB;

CenterDataSourceConfig

Code อาจจะยาวแต่จะค่อยอธิบายไปทีล่ะส่วน

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Configuration
@EnableJpaRepositories(
basePackages = "blog.surapong.example.dynamicdb.repository.center",
entityManagerFactoryRef = "centerEntityManagerFactory",
transactionManagerRef = "centerTransactionManager"
)
public class CenterDataSourceConfig {

@Value("${spring.center.hibernate-dialect}")
private String hibernateDialect;

@Bean(name = "centerDataSourceProperties")
@ConfigurationProperties(prefix = "spring.center.datasource")
public DataSourceProperties centerDataSourceProperties() {
return new DataSourceProperties();
}

@Bean(name = "centerDataSource")
public DataSource dataSource(
@Qualifier("centerDataSourceProperties") DataSourceProperties dataSourceProperties
) {
return centerDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}

@Bean(name = "centerEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("centerDataSource") DataSource dataSource
) {
LocalContainerEntityManagerFactoryBean em
= new LocalContainerEntityManagerFactoryBean();
em.setDataSource( dataSource );
em.setPackagesToScan(new String[] { "blog.surapong.example.dynamicdb.entity.center" });
Properties properties = new Properties();

properties.put("hibernate.dialect", hibernateDialect);

JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(properties);

return em;
}

@Bean(name = "centerTransactionManager")
public PlatformTransactionManager centerTransactionManager(
@Qualifier("centerEntityManagerFactory") LocalContainerEntityManagerFactoryBean centerEntityManagerFactory) {
return new JpaTransactionManager(centerEntityManagerFactory.getObject());
}
}
1
@Configuration

ส่วนนี้เป็นการบอกว่าเป็นส่วน Configuration เวลา Spring scan ไฟล์นี้จะได้ทราบว่าต้องมาทำการสร้าง Bean

1
2
3
4
5
@EnableJpaRepositories(
basePackages = "blog.surapong.example.dynamicdb.repository.center",
entityManagerFactoryRef = "centerEntityManagerFactory",
transactionManagerRef = "centerTransactionManager"
)

ส่วนนี้เป็นการบอกว่าให้ Spring ทำการสร้าง JpaRepository นั้นตั้งค่าอะไรบ้าง

  • basePackages : ส่วนนี้บอกว่าตัว Repository จาก package ไหน
  • entityManagerFactoryRef : อันนี้เป็นการบอกว่าตัว entityManagetFactory ที่จะใช้เนี่ยจะใช้จาก Bean ที่ชื่อว่าอะไร
  • transactionManagerRef : อันนี้บอกว่าตัว transactionManager ใช้จาก Bean ที่ชื่อว่าอะไร
1
2
3
4
5
@Bean(name = "centerDataSourceProperties")
@ConfigurationProperties(prefix = "spring.center.datasource")
public DataSourceProperties centerDataSourceProperties() {
return new DataSourceProperties();
}

ส่วนนี้จะเป็นการสร้างตัว DataSourceProperties ว่าง่ายๆคือบอกว่า datasource นี้มันไปต่อที่ไหน username password อะไร ใช้ driver ตัวไหน มีกี่ connection pool อะไรประมาณนั้น ซึ่งอ่านตรงนี้ก็อาจจะงงว่าค่ามันมาจากไหน อันนี้ค่ามันมาจากไฟล์ application.properites ครับ โดยตรงตัว @ConfigurationProperties(prefix = “spring.center.datasource”) ตรงนี้เป็นการบอกว่าไปเอาค่าจากตัว key ที่ขึ้นต้นด้วย spring.center.datasource ครับ โดยสามารถไปอ่านรายละเอียดการทำงานของ @ConfigurationProperties ได้ที่ https://www.baeldung.com/configuration-properties-in-spring-boot

1
2
3
4
5
6
@Bean(name = "centerDataSource")
public DataSource dataSource(
@Qualifier("centerDataSourceProperties") DataSourceProperties dataSourceProperties
) {
return centerDataSourceProperties().initializeDataSourceBuilder().type(HikariDataSource.class).build();
}

ส่วนนี้คือการสร้าง dataSource ครับ โดยจะเห็นว่ามีการตัว DataSourceProperties ที่สร้างจากด้านบนมาสร้างโดยส่วนนี้เราจะใช้ตัว HikariDataSource เป็น DataSource นะครับ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Bean(name = "centerEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("centerDataSource") DataSource dataSource
) {
LocalContainerEntityManagerFactoryBean em
= new LocalContainerEntityManagerFactoryBean();
em.setDataSource( dataSource );
em.setPackagesToScan(new String[] { "blog.surapong.example.dynamicdb.entity.center" });
Properties properties = new Properties();

properties.put("hibernate.dialect", hibernateDialect);

JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(properties);

return em;
}

ส่วนนี้เป็นการสร้างตัว EntityManagerFactory จะเห็นว่ามีการเอา dataSource มาใช้งาน ส่วนต่อไปที่อยากให้ลองดูคือส่วนที่นี้

1
em.setPackagesToScan(new String[] { "blog.surapong.example.dynamicdb.entity.center" });

ตรงส่วนนี้เป็นการบอกว่า Entity ที่เราจะใช้นั้นอยู่ที่ package ไหนครับ

1
properties.put("hibernate.dialect", hibernateDialect);

ส่วนตรงนี้จะเป็นการบอกว่า Jpa ที่จะเนี่ยจะใช้ dialect ตัวไหน (ง่ายคือจะใช้ syntax ของ sql แบบไหน ซึ่งถ้าใช้ mysql ก็ต้องไป set ที่เป็นแบบ mysql ) ซึ่งในตัวอย่างผมจะใช้เป็น mariadb ซึ่งตาม manual บอกให้ใช้ตัว org.hibernate.dialect.MySQL5InnoDBDialect ครับ

1
2
3
4
5
@Bean(name = "centerTransactionManager")
public PlatformTransactionManager centerTransactionManager(
@Qualifier("centerEntityManagerFactory") LocalContainerEntityManagerFactoryBean centerEntityManagerFactory) {
return new JpaTransactionManager(centerEntityManagerFactory.getObject());
}

ส่วนก็คือการสร้าง TransactionManager ครับ

จากทั้งหมดที่บอกมาเราก็จะสามารถสร้างตัว Jpa ที่ติดต่อกับ Database ได้แล้ว ซึ่งตัว Center นั้นต่อแค่ database ตัวเดียวตัว DataSource มันเลยมีแค่ค่าเดียว แต่ถ้าต้องต่อหลาย Database ก็กลายเป็นต้องมีหลาย Datasoruce แต่ที่เราเขียนมันรับได้แค่ DataSource เดียว แล้วเราจะทำยังไงล่ะ

AbstractRoutingDataSource

ปัญหานี้ถูกแก้ได้โดยง่ายเพราะใช้หลักการ OOP หากเราลองไปไล่ดู Code ตรง DataSource จะพบว่ามันเป็นแค่ Interface นั่นแปลว่าถ้าเรา Implement ตัว DataSource ที่ทำงานแบบต่อหลาย Database ขึ้นมาภายใต้ Interface DataSource เดิม เราก็สามารถส่ง DataSource ที่เรา Implement ไปแทนก็จะสามารถทำงานได้ ซึ่งเหล่านักพัฒนาก็ได้ลงเห็นตรงส่วนนี้ก็ได้สร้างตัว AbstractRoutingDataSource ที่ทำงานให้ต่อหลาย Database ไว้เป็นฐานให้เราแล้ว เหลือแค่เราไป Extend แล้วเพิ่ม method ที่เกี่ยวกับ Logic ในการเลือกว่าจะใช้ DataSource ไหนมาทำงานลงไปเอง

DataSourceRouting (Implement เอง)

ตัว Class มันยาวมากแต่จุดที่สำคัญมีแค่ไม่กี่จุดครับ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

public class DataSourceRouting extends AbstractRoutingDataSource {


private static final Logger logger = LoggerFactory.getLogger(DataSourceRouting.class);

private DatabaseInfoRepository databaseInfoRepository;

private Map<Object, Object> mapDataSource;

public DataSourceRouting(DatabaseInfoRepository databaseInfoRepository) {
this.databaseInfoRepository = databaseInfoRepository;
this.mapDataSource = new HashMap<>();

DatabaseInfo firstDatabaseInfo = databaseInfoRepository.findAll(PageRequest.of(0, 1)).get().findFirst().orElseThrow();
DataSource firstDataSource = createDataSource(firstDatabaseInfo.getDatabaseInfoId());

this.mapDataSource.put(firstDatabaseInfo.getDatabaseInfoId(), firstDataSource);
this.setTargetDataSources(mapDataSource);
}

@Override
protected Object determineCurrentLookupKey() {

if (BranchDataSourceContext.getCurrentBranchDataSourceId() == null) {
BranchDataSourceContext.setCurrentBranchDataSourceId(
this.mapDataSource.entrySet().stream().findFirst().orElseThrow().getKey().toString());
}

logger.info("datasource : {}", BranchDataSourceContext.getCurrentBranchDataSourceId());
if (this.mapDataSource.get(BranchDataSourceContext.getCurrentBranchDataSourceId()) == null) {
logger.info("Case create new datasource : {}", BranchDataSourceContext.getCurrentBranchDataSourceId());
DataSource dataSource = createDataSource(BranchDataSourceContext.getCurrentBranchDataSourceId());
this.mapDataSource.put(BranchDataSourceContext.getCurrentBranchDataSourceId(), dataSource);
this.afterPropertiesSet();
}
return BranchDataSourceContext.getCurrentBranchDataSourceId();
}


public DataSource createDataSource(String databaseInfoId) {

DatabaseInfo databaseInfo = databaseInfoRepository.findById(databaseInfoId)
.orElseThrow();

String dbUrl = String.format("jdbc:mariadb://%s/%s?useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&&serverTimezone=Asia/Bangkok",
databaseInfo.getHost(), databaseInfo.getName());

logger.info("dbUrl : {}", dbUrl);


DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
dataSource.setUsername(databaseInfo.getUsername());
dataSource.setPassword(databaseInfo.getPassword());
dataSource.setUrl(dbUrl);
return dataSource;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
protected Object determineCurrentLookupKey() {

if (BranchDataSourceContext.getCurrentBranchDataSourceId() == null) {
BranchDataSourceContext.setCurrentBranchDataSourceId(
this.mapDataSource.entrySet().stream().findFirst().orElseThrow().getKey().toString());
}

logger.info("datasource : {}", BranchDataSourceContext.getCurrentBranchDataSourceId());
if (this.mapDataSource.get(BranchDataSourceContext.getCurrentBranchDataSourceId()) == null) {
logger.info("Case create new datasource : {}", BranchDataSourceContext.getCurrentBranchDataSourceId());
DataSource dataSource = createDataSource(BranchDataSourceContext.getCurrentBranchDataSourceId());
this.mapDataSource.put(BranchDataSourceContext.getCurrentBranchDataSourceId(), dataSource);
this.afterPropertiesSet();
}
return BranchDataSourceContext.getCurrentBranchDataSourceId();
}

นี่คือ Method ที่เราต้องทำการ Override ซึ่ง method นี้จะเป็น Logic ที่เราต้องบอกกับว่าเราจะใช้ datasource ตัวไหนในการทำงาน ซึ่งตัว AbstractRoutingDataSource เนี่ยเขาจะมี attribute ที่ชื่อ mapDataSource ไว้เก็บค่า key กับ DataSource โดย method ให้เรา return key ที่ Map กับ datasource ที่เราต้องการคืนไป ซึ่งจะเห็นว่าเขาโคตรเปิดกว้างให้เรา Implement อะไรก็ได้ตามใจเราเลย ซึ่ง Code ของผมไม่ได้ทำอะไรมากไปกว่าการไปเอาค่าจาก BranchDataSourceContext ที่เก็บค่าไว้ว่าอยากจะใช้ datasource ไหน ลองอ่าน Logic เอาเลยครับ ที่มันดูยาวๆเพราะทำแบบ ถ้าไม่มี DataSource ก็ไปหาข้อมูลใน DatabaseInfo ว่ามีไหม ถ้ามีก็เอามาสร้าง dataSource แล้วก็เพิ่มเข้าไปใน map เอง

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public DataSource createDataSource(String databaseInfoId) {

DatabaseInfo databaseInfo = databaseInfoRepository.findById(databaseInfoId)
.orElseThrow();

String dbUrl = String.format("jdbc:mariadb://%s/%s?useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&&serverTimezone=Asia/Bangkok",
databaseInfo.getHost(), databaseInfo.getName());

logger.info("dbUrl : {}", dbUrl);


DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.mariadb.jdbc.Driver");
dataSource.setUsername(databaseInfo.getUsername());
dataSource.setPassword(databaseInfo.getPassword());
dataSource.setUrl(dbUrl);
return dataSource;
}

ส่วนนี้คือการสร้าง DatatSource จะเห็นว่าต่างจากตอนสร้างใน Center แต่ไม่ต่างอะไรมากแค่ทำการสร้างแบบ Manual เท่านั้นเอง ส่วนข้อมูลพวก username,password, host, dbName มาจาก DatabaseInfo ใน database

BranchDataSourceContext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class BranchDataSourceContext {

private BranchDataSourceContext() {}

private static final ThreadLocal<String> currentThread = new ThreadLocal<>();

public static void setCurrentBranchDataSourceId(String dataSourceId) {
currentThread.set(dataSourceId);
}

public static String getCurrentBranchDataSourceId() {
return currentThread.get();
}

public static void clearBranchContext() {
currentThread.remove();
}

}

คราวนี้อาจจะสงสัยเฮ้ยคุณเลือกว่าใช้ DataSource นี้ DataSouce ยังไง รู้ได้ไงว่าต้องใช้ตัวนี้ตัวนั้น คืออันนี้ผมเลือกว่าจะใช้ DataSouce ไหนตาม ค่าที่ถูก Set ค่าไว้ ว่าง่ายๆผมจะมีตัวที่เรียกใช้ตัว BranchDataSourceContext แล้ว set ค่าว่าจะใช้ตัวไหน ซึ่งคราวนี้ก็จะมีประเด็นว่า ถ้ามันทำงานหลายๆ Thread พร้อมกันจะเกิดอะไรขึ้น ซึ่งผมก็แก้ปัญหาโดยการใช้ ThreadLocal ของ Java มาเก็บค่าให้กับ Thread นั้นว่ากำลังใช้ Thread อะไรอยู่นั่นเอง

BranchDataSourceConfig

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Configuration
@EnableJpaRepositories(
basePackages = "blog.surapong.example.dynamicdb.repository.branch",
entityManagerFactoryRef = "branchEntityManagerFactory",
transactionManagerRef = "branchTransactionManager"
)
public class BranchDataSourceConfig {

@Value("${spring.center.hibernate-dialect}")
private String hibernateDialect;

@Bean(name = "branchDataSource")
public DataSource branchDataSource(@Autowired DatabaseInfoRepository databaseInfoRepository) {
return new DataSourceRouting(databaseInfoRepository);
}


@Bean(name = "branchEntityManagerFactory")
public LocalContainerEntityManagerFactoryBean entityManagerFactory(
@Qualifier("branchDataSource") DataSource branchDataSource
) {
LocalContainerEntityManagerFactoryBean em
= new LocalContainerEntityManagerFactoryBean();
em.setDataSource(branchDataSource);
em.setPackagesToScan(new String[] { "blog.surapong.example.dynamicdb.entity.branch" });

Properties properties = new Properties();
properties.put("hibernate.dialect", hibernateDialect);

JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
em.setJpaVendorAdapter(vendorAdapter);
em.setJpaProperties(properties);

return em;
}

@Bean(name = "branchTransactionManager")
public PlatformTransactionManager centerTransactionManager(
@Qualifier("branchEntityManagerFactory") LocalContainerEntityManagerFactoryBean branchEntityManagerFactory) {
return new JpaTransactionManager(branchEntityManagerFactory.getObject());
}

}

อันนี้จะเห็นว่าแทบไม่ต่าง CenterDataSource เลย แค่ต่างกันตรง Config ค่าแล้วก็ตรง DataSource เท่านั้นเอง

AccountController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@RestController
public class AccountController {
@Autowired
private AccountService accountService;

@Autowired
private AccountRepository accountRepository;

@GetMapping("/test/insert")
public String testInsert(
@RequestParam("db") String dbInfoId
) {
BranchDataSourceContext.setCurrentBranchDataSourceId(dbInfoId);
String id = UUID.randomUUID().toString();
Account account = new Account()
.setAccountId(dbInfoId + id )
.setAccountName("AccountName-" + id);
account = accountRepository.save(account);

return account.getAccountName();
}

@GetMapping("/test/error")
public String testError(
@RequestParam("db") String dbInfoId
) {
BranchDataSourceContext.setCurrentBranchDataSourceId(dbInfoId);

String id = UUID.randomUUID().toString();
Account account = new Account()
.setAccountId(dbInfoId + id )
.setAccountName("AccountName-" + id);
account = accountRepository.save(account);

return account.getAccountName();
}
}

ส่วนนี้เป็นส่วนการจำลองการทำงานหลาย Database ดูเหมือนเยอะแต่ไม่มีอะไรมากลองมาอ่านทีละส่วนกันครับ

1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping("/test/insert")
public String testInsert(
@RequestParam("db") String dbInfoId
) {
BranchDataSourceContext.setCurrentBranchDataSourceId(dbInfoId);
String id = UUID.randomUUID().toString();
Account account = new Account()
.setAccountId(dbInfoId + id )
.setAccountName("AccountName-" + id);
account = accountRepository.save(account);

return account.getAccountName();
}

ส่วนนี้จะเป็นการสร้างขารับ Http request โดยรับ query parameter ที่ชื่อ db เข้ามาโดยค่า db นั้นจะเป็นตัวบอกว่าใช้ Database อันไหน ซึ่งจะเห็นว่ามีการใช้ BranchDataSourceContext.setCurrentBranchDataSourceId(dbInfoId); อันนี้เป็นการ Set แล้วว่าจะใช้ database ตัวไหนผ่าน BranchDataSourceContext ซึ่งตัว DataSourceRouting ของเราจะใช้ BranchDataSourceContext ตัดสินใจต่อใน method : determineCurrentLookupKey ซึ่งเรามาลองทดลองดูว่าจะได้ผลลัพธ์อะไร

ทดลอง /test/insert

ส่วนข้อมูลใน DatabaseInfo

เมื่อทดลองยิงไปที่ /test/insert โดยใช้ query param เป็น england จะได้ผลลัพธ์ดังภาพ ขอรวมผลลัพธ์จากการเรียกผ่าน browser และผลลัพธ์จากการ Query Database ซึ่งจะเห็นว่ามีข้อมูล insert เข้าไปใน Db จริงๆ และค่า accountName นั้นมีค่าตรงกันกับผลลัพธ์จาก Browswer

คราวนี้มาทดลองโดยเปลี่ยน query param เป็น thailand กันดูบ้างซึ่งผลลัพธ์เป็นดังภาพ

ซึ่งจากการทดลองก็พบว่าทำงานได้ถูกต้อง

ทดลอง Transactional กัน

ก็ตามสูตรครับถ้าใช้ Relational Database Feature ที่เราคาดหวังอยากได้จากมันคือ Transaction ดังนั้นเราก็ต้องมา Test ว่ามันทำงานได้จริงรึเปล่าโดยเราทดสอบผ่าน ตัว AccountController ที่ไปเรียกใช้งานตัว AccountService ซึ่งมีการเขียนจงใจให้ Error เพื่อทดสอบว่าข้อมูลที่ Insert ไปต้องหายไปเสมือนว่าไม่เคยเกิดขึ้น

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

// AccountController

@GetMapping("/test/error")
public String testError(
@RequestParam("db") String dbInfoId
) {
BranchDataSourceContext.setCurrentBranchDataSourceId(dbInfoId);

String id = UUID.randomUUID().toString();
Account account = new Account()
.setAccountId(dbInfoId + id )
.setAccountName("AccountName-" + id);
account = accountRepository.save(account);

return account.getAccountName();
}


// AccountService

@Service
public class AccountService {

@Autowired
private AccountRepository accountRepository;

@Transactional(transactionManager = "branchTransactionManager")
public Account testTransactional(Account account) {
account = accountRepository.save(account);
if (true) {
throw new RuntimeException("Test throw error");
}
return account;
}
}

สังเกตตรง @Transactional(transactionManager = “branchTransactionManager”) นั้นต้องมีการกำหนดชื่อด้วย เพราะตัว transactionManager นั้นถูกสร้างขึ้นมาสองตัวนั่นคือของ center กับ ของ branch เราจึงต้องกำหนดชื่อมันเลยว่าใช้ตัวไหน ซึ่งจากการทดลองพบว่าไม่มีข้อมูลโผล่ไปเพิ่มใน Database ของ england มีแค่ 1 ROW เท่าเดิม นั่นก็แปลว่า Feature : Transaction ของเราทำงาน

สรุป

สำหรับตอนนี้เราก็ได้ทดลองทำอะไรแปลกๆก็คือทดลองต่อหลาย Database ในเวลาเดียวกันแบบ Runtime กันแล้ว หวังว่าคงจะสนุกและได้แนวทางไปประยุกต์ใช้ในงานต่างๆกันนะครับ

Ref

เพลงประกอบการเขียน Blog

อยู่ Joox ก็แนะนำเพลงนี้มาให้ฟังซึ่งงงมากว่าเฮ้ยไม่ได้อยู่ใน Playlist อะไรเลย เด้งมาได้ไง พอฟังๆไปก็รู้สึกคุ้นมากๆเลยไป Search หาใน Youtube ก็เลยร้องอ๋อเลยเพราะเป็นเพลง OST ของละครช่อง 5 สมัยตอนเด็ก

Spring - ทำการ Autowire เข้า Object แบบ Manual

Spring - ทำการ Autowire เข้า Object แบบ Manual

สำหรับตอนนี้มันเริ่มจากต้องการสร้าง Class นึงขึ้นมา แล้วมันต้องการ Dependency จำนวนมาก แต่มีแค่บาง Field เท่านั้นที่ไม่เหมือนกัน หรือใช้แล้วทิ้งไม่อยากเอาไปเก็บใน IOC ซึ่งจริงๆมันก็ทำได้ แต่คุณต้องมานั่งสั่ง setter หรือทำ constructor 10 - 20 field ด้วยความขี้เกียจของ Programmer ก็เลยไปหาวิธีที่มันจะทำการ Autowire แบบ Manual เองไม่ได้ ไม่ต้องให้ Spring ทำให้อัตโนมัติ

ลองทำกันเลย

สำหรับใครต้องการตัว Source code เต็มสามารถไปดูได้ที่ https://github.com/surapong-taotiamton/example-autowireafterstart

ต้องไปเปลี่ยนตัวค่าตัว config เหล่านี้ด้วยนะครับเป็น db host , username , password ของเครื่องท่านเองนะครับ

1
2
3
4
spring.datasource.jdbc-url=jdbc:mariadb://192.168.56.101:3306/test?useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&&serverTimezone=Asia/Bangkok
spring.datasource.url=jdbc:mariadb://192.168.56.101:3306/test?useUnicode=yes&characterEncoding=UTF-8&allowPublicKeyRetrieval=true&&serverTimezone=Asia/Bangkok
spring.datasource.username=root
spring.datasource.password=root

ตัวอย่าง AccountService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class AccountService {

@Autowired
private AccountRepository accountRepository;

@Value("${config.account}")
private String accountConfig;

public String getAccountConfig() {
return this.accountConfig;
}

public Account createAccount(String accountId, String accountName) {
return this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
}

@Transactional(isolation = Isolation.REPEATABLE_READ)
public Account testTransactional(String accountId, String accountName) {
Account account = this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
if (true) {
throw new RuntimeException("Test transactional");
}
return account;
}
}

Class นี้เป็น Service จะเห็นว่ามีการใช้ @Autowired และ @Value ซึ่งถ้าเราใส่ @Service หรือ @Component ไว้เหนือ Class เวลา Start server ตัว Spring จะสร้าง Bean และ Inject Bean ต่างๆให้เราอัตโนมัติ แต่ตอนนี้เราต้องการสร้างแบบ Manual ดังนั้นจึงไม่มี

AccountController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RestController
public class AccountController {

@Autowired
private ApplicationContext applicationContext;

@GetMapping("/get-config")
public String getConfig() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
return accountService.getAccountConfig();
}

@GetMapping("/test-create")
public String testCreate() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.createAccount("test-create-id", "test-create-name");
return account.getAccountId() + " : " + account.getAccountName();
}

@GetMapping("/test-transactional-error-case")
public String testCreateErrorCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.testTransactional("test-transactional-create-id", "test-transactional-create-id");
return account.getAccountId() + " : " + account.getAccountName();
}

@GetMapping("/test-error-transactional-success-case")
public String testCreateSuccessCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
accountService = (AccountService) applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(accountService, null);
Account account = accountService.testTransactional("test-transactional-create-id-complete", "test-transactional-create-id-complete");
return account.getAccountId() + " : " + account.getAccountName();
}
}

โอเคมันอาจจะยาวนะครับ เพราะมันมีกรณีที่อยากแสดงให้เห็นว่าทำงานได้จริงมีปัญหาไหม เดี๋ยวเราค่อยๆมาดูทีละส่วนเพื่อทำความเข้าใจกันครับ

1
2
@Autowired
private ApplicationContext applicationContext;

ส่วนนี้คือการการ Autowired ตัว ApplicationContext เข้ามา โดยตัว ApplicationContext นั้นจำเป็นในการ Autowired แบบ Manaul ครับ

1
2
3
4
5
6
@GetMapping("/get-config")
public String getConfig() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
return accountService.getAccountConfig();
}

ตรงส่วนนี้ผมมีไว้ Test โดยยิง Http request มาแล้วทดลองสร้าง AccountService และ Autowire ค่าเข้าไป โดยส่วนที่ทำการ Autowire คือบรรทัด

1
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);

ส่วนตรงนี้มีไว้เพื่อทดสอบว่าสามารถ Autowire ตรง @Value(“${config.account}”) เข้าไปได้หรือไม่

1
return accountService.getAccountConfig();

โดยเมื่อทำการทดลองยิง http request ผ่าน Browser ก็ได้ผลลัพธ์ดังภาพ

โดยค่านั้นตรงกับที่ผม Set ใน application.properties

ซึ่งทำให้เห็นว่าเราสามารถ Autowire ค่าเข้าไปได้

ทดสอบดูว่าสามารถ Insert ค่าลง Database ได้ไหม

ตัวอย่างที่แล้วเราทดลองตัว @Value ไปแล้ว เรามาลองดู @Autowired นั้นทำงานได้ไหม

1
2
3
4
// AccountService
public Account createAccount(String accountId, String accountName) {
return this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
}

Code ส่วนนี้เป็นการ Save ข้อมูล Account ลง Database

1
2
3
4
5
6
7
8
// AccountController
@GetMapping("/test-create")
public String testCreate() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.createAccount("test-create-id", "test-create-name");
return account.getAccountId() + " : " + account.getAccountName();
}

Code ส่วนนี้เป็นขา Controller ไว้รับ Request แล้วทำเรียกใช้ AccountService อีกที โดย Code ทั้งสองส่วนนี้จะทดลองว่าเราสามารถ Autowire ตัว AccountRepository ลงไปได้ไหม ซึ่งผลการทดลองเป็นดังภาพด้านล่าง

ซึ่งจะเห็นว่าเราสามารถ Insert ข้อมูลลง Database ได้

Transaction ใช้งานได้ไหม

Feature ที่สำคัญ Feture นึงที่เรามักใช้บ่อยๆเพื่อลดความซับซ้อนเวลาเกิดปัญหาในระหว่างการทำงานนั่นคือ Feature : Transaction ส่วนใครไม่รู้ว่า transaction คืออะไรสามารถไปอ่านได้ที่ Concurrency Part 2 - Transaction , ACID และ Isolation level ของ Relational Database เลยครับ

1
2
3
4
5
6
7
8
9
// AccountService
@Transactional(isolation = Isolation.REPEATABLE_READ)
public Account testTransactional(String accountId, String accountName) {
Account account = this.accountRepository.save(new Account().setAccountId(accountId).setAccountName(accountName));
if (true) {
throw new RuntimeException("Test transactional");
}
return account;
}

ตัว method : testTransactional มีไว้ทดลองว่าถ้าเราทำการ insert ข้อมูลแล้วหลังจากนั้นเกิด Error (จะเห็นว่าจงใจ Throw ออกมาเลย) ถ้า Transaction มันทำงานอยู่มันจะต้องทำการ Rollback ไปเป็นเหมือนไม่เคยทำการ Insert ได้

1
2
3
4
5
6
7
8
// AccountController
@GetMapping("/test-transactional-error-case")
public String testCreateErrorCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
Account account = accountService.testTransactional("test-transactional-create-id", "test-transactional-create-id");
return account.getAccountId() + " : " + account.getAccountName();
}

ตรงนี้จะเห็นว่าตรงการสร้าง AccountService ยังเหมือนเดิมแต่เรียกใช้ method : testTransactional แทน โดยเรามาทดลองกันว่าผลลัพธ์จะเป็นยังไง

ตรงนี้เกิดการ Error เพราะ Code เรากะให้มัน Error อยู่แล้วไม่ต้องตกใจ

ตรงนี้จะเห็นว่ายังมีข้อมูล Insert ลงไปใน Database ซึ่งแปลว่า Feature : Transaction นั้นไม่ทำงาน

เรามาทำให้ Feature : Transaction ทำงานกัน

วิธีนั้นเพิ่ม Code แค่บรรทัดเดียวครับดูได้จากตัว Code ด้านล่าง

1
2
3
4
5
6
7
8
@GetMapping("/test-error-transactional-success-case")
public String testCreateSuccessCase() {
AccountService accountService = new AccountService();
applicationContext.getAutowireCapableBeanFactory().autowireBean(accountService);
accountService = (AccountService) applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(accountService, null);
Account account = accountService.testTransactional("test-transactional-create-id-complete", "test-transactional-create-id-complete");
return account.getAccountId() + " : " + account.getAccountName();
}

โดยจะเห็นว่ามีการเพิ่ม Code ด้านล่างเข้าไป โดย Code ตรงนี้เป็นการบอกให้ตัว spring ทำ BeanPostProcessor ด้วย ซึ่งตัว @Transactional นั้นจะทำงานตอน BeanPostProcessor

1
accountService = (AccountService) applicationContext.getAutowireCapableBeanFactory().applyBeanPostProcessorsAfterInitialization(accountService, null);

คราวนี้มาทดลองกันว่ามันทำงานได้ไหมกัน

ซึ่งจากภาพจะเห็นว่าไม่มีการ insert row ที่มี id : test-transactional-create-id-complete ลงไปใน Database เลย ดังนั้นแสดงว่า Feature : Transaction ของเราทำงานได้แล้ว

สรุป

สำหรับตอนนี้เรามาทดลอง Inject Bean แบบ Manual กันแล้ว ซึ่งน่าจะมีประโยชน์สำหรับผู้ที่ต้องการทำอะไรที่ต้องทำแบบ Runtime ไม่ได้ทำตอน Start server สำหรับตอนนี้ก็ขอจบเพียงเท่านี้ครับ

เพลงประกอบการเขียน Code

เป็นครึ่งใจ version ที่เร็วกว่าเดิมขึ้นมานิดนึง ฟังแล้วได้อารมณ์แบบเออเอาไง ครึ่งใจที่เหลือเอายังไง