<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Java on yothinix</title><link>https://yothinix-com.pages.dev/tags/java/</link><description>Recent content in Java on yothinix</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Sun, 13 Feb 2022 18:40:43 +0700</lastBuildDate><atom:link href="https://yothinix-com.pages.dev/tags/java/index.xml" rel="self" type="application/rss+xml"/><item><title>เรื่องประหลาดๆ ของการนับปีใน Java</title><link>https://yothinix-com.pages.dev/java-year-behavior/</link><pubDate>Wed, 01 Dec 2021 13:11:00 +0700</pubDate><guid>https://yothinix-com.pages.dev/java-year-behavior/</guid><description>&lt;p&gt;ลืมหัวข้อ blog นี้ไปก่อนนะครับ แล้วลองพยายามเดาดูว่าโค้ด Java ข้างล่างนี้พยายามทำอะไร มันก็ดูตรงไปตรงมาใช่มั้ยครับ แล้วถ้ารันออกมาสมมติวันนี้วันที่ 1 ธันวาคม 2021 สิ่งที่ function นี้คืนกลับมาก็&lt;/p&gt;
&lt;p&gt;&lt;img src="https://yothinix-com.pages.dev/java-year-behavior/lfVaJ8HNV_hu_bc77c8395af92cfa.webp"
srcset="https://yothinix-com.pages.dev/java-year-behavior/lfVaJ8HNV_hu_7408599a6dfa8fa0.webp 480w, https://yothinix-com.pages.dev/java-year-behavior/lfVaJ8HNV_hu_bb8a658fc8c5b508.webp 960w, https://yothinix-com.pages.dev/java-year-behavior/lfVaJ8HNV_hu_bc77c8395af92cfa.webp 1440w"
sizes="(max-width: 960px) 100vw, 960px"
width="1440" height="810"
alt="SomeUtils_java.png"
loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;เป็น 2020-12-01 ใช่มั้ยครับ ถ้าเราเขียน Unit test ไว้ เทสนี้ก็น่าจะผ่านได้ง่ายๆ ปกติไม่มีอะไร&lt;/p&gt;
&lt;p&gt;เรื่องมันมีอยู่ว่า ถ้าสมมติวันนี้เป็นวันที่ 31 ธันวาคม 2021 แล้วโค้ดชุดนี้เอาไว้บอกว่าควรจะจ่ายเงินเดือนเราตอนวันที่เท่าไร ลองทายดูมั้ยครับว่าจะเกิดอะไรขึ้น&lt;/p&gt;
&lt;h2 id="รจกกบ-datetimeformatter-กนกอน"&gt;&lt;strong&gt;รู้จักกับ DateTimeFormatter กันก่อน&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;ผมสังเกตุเห็น อย่างน้อยก็ใน Codebase ที่ผมทำงานด้วยอยู่ว่า Java Developer ส่วนใหญ่จะยังคุ้นเคยกับ Date API ที่มีมาตั้งแต่ Java 1.0 หน้าตาประมาณโค้ดข้างล่างครับ&lt;/p&gt;
&lt;p&gt;&lt;img src="https://yothinix-com.pages.dev/java-year-behavior/kL7ArNt8K_hu_3c4e19a5eda88d50.webp"
srcset="https://yothinix-com.pages.dev/java-year-behavior/kL7ArNt8K_hu_3224114059aece06.webp 480w, https://yothinix-com.pages.dev/java-year-behavior/kL7ArNt8K_hu_63b5540bf5387b80.webp 960w, https://yothinix-com.pages.dev/java-year-behavior/kL7ArNt8K_hu_3c4e19a5eda88d50.webp 1440w"
sizes="(max-width: 960px) 100vw, 960px"
width="1440" height="810"
alt="SomeOldUtils_java.png"
loading="lazy" decoding="async"&gt;&lt;/p&gt;
&lt;p&gt;ซึ่ง จะเห็นได้ว่า Date API ตัวเก่าของ Java นั้นมีปัญหาเยอะมาก อย่างเช่นการที่จะปั้นเวลาซักอย่างก็เริ่มที่ปี 1900 แล้วบวกไปเอาหรือเดือนที่เริ่มที่ index ที่ 0 ซึ่งไม่ใช่ว่าทางทีมพัฒนา Java จะไม่พยายามแก้ปัญหานี้นะครับ ความพยายามแรกคือ Calendar class ที่ออกมาใน Java 1.1 แต่ก็ยังมีปัญหาหลายๆ อย่างอยู่ จน Java Developer หลายคนเลือกที่จะไปใช้ 3rd Party Library อย่าง Joda-Time แทน ซึ่งหลังจาก Oracle ได้ยึด Java ไปก็เลยเกิด Date/Time API ตัวใหม่ขึ้นมาใน Java 8 ซึ่งมีพื้นฐานหลายๆ อย่างมาจาก Joda-Time ครับ&lt;/p&gt;</description><content:encoded><![CDATA[<p>ลืมหัวข้อ blog นี้ไปก่อนนะครับ แล้วลองพยายามเดาดูว่าโค้ด Java ข้างล่างนี้พยายามทำอะไร มันก็ดูตรงไปตรงมาใช่มั้ยครับ แล้วถ้ารันออกมาสมมติวันนี้วันที่ 1 ธันวาคม 2021 สิ่งที่ function นี้คืนกลับมาก็</p>
<p><img src="/java-year-behavior/lfVaJ8HNV_hu_bc77c8395af92cfa.webp"
       srcset="/java-year-behavior/lfVaJ8HNV_hu_7408599a6dfa8fa0.webp 480w, /java-year-behavior/lfVaJ8HNV_hu_bb8a658fc8c5b508.webp 960w, /java-year-behavior/lfVaJ8HNV_hu_bc77c8395af92cfa.webp 1440w"
       sizes="(max-width: 960px) 100vw, 960px"
       width="1440" height="810"
       alt="SomeUtils_java.png"
       loading="lazy" decoding="async"></p>
<p>เป็น 2020-12-01 ใช่มั้ยครับ ถ้าเราเขียน Unit test ไว้ เทสนี้ก็น่าจะผ่านได้ง่ายๆ ปกติไม่มีอะไร</p>
<p>เรื่องมันมีอยู่ว่า ถ้าสมมติวันนี้เป็นวันที่ 31 ธันวาคม 2021 แล้วโค้ดชุดนี้เอาไว้บอกว่าควรจะจ่ายเงินเดือนเราตอนวันที่เท่าไร ลองทายดูมั้ยครับว่าจะเกิดอะไรขึ้น</p>
<h2 id="รจกกบ-datetimeformatter-กนกอน"><strong>รู้จักกับ DateTimeFormatter กันก่อน</strong></h2>
<p>ผมสังเกตุเห็น อย่างน้อยก็ใน Codebase ที่ผมทำงานด้วยอยู่ว่า Java Developer ส่วนใหญ่จะยังคุ้นเคยกับ Date API ที่มีมาตั้งแต่ Java 1.0 หน้าตาประมาณโค้ดข้างล่างครับ</p>
<p><img src="/java-year-behavior/kL7ArNt8K_hu_3c4e19a5eda88d50.webp"
       srcset="/java-year-behavior/kL7ArNt8K_hu_3224114059aece06.webp 480w, /java-year-behavior/kL7ArNt8K_hu_63b5540bf5387b80.webp 960w, /java-year-behavior/kL7ArNt8K_hu_3c4e19a5eda88d50.webp 1440w"
       sizes="(max-width: 960px) 100vw, 960px"
       width="1440" height="810"
       alt="SomeOldUtils_java.png"
       loading="lazy" decoding="async"></p>
<p>ซึ่ง จะเห็นได้ว่า Date API ตัวเก่าของ Java นั้นมีปัญหาเยอะมาก อย่างเช่นการที่จะปั้นเวลาซักอย่างก็เริ่มที่ปี 1900 แล้วบวกไปเอาหรือเดือนที่เริ่มที่ index ที่ 0 ซึ่งไม่ใช่ว่าทางทีมพัฒนา Java จะไม่พยายามแก้ปัญหานี้นะครับ ความพยายามแรกคือ Calendar class ที่ออกมาใน Java 1.1 แต่ก็ยังมีปัญหาหลายๆ อย่างอยู่ จน Java Developer หลายคนเลือกที่จะไปใช้ 3rd Party Library อย่าง Joda-Time แทน ซึ่งหลังจาก Oracle ได้ยึด Java ไปก็เลยเกิด Date/Time API ตัวใหม่ขึ้นมาใน Java 8 ซึ่งมีพื้นฐานหลายๆ อย่างมาจาก Joda-Time ครับ</p>
<p>ซึ่งใน API ตัวใหม่นอกจาก LocalDate, LocalTime ฯลฯ ตัว API ยังมาพร้อมกับ Formatter กับ Parser ตัวใหม่ด้วยครับชื่อว่า DateTimeFormatter ซึ่งช่วยให้เรา Parse เวลาเข้ามาได้ง่ายขึ้นพร้อมกับ constants อีกจำนวนนึงที่ช่วยให้เราประหยัดเวลาในการปั้น Date format ไปได้เยอะเลย แถมยัง Thread-safe ด้วยนะ</p>
<h2 id="pattern-letters-and-symbols-for-formatting-and-parsing"><strong>Pattern letters and Symbols for Formatting and Parsing</strong></h2>
<p>เวลาที่เรา Format Date/Time เนี่ย pattern ส่วนใหญ่ที่เราต้องทำคงหนีไม่พ้น yyyy-MM-dd ดูเผินๆ มันก็ตรงตัวใช่มั้ยครับ อาจจะมีแค่ M เดือน (Month-of-year) ที่ต้องเปลี่ยนไปใช้ M ใหญ่ เพราะมันดันไปพ้องกับอีก m นึงในหน่วยเวลาคือ นาที (minute-of-hour)</p>
<p><img src="/java-year-behavior/Wu-mbMMin_hu_2e0db77e2968da5a.webp"
       srcset="/java-year-behavior/Wu-mbMMin_hu_95979546aa73fe05.webp 480w, /java-year-behavior/Wu-mbMMin_hu_af1407fab0e6a621.webp 960w, /java-year-behavior/Wu-mbMMin_hu_2e0db77e2968da5a.webp 1440w"
       sizes="(max-width: 960px) 100vw, 960px"
       width="1440" height="1946"
       alt="Screen Shot 2564-12-01 at 22.07.06.png"
       loading="lazy" decoding="async"></p>
<p>ซึ่งพวกเราน่าจะคุ้นเคยกับ pattern พวกนี้ดีอยู่แล้วใช่มั้ยครับ แล้วผมจะเขียนบล็อกนี้มาทำไมนะ ผมถามอีกทีว่า พวกเราน่าจะคุ้นเคยกับ pattern พวกนี้ดีอยู่แล้วใช่มั้ยครับ?</p>
<p>พอดีวันนี้ ในขณะที่ผมไต่ไปตาม codebase ที่ผมดูแลอยู่ผมไปสะดุดกับ pattern หน้าตาแบบภาพบนสุดเลยครับคือ YYYY-MM-dd ซึ่ง ถ้ามองผ่านๆ ผมก็อาจจะไม่คิดอะไร แต่ผมดันไปเอะใจว่า ผมเคยเห็นแต่ yyyy-MM-dd นี่นา แล้ว YYYY กับ yyyy มันต่างกันยังไงนะ</p>
<h2 id="เรองของป-ตางๆ"><strong>เรื่องของปี (ต่างๆ)</strong></h2>
<p>ถ้าเทียบจากตาราง Pattern Letters and Symbols ในหัวข้อที่แล้ว เราจะเจอว่า y เนี่ยมันคือ year-of-era ส่วน Y มันเป็น week-based-year ซึ่งในส่วนของ Presentation ก็มีความหมายว่า year เหมือนกัน และถ้ามองดูดีๆ จะเห็นว่าเรามี u ด้วยซึ่งมีความหมายว่า year เหมือนกัน</p>
<p>ส่วนที่ยิ่งประหลาดไปอีกคือ Example ครับ จะเห็นว่า u กับ y เนี่ยเหมือนกันเลยคือ 2004; 04 แต่ว่า Y เนี่ยดันเป็น 1996; 96 อะไรของ Java วะคับ อะไรคือ year-of-era อะไรคือ week-based-year แล้วมันต่างจากปีที่เรารู้จักกันยังไง ผมจะสรุปสั้นๆ ให้ฟัง</p>
<h3 id="year-of-era-y-เลก-หรอ-u">Year-of-era (y เล็ก หรือ u)</h3>
<p>ตัวเนี่ยไม่มีอะไรซับซ้อนเลยครับ มันคือปีอย่างที่เรารู้จักกันนี่แหละ หรือถ้าทางเทคนิคหน่อยคือปีในทาง Gregorian calendar ที่เราใช้กันอยู่เป็นปกติหรือ ค.ศ. นั่นเองครับ ถ้าเทียบกับปีปัจจุบันคือ 2021</p>
<h3 id="week-based-year-y-ใหญ">Week-based-year (Y ใหญ่)</h3>
<p>ตัวนี้แหละที่เป็นสาเหตุให้ผมต้องเขียนบล็อกนี้ขึ้นมา Week-based-year คือการนับปีตามสัปดาห์ครับ ซึ่งเราก็น่าจะเอะต่อว่า แล้วสัปดาห์นี่มันเริ่มนับยังไงนะ บางปฏิทินก็นับวันอาทิตย์ บางก็นับวันจันทร์ ในส่วนของ Java นี้นับปฏิทินเริ่มที่วันจันทร์ครับตามมาตรฐาน ISO 8601</p>
<p><img src="/java-year-behavior/nniC59Jv6_hu_283f24ca68f7745c.webp"
       srcset="/java-year-behavior/nniC59Jv6_hu_cacad3ea342d843e.webp 480w, /java-year-behavior/nniC59Jv6_hu_3d188fb0de997991.webp 960w, /java-year-behavior/nniC59Jv6_hu_283f24ca68f7745c.webp 1440w"
       sizes="(max-width: 960px) 100vw, 960px"
       width="1440" height="470"
       alt="Screen Shot 2564-12-01 at 17.32.01.png"
       loading="lazy" decoding="async"></p>
<p>ถ้ายังงงอยู่ งั้นลองดูตัวอย่างนี้นะครับ (จริงๆ App Calendar ของ Apple นี่มันก็แอบสปอยอยู่นะ) ถ้าเราใช้ YYYY (Week-based-year) ตอนเรา Format date ในวันที่ 31 ธันวาคม 2021 แทนที่มันจะออกมาเป็น 2021-12-31 สิ่งที่คืนกลับมาจาก DateFormatter จะกลายเป็น 2022-12-31 ครับ เพราะว่าในสัปดาห์นี้ตั้งแต่วันจันทร์ที่ 27 ธันวาคม 2021</p>
<h2 id="แตละสปดาหเรมนบวนจนทรจรงเหรอ"><strong>แต่ละสัปดาห์เริ่มนับวันจันทร์จริงเหรอ?</strong></h2>
<p>พอยิ่งขุดลึกลงไปอีก ผมเลยเจอว่าการที่จะนับแต่ละสัปดาห์จะเริ่มวันจันทร์หรือวันอาทิตย์ ขึ้นอยู่กับ Locale ที่เราใช้ในการ Format ครับโดยเราสามารถใส่หรือไม่ใส่ก็ได้เป็น arguments ตัวที่สองของ DateTimeFormatter.ofPattern() ซึ่ง default Locale จะเป็น <a href="http://locale.us/">Locale.US</a> ซึ่งนับวันอาทิตย์เป็นวันเริ่มต้นของสัปดาห์ครับ ทำให้ในเคสข้างบนของเรามันจะนับ YYYY เป็นปี 2022 ตั้งแต่วันอาทิตย์ที่ 26 ธันวาคม 2021 เลย</p>
<p>แต่ถ้าเราอยากให้นับสัปดาห์เริ่มที่วันจันทร์เราสามารถเปลี่ยนไปใช้ Locale อื่นได้ครับ อย่างเช่น Locale.FRANCE จะเริ่มในวันจันทรแทน ซึ่งจะทำให้เคสวันอาทิตย์ที่ 26 ธันวาคม 2021 ยังคงเป็นปี 2021 อยู่ครับ</p>
<h2 id="แลวเราควรจะใชตวไหนด-u-y-y"><strong>แล้วเราควรจะใช้ตัวไหนดี u, y, Y</strong></h2>
<p>ถ้าตอบแบบ Consult คงต้องตอบว่า It’s depends แต่ว่าผมสรุปให้คร่าวๆ ครับ</p>
<h3 id="u-year">u (year)</h3>
<p>ผมเข้าใจว่าในทาง Meaning ตัวนี้น่าจะเหมาะกับเราที่สุดครับ เพราะมันเป็นปีตรงๆ โดยไม่มีอะไร Constraint เลย แต่ผมเข้าใจที่หลายคนไม่เลือกใช้ เพราะมันน่าจะงงๆ เวลาไปเขียนร่วมกับหน่วยอื่นๆ คนเลยนิยม y มากกว่า</p>
<h3 id="y-year-of-era">y (year-of-era)</h3>
<p>ตัวนี้คนน่าจะใช้เยอะสุด แล้วน่าจะสับสนน้อยสุด เพราะเราชินกับมันมาจากหลายๆ ภาษา และโดยตัวมันเองก็สื่อออกมาด้วย แต่ตามที่มัน Design มามันถูกออกแบบให้ใช้คู่กับตัว G (era) ด้วยครับ แต่ถ้าเราไม่ใช้ผมว่าก็ไม่ผิดอะไรนะ ส่วนตัวผม Prefer ตัวนี้นะ</p>
<h3 id="y-week-based-year">Y (week-based-year)</h3>
<p>ตัวนี้เหมาะกับ calculation อะไรก็ตามที่เราต้องทำเป็น week-based ครับ ซึ่งอาจจะใช้กับงานที่เป็น batch รันรายสัปดาห์แล้วเก็บแค่ปีกับสัปดาห์</p>
<p>ก็ประมาณนี้ครับ คิดว่าใครที่อ่านมาถึงตรงนี้น่าจะได้อะไรติดไม้ติดมือกลับไปบ้าง ผมกำลังสงสัยว่าภาษาอื่นๆ น่าจะมีเรื่องนี้เหมือนกัน ซึ่งต้องบอกว่า เวลา เนี่ยเป็นหนึ่งในเรื่องที่ยากของ Computer เลยครับและถ้าเราไม่ระวัง เราจะเจอบั๊กแปลกๆ ที่ชอบมาในเวลาแปลกๆ อย่างสิ้นปีได้ ถ้าชอบบล็อกแนวนี้ก็ฝากกดไลค์ กดแชร์ หรือคอมเมนต์ด้วยนะครับ แล้วเจอกันใหม่บล็อกหน้าครับ</p>
<h2 id="reference"><strong>Reference</strong></h2>
<ul>
<li>Chapter 12 - <a href="https://www.manning.com/books/modern-java-in-action">manning.com/books/modern-java-in-action</a></li>
<li><a href="https://www.baeldung.com/java-datetimeformatter">baeldung.com/java-datetimeformatter</a></li>
<li><a href="https://stackoverflow.com/questions/26431882/difference-between-year-of-era-and-week-based-year">stackoverflow.com/questions/26431882/differ..</a></li>
<li><a href="https://stackoverflow.com/questions/41177442/uuuu-versus-yyyy-in-datetimeformatter-formatting-pattern-codes-in-java">stackoverflow.com/questions/41177442/uuuu-v..</a></li>
</ul>
]]></content:encoded></item><item><title>Spring ก็เร็วได้! มาเรียก Method ใน Spring แบบ Parallel ด้วย @Async กันเถอะ</title><link>https://yothinix-com.pages.dev/spring-kerwaid-maaeriiyk-method-ain-spring-aebb-parallel-dwy-async-kaneth-a/</link><pubDate>Tue, 12 Jan 2021 12:41:00 +0700</pubDate><guid>https://yothinix-com.pages.dev/spring-kerwaid-maaeriiyk-method-ain-spring-aebb-parallel-dwy-async-kaneth-a/</guid><description>&lt;p&gt;สมมติว่าเรามีโจทย์ที่ต้องยิง HTTP Request เพื่อดึง resource มาจาก Web service อื่นหลายๆ ที่ ถ้าเราไม่คิดอะไรเราก็อาจจะเขียนโค้ดให้ ยิงทีละตัวแบบ Synchronous ไปได้ แต่ถ้าแต่ละ Request ไม่ได้ขึ้นต่อกัน เราจะมีตัวเลือกมากขึ้นผ่านการยิงแบบ Asynchronous ครับ ซึ่งผมเพิ่งรู้ว่า Java มันทำได้&lt;/p&gt;
&lt;h2 id="example"&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;สมมติว่าเรามี Service ที่ไว้ยิง HTTP Request ไปที่ API อื่นแบบง่ายๆ หน้าตาประมาณนี้&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@Service&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ActivityService&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RestTemplate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;RestTemplate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;public&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ResponseSecond&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;getEndpointOne&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#34;http://localhost:8000/1&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ResponseSecond&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;restTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getForObject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ResponseSecond&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;และมี Controller ที่เรียกใช้ Service นี้หน้าตาประมาณนี้&lt;/p&gt;</description><content:encoded><![CDATA[<p>สมมติว่าเรามีโจทย์ที่ต้องยิง HTTP Request เพื่อดึง resource มาจาก Web service อื่นหลายๆ ที่ ถ้าเราไม่คิดอะไรเราก็อาจจะเขียนโค้ดให้ ยิงทีละตัวแบบ Synchronous ไปได้ แต่ถ้าแต่ละ Request ไม่ได้ขึ้นต่อกัน เราจะมีตัวเลือกมากขึ้นผ่านการยิงแบบ Asynchronous ครับ ซึ่งผมเพิ่งรู้ว่า Java มันทำได้</p>
<h2 id="example"><strong>Example</strong></h2>
<p>สมมติว่าเรามี Service ที่ไว้ยิง HTTP Request ไปที่ API อื่นแบบง่ายๆ หน้าตาประมาณนี้</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Service</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">ActivityService</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">RestTemplate</span><span class="w"> </span><span class="n">restTemplate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">RestTemplate</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">ResponseSecond</span><span class="w"> </span><span class="nf">getEndpointOne</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;http://localhost:8000/1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">ResponseSecond</span><span class="w"> </span><span class="n">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">restTemplate</span><span class="p">.</span><span class="na">getForObject</span><span class="p">(</span><span class="n">url</span><span class="p">,</span><span class="w"> </span><span class="n">ResponseSecond</span><span class="p">.</span><span class="na">class</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">response</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>และมี Controller ที่เรียกใช้ Service นี้หน้าตาประมาณนี้</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@RestController</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">ActivityController</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Autowired</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ActivityService</span><span class="w"> </span><span class="n">activityService</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@GetMapping</span><span class="p">(</span><span class="n">path</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;/&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">getActivity</span><span class="p">()</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">Throwable</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">ResponseSecond</span><span class="w"> </span><span class="n">one</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">activityService</span><span class="p">.</span><span class="na">getEndpointOne</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">ResponseSecond</span><span class="w"> </span><span class="n">two</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">activityService</span><span class="p">.</span><span class="na">getEndpointOne</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">ResponseSecond</span><span class="w"> </span><span class="n">three</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">activityService</span><span class="p">.</span><span class="na">getEndpointOne</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">String</span><span class="w"> </span><span class="n">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">one</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">two</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">three</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="n">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">one</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34; &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">two</span><span class="p">.</span><span class="na">getMessage</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34; &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">three</span><span class="p">.</span><span class="na">getMessage</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">ok</span><span class="p">(</span><span class="n">response</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">Exception</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="k">throw</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="na">getCause</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>ก็ดูทำงานได้ปกติใช่มั้ยครับ แต่สิ่งที่เกิดคือ มันจะทำงานแบบ Synchronous ก็คือ getEndpointOne แต่ละตัวต้องถูกทำให้เสร็จก่อนที่จะเรียกตัวถัดไป ซึ่งสมมติว่าแต่ละ Endpoint ต้องทำงาน 1 วิ นั่นคือ 3 วิเลยกว่า controller นี้จะวิ่งจนครบ loop</p>
<h2 id="ทำใหมน-async-เถอะ"><strong>ทำให้มัน Async เถอะ</strong></h2>
<p>อย่างแรกที่เราต้องแก้คือ Service ครับ โดยเราจะใช้ความสามารถของ Annotation ชื่อ <a href="https://hashnode.com/@Async">@Async</a> มาทำให้ Method ของเรานั้นสามารถที่จะเรียกแบบ Asynchronous ได้ แต่มีข้อแม้สองข้อครับคือ</p>
<ul>
<li>Method ที่เราจะทำให้เป็น Async นั้นต้องเป็น public method</li>
<li>Method ที่เรียกใช้งาน Async Method นั้นต้องอยู่คนละ Class กันครับ เพราะ Async ทำ self-invocation ไม่ได้</li>
</ul>
<p>พอรู้แบบนี้แล้ว ขั้นแรกคือเราต้องแปลง method ที่เราจะทำให้มันเป็น async ให้หน้าตาประมาณนี้ครับ</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Async</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">ResponseSecond</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">getEndpointOne</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">String</span><span class="w"> </span><span class="n">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;http://localhost:8000/1&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">ResponseSecond</span><span class="w"> </span><span class="n">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">restTemplate</span><span class="p">.</span><span class="na">getForObject</span><span class="p">(</span><span class="n">url</span><span class="p">,</span><span class="w"> </span><span class="n">ResponseSecond</span><span class="p">.</span><span class="na">class</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">CompletableFuture</span><span class="p">.</span><span class="na">completedFuture</span><span class="p">(</span><span class="n">response</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>จะเห็นว่ามีสิ่งที่เพิ่มขึ้นมา 3 อย่างคือ</p>
<ul>
<li><a href="https://hashnode.com/@Async">@Async</a> ที่เป็นการระบุว่า Method นี้สามารถเรียกใช้แบบ Asynchronous ได้</li>
<li>Return type signature จะถูกครอบด้วย <code>CompletableFuture&lt;T&gt;</code> ครับ</li>
<li>Return value จะต้องเพิ่ม <code>CompletableFuture.completedFuture()</code> ครอบไปครับ</li>
</ul>
<p>ในส่วนของ method ที่เรียกใช้งาน Async method นั้นจะเปลี่ยนไปหน้าตาประมาณนี้ครับ</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="o">&lt;</span><span class="n">String</span><span class="o">&gt;</span><span class="w"> </span><span class="nf">getAsync</span><span class="p">()</span><span class="w"> </span><span class="kd">throws</span><span class="w"> </span><span class="n">Throwable</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">try</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">ResponseSecond</span><span class="o">&gt;</span><span class="w"> </span><span class="n">one</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">activityService</span><span class="p">.</span><span class="na">getEndpointOne</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">ResponseSecond</span><span class="o">&gt;</span><span class="w"> </span><span class="n">two</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">activityService</span><span class="p">.</span><span class="na">getEndpointOne</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">CompletableFuture</span><span class="o">&lt;</span><span class="n">ResponseSecond</span><span class="o">&gt;</span><span class="w"> </span><span class="n">three</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">activityService</span><span class="p">.</span><span class="na">getEndpointOne</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">CompletableFuture</span><span class="p">.</span><span class="na">allOf</span><span class="p">(</span><span class="n">one</span><span class="p">,</span><span class="w"> </span><span class="n">two</span><span class="p">,</span><span class="w"> </span><span class="n">three</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">String</span><span class="w"> </span><span class="n">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">&#34;&#34;</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">one</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">two</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span><span class="n">three</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">            </span><span class="n">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">one</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">getMessage</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34; &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">two</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">getMessage</span><span class="p">()</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="s">&#34; &#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">three</span><span class="p">.</span><span class="na">get</span><span class="p">().</span><span class="na">getMessage</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">ResponseEntity</span><span class="p">.</span><span class="na">ok</span><span class="p">(</span><span class="n">response</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w"> </span><span class="k">catch</span><span class="w"> </span><span class="p">(</span><span class="n">Exception</span><span class="w"> </span><span class="n">e</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">throw</span><span class="w"> </span><span class="n">e</span><span class="p">.</span><span class="na">getCause</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>จะเห็นว่าในส่วนที่เรียก method จะเปลี่ยน return type เป็น <code>CompletableFuture&lt;T&gt;</code> แทนครับ และจะมีโค้ดเพิ่มขึ้นมาคือ <code>CompletableFuture.allOf(&lt;async method&gt;)</code> ซึ่งจะทำหน้าที่เรียกและรอ return future value ให้เราจนกว่า async method ที่เรียกข้างในทำงานเสร็จทั้งหมดครับ ซึ่งหลังจากนั้นเราสามารถใช้ method get() เรียก CompletableFuture เพื่อดึงค่าออกมาจาก async method ได้เลย</p>
<p>แต่ถ้าเราเรียกตอนนี้จะเห็นว่ามันยังเรียกแต่ละ method แบบ sychronous กันอยู่ทำให้ใช้เวลารวมทั้งหมดอยู่ 3 วิ (ถ้าแต่ละ method ใช้ 1 วิ) ซึ่งเกิดจากการที่ Executor ของเรายังเป็น applicationTaskExecutor ซึ่งรันอยู่บน single thread อยู่ครับ โดยเราสามารถเปลี่ยนไปใช้ Executor แบบ Async โดยการเพิ่ม Async Config ตามตัวอย่างข้างล่างได้ครับ</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Configuration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@EnableAsync</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">AsyncConfig</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Bean</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">Executor</span><span class="w"> </span><span class="nf">asyncExecutor</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">ThreadPoolTaskExecutor</span><span class="w"> </span><span class="n">executor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ThreadPoolTaskExecutor</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">executor</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>ซึ่งพอถึงจุดนี้ใน Service ของเราก็จะทำงานผ่าน asyncExecutor แล้วครับ แต่ถ้าเราสั่งให้ controller เราทำงานจะเห็นว่ามันยังทำงานอยู่ที่ 3 วิ อยู่ ทำไมมันยังไม่เร็วขึ้นนะ?</p>
<h2 id="corepoolsize-vs-maxpoolsize"><strong>corePoolSize vs maxPoolSize</strong></h2>
<p>สาเหตุที่มันยังทำงานแบบ synchronous อยู่ทั้งๆ ที่เราใช้ asyncExecutor เป็นเพราะว่ามันยังใช้ Thread เดียวอยู่ครับ ซึ่งถ้าเราไปดู Implementation ของ ThreadPoolTaskExecutor จะเห็นว่าเซต corePoolSize อยู่ที่ 1 เลยทำให้มันเป็น single thread แต่เราสามารถเพิ่มจำนวน thread ที่ stand by อยู่ได้ผ่าน method: <strong>setCorePoolSize</strong> ได้ โดยในที่นี้จะเซตเป็น 3 เท่าจำนวน tasks ที่เราจะแยก Thread ออกสูงสุดครับ</p>
<p>COPY</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Configuration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@EnableAsync</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">AsyncConfig</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Bean</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">Executor</span><span class="w"> </span><span class="nf">asyncExecutor</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">ThreadPoolTaskExecutor</span><span class="w"> </span><span class="n">executor</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">ThreadPoolTaskExecutor</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">executor</span><span class="p">.</span><span class="na">setCorePoolSize</span><span class="p">(</span><span class="n">3</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">executor</span><span class="p">.</span><span class="na">setMaxPoolSize</span><span class="p">(</span><span class="n">5</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">executor</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>อีกส่วนคือ maxPoolSize ซึ่งเป็นตัวกำหนดว่า Java จะแตก Thread ได้มากที่สุดกี่ตัวครับ ซึ่ง default จะเป็นเท่ากับจำนวน Integer เลย ซึ่งเราควรจะกำหนดไว้เพื่อกันไม่ให้ Java แตก Thread ออกมามากเกินไปครับ ในที่นี้จะกำหนดเป็น 5</p>
<p>ซึ่งพอถึงจุดนี้ตัว Controller นี้จะทำงานอยู่ที่ประมาณ 1 วิ + นิดหน่อยครับ แล้วถ้าเราดูที่ service ปลายทางได้เราจะเห็นว่ามันยิง HTTP Request ออกมาแทบจะเวลาเดียวกันแล้วครับ</p>
<h2 id="reference"><strong>Reference</strong></h2>
<ul>
<li>เริ่มที่บล็อกนี้ได้ จะพาทัวร์ CompletableFuture API แบบง่ายๆ แต่ถ้าพูดถึง Thread Pool ไว้สั้นมาก จนอาจจะเผลอเลื่อนเลยไป <a href="https://medium.com/sipios/how-to-make-parallel-calls-in-java-springboot-application-and-how-to-test-them-dcc27318a0cf">https://medium.com/sipios/how-to-make-parallel-calls-in-java-springboot-application-and-how-to-test-them-dcc27318a0cf</a></li>
<li>บล็อกนี้พาทัวร์ CompletableFutureAPI แบบ Advance กว่าลิ้งค์ข้างบน <a href="https://www.baeldung.com/java-completablefuture">https://www.baeldung.com/java-completablefuture</a></li>
<li>บล็อกนี้จะคล้ายๆ ลิ้งค์แรก แต่มีสิ่งที่เลอค่าตรง Limitation ของ <a href="https://hashnode.com/@Async">@Async</a> ที่สองบล็อกก่อนหน้าไม่ได้พูดถึง <a href="https://www.baeldung.com/spring-async">https://www.baeldung.com/spring-async</a></li>
<li>บล็อกนี้พาไปทัวร์ Future API (เข้าใจว่าโบราณกว่า CompletableFuture) แต่จุดที่น่าสนใจคือพาเรามารู้จัก Thread Pools นี่แหละ <a href="https://www.baeldung.com/java-future">https://www.baeldung.com/java-future</a></li>
<li>บล็อกนี้อธิบายว่าเราควรเลือกใช้ corePoolSize กับ maxPoolSize ยังไง <a href="https://www.baeldung.com/java-threadpooltaskexecutor-core-vs-max-poolsize">https://www.baeldung.com/java-threadpooltaskexecutor-core-vs-max-poolsize</a></li>
</ul>
]]></content:encoded></item><item><title>แชร์ประสบการณ์ Config ตัว Amazon ElastiCache ให้ใช้งานกับ Spring Boot</title><link>https://yothinix-com.pages.dev/spring-boot-with-aws-elasticache/</link><pubDate>Thu, 24 Dec 2020 12:38:00 +0700</pubDate><guid>https://yothinix-com.pages.dev/spring-boot-with-aws-elasticache/</guid><description>&lt;h2 id="story"&gt;&lt;strong&gt;Story&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;2 - 3 วันที่ผ่านมาพยายามต่อ Redis บน ElastiCache แล้ว Error ตลอดเวลาเรียกใช้ จะมี Log ขึ้นประมาณข้างล่างนี้ แล้วแก้จนมันใช้ได้แล้วเอาขึ้น Server ไปละ นี่คือเรื่องของมัน&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;org&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;springframework&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RedisConnectionFailureException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Unable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;nested&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lettuce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RedisConnectionException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Unable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;███████&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;███████&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;6379&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Caused&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;lettuce&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;core&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;RedisConnectionException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Unable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connect&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;to&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;███████&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;███████&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;6379&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Caused&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;javax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SSLHandshakeException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;General&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SSLEngine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Caused&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;javax&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;net&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;SSLHandshakeException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;General&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SSLEngine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;problem&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;Caused&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;java&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;security&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;cert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;CertificateException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;No&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alternative&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DNS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;matching&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;███████&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="err"&gt;███████&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;com&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;found&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;สันนิษฐานแรกคือน่าจะเกี่ยวกับ In-Transit Encryption feature ของ ElastiCache&amp;rsquo;s Redis ซึ่งถูกเลือกไว้แล้ว แล้วหลังจาก Research ไปซักระยะเลยเจอว่า ElastiCache&amp;rsquo;s Redis allow ให้ Connect ผ่าน VPC เท่านั้น&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="story"><strong>Story</strong></h2>
<p>2 - 3 วันที่ผ่านมาพยายามต่อ Redis บน ElastiCache แล้ว Error ตลอดเวลาเรียกใช้ จะมี Log ขึ้นประมาณข้างล่างนี้ แล้วแก้จนมันใช้ได้แล้วเอาขึ้น Server ไปละ นี่คือเรื่องของมัน</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">org</span><span class="p">.</span><span class="na">springframework</span><span class="p">.</span><span class="na">data</span><span class="p">.</span><span class="na">redis</span><span class="p">.</span><span class="na">RedisConnectionFailureException</span><span class="p">:</span><span class="w"> </span><span class="n">Unable</span><span class="w"> </span><span class="kd">to</span><span class="w"> </span><span class="n">connect</span><span class="w"> </span><span class="kd">to</span><span class="w"> </span><span class="n">Redis</span><span class="p">;</span><span class="w"> </span><span class="n">nested</span><span class="w"> </span><span class="n">exception</span><span class="w"> </span><span class="n">is</span><span class="w"> </span><span class="n">io</span><span class="p">.</span><span class="na">lettuce</span><span class="p">.</span><span class="na">core</span><span class="p">.</span><span class="na">RedisConnectionException</span><span class="p">:</span><span class="w"> </span><span class="n">Unable</span><span class="w"> </span><span class="kd">to</span><span class="w"> </span><span class="n">connect</span><span class="w"> </span><span class="kd">to</span><span class="w"> </span><span class="err">███████</span><span class="o">-</span><span class="n">cache</span><span class="p">.</span><span class="err">███████</span><span class="p">.</span><span class="na">com</span><span class="p">:</span><span class="n">6379</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">Caused</span><span class="w"> </span><span class="n">by</span><span class="p">:</span><span class="w"> </span><span class="n">io</span><span class="p">.</span><span class="na">lettuce</span><span class="p">.</span><span class="na">core</span><span class="p">.</span><span class="na">RedisConnectionException</span><span class="p">:</span><span class="w"> </span><span class="n">Unable</span><span class="w"> </span><span class="kd">to</span><span class="w"> </span><span class="n">connect</span><span class="w"> </span><span class="kd">to</span><span class="w"> </span><span class="err">███████</span><span class="o">-</span><span class="n">cache</span><span class="p">.</span><span class="err">███████</span><span class="p">.</span><span class="na">com</span><span class="p">:</span><span class="n">6379</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">e</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">Caused</span><span class="w"> </span><span class="n">by</span><span class="p">:</span><span class="w"> </span><span class="n">javax</span><span class="p">.</span><span class="na">net</span><span class="p">.</span><span class="na">ssl</span><span class="p">.</span><span class="na">SSLHandshakeException</span><span class="p">:</span><span class="w"> </span><span class="n">General</span><span class="w"> </span><span class="n">SSLEngine</span><span class="w"> </span><span class="n">problem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">Caused</span><span class="w"> </span><span class="n">by</span><span class="p">:</span><span class="w"> </span><span class="n">javax</span><span class="p">.</span><span class="na">net</span><span class="p">.</span><span class="na">ssl</span><span class="p">.</span><span class="na">SSLHandshakeException</span><span class="p">:</span><span class="w"> </span><span class="n">General</span><span class="w"> </span><span class="n">SSLEngine</span><span class="w"> </span><span class="n">problem</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">Caused</span><span class="w"> </span><span class="n">by</span><span class="p">:</span><span class="w"> </span><span class="n">java</span><span class="p">.</span><span class="na">security</span><span class="p">.</span><span class="na">cert</span><span class="p">.</span><span class="na">CertificateException</span><span class="p">:</span><span class="w"> </span><span class="n">No</span><span class="w"> </span><span class="n">subject</span><span class="w"> </span><span class="n">alternative</span><span class="w"> </span><span class="n">DNS</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="n">matching</span><span class="w"> </span><span class="err">███████</span><span class="o">-</span><span class="n">cache</span><span class="p">.</span><span class="err">███████</span><span class="p">.</span><span class="na">com</span><span class="w"> </span><span class="n">found</span><span class="p">.</span><span class="w">
</span></span></span></code></pre></div><p>สันนิษฐานแรกคือน่าจะเกี่ยวกับ In-Transit Encryption feature ของ ElastiCache&rsquo;s Redis ซึ่งถูกเลือกไว้แล้ว แล้วหลังจาก Research ไปซักระยะเลยเจอว่า ElastiCache&rsquo;s Redis allow ให้ Connect ผ่าน VPC เท่านั้น</p>
<p>Amazon ElastiCache Nodes, deployed within a VPC, can never be accessed from the Internet or from EC2 Instances outside the VPC. <a href="https://stackoverflow.com/questions/32771859/how-to-connect-to-aws-elasticache?rq=1">Source</a></p>
<p>ระหว่างนั้นเลยไปทดลองสร้างเครื่อง EC2 ในวง VPC เดียวกันมา Connect แต่ก็เจอปัญหา Error: Connection reset by peer เวลาต่อจาก redis-cli ที่วิ่งอยู่บน Stunnel แล้ว ซึ่งแก้เท่าไรก็ยังไม่ได้ และไม่ได้เกี่ยวกับ IAM Role ด้วยเพราะลอง <em>FullAccessElastiCache</em> ก็ยังไม่ได้</p>
<p>สุดท้ายพี่กานต์ลองใช้ Python Redis client ยิงดูปรากฏว่าติด หน้าตาโค้ดประมาณนี้ เลยรู้สึกว่าไม่ใช่ละ</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">redis</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Redis</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="s1">&#39;███████-cache.███████.com&#39;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">6379</span><span class="p">,</span> <span class="n">db</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">ssl</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">r</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">&#39;foo&#39;</span><span class="p">,</span> <span class="s1">&#39;bar&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">r</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;foo&#39;</span><span class="p">))</span>
</span></span></code></pre></div><p>ซึ่งมีจุดที่ remind อย่างนึงคือ <code>ssl=True</code> เลยพุ่งเป้ากลับมาที่เรื่อง SSL Certificate น่าจะมีปัญหาอะไรบางอย่าง หลังจากนั้นพยายามลองเซต <code>spring.redis.ssl=true</code> ใน <a href="http://application.properties/">application.properties</a> ก็ยังไม่ได้ขึ้น Exception เหมือนตอนแรก ก็เลยมานั่งมอง Exception ละเอียดๆ อีกทีจนไปสะดุดตาอันสุดท้าย</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">Caused</span><span class="w"> </span><span class="n">by</span><span class="p">:</span><span class="w"> </span><span class="n">java</span><span class="p">.</span><span class="na">security</span><span class="p">.</span><span class="na">cert</span><span class="p">.</span><span class="na">CertificateException</span><span class="p">:</span><span class="w"> </span><span class="n">No</span><span class="w"> </span><span class="n">subject</span><span class="w"> </span><span class="n">alternative</span><span class="w"> </span><span class="n">DNS</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="n">matching</span><span class="w"> </span><span class="err">███████</span><span class="o">-</span><span class="n">cache</span><span class="p">.</span><span class="err">███████</span><span class="p">.</span><span class="na">com</span><span class="w"> </span><span class="n">found</span><span class="p">.</span><span class="w">
</span></span></span></code></pre></div><p>Search ไปๆ มาๆ เลยมาเจอบล็อกนี้ <a href="https://medium.com/@sajithekanayaka/solved-java-security-cert-certificateexception-no-subject-alternative-names-present-eec1669faf0d">[Solved] java.security.cert.CertificateException: No subject alternative names present</a> ใจความหลักๆ เหมือนกันคือต่อ LDAP ไม่ได้ เพราะ Java 1.8.0_181 เปิด &ldquo;Endpoint identification&rdquo; เป็น default เพื่อให้ใช้กับ LDAPS connection ได้ ซึ่งก็มีวิธีปิดอยู่ง่ายๆ คือไปเพิ่ม property ข้างล่างก็ได้ละ</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="o">-</span><span class="n">Dcom</span><span class="p">.</span><span class="na">sun</span><span class="p">.</span><span class="na">jndi</span><span class="p">.</span><span class="na">ldap</span><span class="p">.</span><span class="na">object</span><span class="p">.</span><span class="na">disableEndpointIdentification</span><span class="o">=</span><span class="kc">true</span><span class="w">
</span></span></span></code></pre></div><p>ซึ่งถึงแม้มันจะไม่ได้แก้ปัญหาของ ElastiCache ต่อไม่ได้ตรงๆ แต่ได้ keyword สำคัญมากคือ <code>disableEndpointIdentification</code> ลิ้งค์ต่อๆ มาเลยยังพุ่งเป้าไปที่เรื่องนี้อยู่ แล้วมาเจอกับ issues นี้ใน github <a href="https://github.com/redisson/redisson/issues/1296">CertificateException while connecting to Azure redis cluster. #1296</a> ซึ่งเจอกับ Redis เต็มๆ แต่เป็น Azure แต่ที่สำคัญคือ อันนี้เป็นปัญหาบน Library: Redisson ซึ่งเป็น Redis Java Client เหมือนกัน และก็ยิ่งย้ำความเชื่อว่ามันเป็นเรื่อง EndpointIdentification นี่แหละ</p>
<p>ณ จุดนี้คือกลับมานั่งทำความเข้าใจอีกทีว่า spring-data-redis ใช้อะไรอยู่ข้างล่าง จาก <a href="https://docs.spring.io/spring-data/redis/docs/current/reference/html/#redis:connectors:connection">Official Document</a> เลยค้นพบว่าข้างล่างมันใช้ Lettuce เป็น Client อยู่ เลยมาเจอ issue <a href="https://github.com/lettuce-io/lettuce-core/issues/1454">Lettuce can&rsquo;t connect to Redis Cluster + SSL but can connect to same Redis server + SSL if treated as Standalone node. #1454</a> ซึ่งคำตอบแรกของ issue นี้ คือคำตอบเลย</p>
<p>There are a couple of things you can do about:</p>
<ol>
<li>If the Redis IP addresses can be mapped onto a hostname, then please configure a MappingSocketAddressResolver through ClientResources to map IP addresses into hostnames that are listed in the SSL certificate</li>
<li>Reach out to the Azure support team so that they fix the SSL certificates and include all cluster IP addresses</li>
<li>(least favorable option) Disable SSL certificate validation. That can be done with RedisURI.setVerifyPeer(false) when using Lettuce directly. For Spring, you should be able to register a LettuceClientConfigurationBuilder customizer to invoke LettuceSslClientConfigurationBuilder.disablePeerVerification().</li>
</ol>
<p>ซึ่งทำให้เราได้คีย์เวิร์คสำคัญมาคือ <code>disablePeerVerification</code> (มันต้องเหมือนข้างบนแน่ๆ) คำถามถัดมาคือ เราจะ disable มันยังไงใน Spring เลยต้องไปหาวิธีใช้ LettuceSslClientConfigurationBuilder อยู่ซักพักใหญ่ๆ เลย ซึ่งอ่าน <a href="https://lettuce.io/core/release/reference/">Doc</a> เฉยๆ ก็เขียนยังไม่เป็นด้วยประสบการณ์ Java แค่นี้</p>
<p>สุดท้ายเลยไปเริ่มจากตัวอย่างของ repository นี้ <a href="https://github.com/cristianprofile/spring-data-redis-lettuce">spring-data-redis-lettuce</a> ซึ่งยกตัวอย่าง custom LettuceConnectionFactory ไว้ได้โอเคเลย แล้วทำให้เริ่มจากตรงนี้จนมาเป็น ลองผิดลองถูก อ่าน Document ของ LettuceClientConfigurationBuilder จนได้โค้ดของ configuration ข้างล่างนี้</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.beans.factory.annotation.Value</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.context.annotation.Bean</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.context.annotation.Configuration</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.data.redis.connection.RedisStandaloneConfiguration</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kn">import</span><span class="w"> </span><span class="nn">org.springframework.data.redis.core.StringRedisTemplate</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nd">@Configuration</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="kd">class</span> <span class="nc">RedisClientConfig</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Value</span><span class="p">(</span><span class="s">&#34;${spring.redis.host}&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="n">String</span><span class="w"> </span><span class="n">redisHost</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Value</span><span class="p">(</span><span class="s">&#34;${spring.redis.port}&#34;</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">private</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="n">redisPort</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Bean</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">public</span><span class="w"> </span><span class="n">StringRedisTemplate</span><span class="w"> </span><span class="nf">redisTemplate</span><span class="p">(</span><span class="n">LettuceConnectionFactory</span><span class="w"> </span><span class="n">lettuceConnectionFactory</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="kd">final</span><span class="w"> </span><span class="n">StringRedisTemplate</span><span class="w"> </span><span class="n">template</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">StringRedisTemplate</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">template</span><span class="p">.</span><span class="na">setConnectionFactory</span><span class="p">(</span><span class="n">lettuceConnectionFactory</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">template</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nd">@Bean</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">LettuceConnectionFactory</span><span class="w"> </span><span class="nf">lettuceConnectionFactory</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">LettuceClientConfiguration</span><span class="w"> </span><span class="n">clientConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">LettuceClientConfiguration</span><span class="p">.</span><span class="na">builder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">.</span><span class="na">useSsl</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">.</span><span class="na">disablePeerVerification</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">                </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="n">RedisStandaloneConfiguration</span><span class="w"> </span><span class="n">serverConfig</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">RedisStandaloneConfiguration</span><span class="p">(</span><span class="n">redisHost</span><span class="p">,</span><span class="w"> </span><span class="n">redisPort</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">LettuceConnectionFactory</span><span class="p">(</span><span class="n">serverConfig</span><span class="p">,</span><span class="w"> </span><span class="n">clientConfig</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>แล้วใน <a href="http://application.properties/">application.properties</a> จะเหลือแค่</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">spring</span><span class="p">.</span><span class="na">cache</span><span class="p">.</span><span class="na">type</span><span class="o">=</span><span class="n">redis</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">spring</span><span class="p">.</span><span class="na">redis</span><span class="p">.</span><span class="na">host</span><span class="o">=</span><span class="err">███████</span><span class="o">-</span><span class="n">cache</span><span class="p">.</span><span class="err">███████</span><span class="p">.</span><span class="na">com</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">spring</span><span class="p">.</span><span class="na">redis</span><span class="p">.</span><span class="na">port</span><span class="o">=</span><span class="n">6379</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">spring</span><span class="p">.</span><span class="na">cache</span><span class="p">.</span><span class="na">redis</span><span class="p">.</span><span class="na">time</span><span class="o">-</span><span class="kd">to</span><span class="o">-</span><span class="n">live</span><span class="o">=</span><span class="n">18000</span><span class="w">
</span></span></span></code></pre></div><h2 id="resources"><strong>Resources</strong></h2>
<ul>
<li>Official documentation ของ AWS ใช้ต่อ Redis ใน ElastiCache</li>
<li><a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/GettingStarted.ConnectToCacheNode.html">Connect to a Cluster&rsquo;s Node</a> a.k.a. วิธี Build redis-cli</li>
<li><a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/red-ug/in-transit-encryption.html#connect-tls">ElastiCache for Redis In-Transit Encryption (TLS)</a> a.k.a. วิธีเซต Stunnel</li>
<li><a href="https://aws.amazon.com/premiumsupport/knowledge-center/elasticache-connect-redis-node/">How do I connect to an Amazon ElastiCache In-Transit encryption-enabled Redis node using redis-cli?</a> a.k.a. วิธีเซต Stunnel</li>
<li>สองบล็อกนี้แนะนำให้ไปใช้ stunnel วิธีเดียวกับ AWS Official ถ้าจะต่อเข้าไป Redis ใน ElastiCache</li>
<li><a href="https://kevinhakanson.com/2018-06-27-making-a-secure-connection-to-elasticache-redis">Making a secure connection to ElastiCache (Redis)</a></li>
<li><a href="https://datanextsolutions.com/blog/how-to-fix-redis-cli-error-connection-reset-by-peer/">How to Fix Redis CLI Error Connection Reset by Peer</a></li>
<li>Example how to config RedisConnectionFactory (<a href="https://github.com/agrawald/spring-redis-ssl/blob/master/src/main/java/au/com/mayi/config/AppConfig.java">link</a>)</li>
<li>Spring Official Document: Configuring the Lettuce Connector (<a href="https://docs.spring.io/spring-data/redis/docs/current/reference/html/#redis:connectors:lettuce">link</a>)</li>
<li><a href="https://github.com/lettuce-io/lettuce-core/issues/1454">Lettuce can&rsquo;t connect to Redis Cluster + SSL but can connect to same Redis server + SSL if treated as Standalone node. #1454</a> a.k.a. Issue ที่เป็นคำตอบ</li>
<li><a href="https://github.com/cristianprofile/spring-data-redis-lettuce">spring-data-redis-lettuce</a> a.k.a. ตัวอย่าง Implement Custom LettuceConnection</li>
<li><a href="https://lettuce.io/core/release/reference/#ssl.hostpeer-verification">Lettuce Host/Peer Verification</a> a.k.a. วิธีใช้ Builder กับ Peer verification</li>
<li><a href="https://lettuce.io/core/release/reference/#ssl">Lettuce SSL</a> มาจากตรงนี้ก่อน</li>
</ul>
]]></content:encoded></item><item><title>บันทึกวิธีใช้ liquibase ทำ database migration ให้ Spring boot</title><link>https://yothinix-com.pages.dev/liquibase-spring-boot-setup/</link><pubDate>Tue, 27 Oct 2020 14:26:00 +0700</pubDate><guid>https://yothinix-com.pages.dev/liquibase-spring-boot-setup/</guid><description>&lt;h2 id="installation"&gt;&lt;strong&gt;Installation&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;ลง Liquibase ใน pom.xml&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;dependencies&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;dependency&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;groupId&amp;gt;org.liquibase&amp;lt;/groupId&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;artifactId&amp;gt;liquibase-core&amp;lt;/artifactId&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;version&amp;gt;3.6.2&amp;lt;/version&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &amp;lt;/dependency&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&amp;lt;/dependencies&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;เพิ่ม configuration ให้ liquibase ใน &lt;code&gt;application.properties&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; ## Liquibase
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; spring.liquibase.enabled=true
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; spring.liquibase.parameters.EnumDataType=ENUM(&amp;#39;VALUE1&amp;#39;, &amp;#39;VALUE2&amp;#39;, &amp;#39;VALUE3&amp;#39;)
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; spring.liquibase.contexts=local
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ตัวสำคัญคือ &lt;code&gt;spring.liquibase.contexts&lt;/code&gt; ตัวนี้เหมือนคอยบอกว่าเรารัน Spring โหมดอะไรอยู่ แล้วทำให้เราใช้ connection database ที่เราเซตไว้ใน spring ได้ด้วย&lt;/p&gt;
&lt;p&gt;อีกจุดนึงคือ &lt;code&gt;spring.liquibase.parameters&lt;/code&gt; อันนี้เป็น custom parameter ที่เราเอาไว้ใช้ใน Changeset ซึ่งบางครั้งของ database คนละแบบจะต่างกันอย่างเช่น type ENUM ของ H2 เก็บเป็น enum ได้เลย แต่ PostgreSQL ตัว spring จะแปลงเป็น INT เลยต้องมาประกาศไว้ด้วย&lt;/p&gt;
&lt;h2 id="usage"&gt;&lt;strong&gt;Usage&lt;/strong&gt;&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;เพิ่มไฟล์ ChangeLog master ไฟล์นี้เป็นเหมือนไฟล์หลักไว้บอกว่ามี migration step อะไรบ้างผ่าน property ชื่อ &lt;code&gt;include.file&lt;/code&gt; ไฟล์กับชื่อจะอยู่ที่ &lt;code&gt;/resources/db/changelog/db.changelog-master.yaml&lt;/code&gt; ตัวนี้สามารถตั้งค่าให้ไปอยู่ที่อื่น ชื่ออื่นได้ แต่อันนี้คือชื่อ default หน้าตาของไฟล์จะประมาณ&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-fallback" data-lang="fallback"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; databaseChangeLog:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; - include:
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; file: db/changelog/001-add-user-table.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;สร้าง ChangeSet ตัวนี้ไว้บอกว่าใน migration นี้มี change อะไรเกี่ยวกับ database บ้าง ตัวอย่างเช่นของ user&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-gdscript3" data-lang="gdscript3"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;databaseChangeLog&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;changeSet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;001&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;man&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;changes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;createTable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;columns&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;primaryKeyName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_pkey&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UUID&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;nullable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;false&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;type&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;$&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;EnumDataType&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;is_admin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BOOLEAN&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;column&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TIMESTAMP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;tableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;ใน changeSet ต้องบอกว่ามันเป็น changeSet ซึ่งมี property หลักๆ ที่ใช้คือ&lt;/p&gt;</description><content:encoded><![CDATA[<h2 id="installation"><strong>Installation</strong></h2>
<ol>
<li>ลง Liquibase ใน pom.xml</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">&lt;dependencies&gt;
</span></span><span class="line"><span class="cl">    &lt;dependency&gt;
</span></span><span class="line"><span class="cl">        &lt;groupId&gt;org.liquibase&lt;/groupId&gt;
</span></span><span class="line"><span class="cl">        &lt;artifactId&gt;liquibase-core&lt;/artifactId&gt;
</span></span><span class="line"><span class="cl">        &lt;version&gt;3.6.2&lt;/version&gt;
</span></span><span class="line"><span class="cl">    &lt;/dependency&gt;
</span></span><span class="line"><span class="cl">&lt;/dependencies&gt;
</span></span></code></pre></div><p>เพิ่ม configuration ให้ liquibase ใน <code>application.properties</code></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl"> ## Liquibase
</span></span><span class="line"><span class="cl"> spring.liquibase.enabled=true
</span></span><span class="line"><span class="cl"> spring.liquibase.parameters.EnumDataType=ENUM(&#39;VALUE1&#39;, &#39;VALUE2&#39;, &#39;VALUE3&#39;)
</span></span><span class="line"><span class="cl"> spring.liquibase.contexts=local
</span></span></code></pre></div><p>ตัวสำคัญคือ <code>spring.liquibase.contexts</code> ตัวนี้เหมือนคอยบอกว่าเรารัน Spring โหมดอะไรอยู่ แล้วทำให้เราใช้ connection database ที่เราเซตไว้ใน spring ได้ด้วย</p>
<p>อีกจุดนึงคือ <code>spring.liquibase.parameters</code> อันนี้เป็น custom parameter ที่เราเอาไว้ใช้ใน Changeset ซึ่งบางครั้งของ database คนละแบบจะต่างกันอย่างเช่น type ENUM ของ H2 เก็บเป็น enum ได้เลย แต่ PostgreSQL ตัว spring จะแปลงเป็น INT เลยต้องมาประกาศไว้ด้วย</p>
<h2 id="usage"><strong>Usage</strong></h2>
<ol>
<li>เพิ่มไฟล์ ChangeLog master ไฟล์นี้เป็นเหมือนไฟล์หลักไว้บอกว่ามี migration step อะไรบ้างผ่าน property ชื่อ <code>include.file</code> ไฟล์กับชื่อจะอยู่ที่ <code>/resources/db/changelog/db.changelog-master.yaml</code> ตัวนี้สามารถตั้งค่าให้ไปอยู่ที่อื่น ชื่ออื่นได้ แต่อันนี้คือชื่อ default หน้าตาของไฟล์จะประมาณ</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">    databaseChangeLog:
</span></span><span class="line"><span class="cl">      - include:
</span></span><span class="line"><span class="cl">          file: db/changelog/001-add-user-table.yaml
</span></span></code></pre></div><ol>
<li>สร้าง ChangeSet ตัวนี้ไว้บอกว่าใน migration นี้มี change อะไรเกี่ยวกับ database บ้าง ตัวอย่างเช่นของ user</li>
</ol>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl">    <span class="n">databaseChangeLog</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">      <span class="o">-</span> <span class="n">changeSet</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="n">id</span><span class="p">:</span> <span class="mi">001</span><span class="o">-</span><span class="n">add</span><span class="o">-</span><span class="n">user</span><span class="o">-</span><span class="n">table</span>
</span></span><span class="line"><span class="cl">          <span class="n">author</span><span class="p">:</span> <span class="n">man</span>
</span></span><span class="line"><span class="cl">          <span class="n">changes</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="o">-</span> <span class="n">createTable</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                <span class="n">columns</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                  <span class="o">-</span> <span class="n">column</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                      <span class="n">constraints</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                        <span class="n">nullable</span><span class="p">:</span> <span class="bp">false</span>
</span></span><span class="line"><span class="cl">                        <span class="n">primaryKey</span><span class="p">:</span> <span class="bp">true</span>
</span></span><span class="line"><span class="cl">                        <span class="n">primaryKeyName</span><span class="p">:</span> <span class="n">user_pkey</span>
</span></span><span class="line"><span class="cl">                      <span class="n">name</span><span class="p">:</span> <span class="n">id</span>
</span></span><span class="line"><span class="cl">                      <span class="n">type</span><span class="p">:</span> <span class="n">UUID</span>
</span></span><span class="line"><span class="cl">                  <span class="o">-</span> <span class="n">column</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                      <span class="n">constraints</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                        <span class="n">nullable</span><span class="p">:</span> <span class="bp">false</span>
</span></span><span class="line"><span class="cl">                      <span class="n">name</span><span class="p">:</span> <span class="n">name</span>
</span></span><span class="line"><span class="cl">                      <span class="n">type</span><span class="p">:</span> <span class="n">VARCHAR</span><span class="p">(</span><span class="mi">250</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                  <span class="o">-</span> <span class="n">column</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                      <span class="n">constraints</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                        <span class="n">nullable</span><span class="p">:</span> <span class="bp">false</span>
</span></span><span class="line"><span class="cl">                      <span class="n">name</span><span class="p">:</span> <span class="n">type</span>
</span></span><span class="line"><span class="cl">                      <span class="n">type</span><span class="p">:</span> <span class="o">$</span><span class="p">{</span><span class="n">EnumDataType</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">                  <span class="o">-</span> <span class="n">column</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                      <span class="n">name</span><span class="p">:</span> <span class="n">is_admin</span>
</span></span><span class="line"><span class="cl">                      <span class="n">type</span><span class="p">:</span> <span class="n">BOOLEAN</span>
</span></span><span class="line"><span class="cl">                  <span class="o">-</span> <span class="n">column</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">                      <span class="n">name</span><span class="p">:</span> <span class="n">created_at</span>
</span></span><span class="line"><span class="cl">                      <span class="n">type</span><span class="p">:</span> <span class="n">TIMESTAMP</span>
</span></span><span class="line"><span class="cl">                <span class="n">tableName</span><span class="p">:</span> <span class="n">user</span>
</span></span></code></pre></div><p>ใน changeSet ต้องบอกว่ามันเป็น changeSet ซึ่งมี property หลักๆ ที่ใช้คือ</p>
<ul>
<li><strong>id:</strong> อันนี้เอาไว้เป็น identifier ไม่ unique ก็ได้ลองแล้วติด แต่ best practice คือจะรันเลขหรือรันเป็นชื่อก็ได้ นี่เลยเอาทั้งสองอย่างแล้วเอาให้เป็นชื่อเดียวกับไฟล์ของ changeset เลยจะได้หาง่ายๆ</li>
<li><strong>author:</strong> ก็คือใครสร้าง changeset นี้</li>
<li><strong>changes:</strong> อันนี้เป็นตัวบอกว่าเราจะทำอะไรเกี่ยวกับ database ในตัวอย่างข้างล่างคือ createTable</li>
</ul>
<p>โน๊ตนิดหน่อยเกี่ยวกับ createTable คือมันต้องการ key หลักสองอย่างคือ columns, tableName ก็ตามตัวเลย แต่ใน columns จะมีรายละเอียดนิดหน่อย</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl">    <span class="o">-</span> <span class="n">column</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">constraints</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">          <span class="n">nullable</span><span class="p">:</span> <span class="bp">false</span>
</span></span><span class="line"><span class="cl">          <span class="n">primaryKey</span><span class="p">:</span> <span class="bp">true</span>
</span></span><span class="line"><span class="cl">          <span class="n">primaryKeyName</span><span class="p">:</span> <span class="n">user_pkey</span>
</span></span><span class="line"><span class="cl">        <span class="n">name</span><span class="p">:</span> <span class="n">id</span>
</span></span><span class="line"><span class="cl">        <span class="n">type</span><span class="p">:</span> <span class="n">UUID</span>
</span></span></code></pre></div><p>ใน column required หลักๆ คือ name กับ type ซึ่งตามที่ใช้ใน SQL ที่สร้างของ database ตัวนั้นเลย ส่วน constraints เป็น optional</p>
<p>ในตัวอย่างจะเห็น <code>type: ${EnumDataType}</code> อันนี้จะเอามาจากที่เราประกาศไว้ใน <code>spring.liquibase.parameters</code> ใน <a href="http://application.properties/">application.properties</a> มาใส่ให้</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl">    <span class="o">-</span> <span class="n">addUniqueConstraint</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">constraintName</span><span class="p">:</span> <span class="n">uq_admin_name_user</span>
</span></span><span class="line"><span class="cl">        <span class="n">columnNames</span><span class="p">:</span> <span class="n">name</span><span class="p">,</span> <span class="n">is_admin</span>
</span></span><span class="line"><span class="cl">        <span class="n">tableName</span><span class="p">:</span> <span class="n">user</span>
</span></span></code></pre></div><p>อีกตัวอย่างเป็นการสร้าง unique constraints ของ table อันนี้ใช้คำสั่ง <code>addUniqueConstraint</code> ในเลเวลเดียวกับ <code>createTable</code> เลย ตรงนี้ required แค่ tableName กับ columnNames ส่วนชื่อถ้าไม่ใส่มันจะเรียงๆ ให้จาก properties ที่เราใส่ไป แนะนำให้ใส่ดีกว่า</p>
<h2 id="how-it-work"><strong>How it work</strong></h2>
<p>เวลาที่ Spring ทำงาน มันจะสั่งรัน liquibase update ให้อัตโนมัติซึ่ง ถ้ายังไม่เคยมี liquibase ทำครั้งแรกมันจะสร้าง table ไว้เก็บ migration step หรือ changelog ด้วยหน้าตาแบบนี้</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">exampledb&gt; select * from databasechangelog;
</span></span><span class="line"><span class="cl">+-------------------------------------+----------+-------------------------------------------------------+----------------------------+-----------------+------------+------------------------------------+-------------------------------------------------------------------
</span></span><span class="line"><span class="cl">| id                                  | author   | filename                                              | dateexecuted               | orderexecuted   | exectype   | md5sum                             | description
</span></span><span class="line"><span class="cl">|-------------------------------------+----------+-------------------------------------------------------+----------------------------+-----------------+------------+------------------------------------+-------------------------------------------------------------------
</span></span><span class="line"><span class="cl">| 001-add-user-table           | man      | db/changelog/001-add-user-table.yaml           | 2020-10-27 10:12:27.875734 | 1               | EXECUTED   | 8:09d73c59f70e73a65420a4eb3f044bc8 | createTable tableName=user
</span></span><span class="line"><span class="cl">+-------------------------------------+----------+-------------------------------------------------------+----------------------------+-----------------+------------+------------------------------------+-------------------------------------------------------------------
</span></span></code></pre></div><p>สำคัญอย่างนึงคือตัว changeset ถ้าถูก execute ไปแล้วห้ามกลับไปแก้เด็ดขาด เพาะ md5sum มันจะไม่ตรงกัน แล้วมันจะไม่ยอม migrate ให้ ถึงแม้ว่า liquibase-maven-plugin จะสั่ง clear checksum ได้ แต่ดีที่สุดคืออย่าไปยุ่งกับมันเลย</p>
<h2 id="unit-test-setup"><strong>Unit test setup</strong></h2>
<p>ถ้าเรา setup เท่าข้างบนแล้วไปรัน unit test เลย สิ่งที่เกิดขึ้นคือ</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">java</span><span class="o">.</span><span class="n">lang</span><span class="o">.</span><span class="n">IllegalStateException</span><span class="p">:</span> <span class="n">Failed</span> <span class="n">to</span> <span class="nb">load</span> <span class="n">ApplicationContext</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="n">Caused</span> <span class="n">by</span><span class="p">:</span> <span class="n">org</span><span class="o">.</span><span class="n">springframework</span><span class="o">.</span><span class="n">beans</span><span class="o">.</span><span class="n">factory</span><span class="o">.</span><span class="n">BeanCreationException</span><span class="p">:</span> <span class="n">Error</span> <span class="n">creating</span> <span class="n">bean</span> <span class="n">with</span> <span class="n">name</span> <span class="s1">&#39;liquibase&#39;</span> <span class="n">defined</span> <span class="ow">in</span> <span class="k">class</span> <span class="n">path</span> <span class="n">resource</span> <span class="p">[</span><span class="n">org</span><span class="o">/</span><span class="n">springframework</span><span class="o">/</span><span class="n">boot</span><span class="o">/</span><span class="n">autoconfigure</span><span class="o">/</span><span class="n">liquibase</span><span class="o">/</span><span class="n">LiquibaseAutoConfiguration</span><span class="o">$</span><span class="n">LiquibaseConfiguration</span><span class="o">.</span><span class="k">class</span><span class="p">]:</span> <span class="n">Invocation</span> <span class="n">of</span> <span class="n">init</span> <span class="n">method</span> <span class="n">failed</span><span class="p">;</span> <span class="n">nested</span> <span class="n">exception</span> <span class="n">is</span> <span class="n">java</span><span class="o">.</span><span class="n">lang</span><span class="o">.</span><span class="n">ArrayIndexOutOfBoundsException</span><span class="p">:</span> <span class="mi">1</span>
</span></span></code></pre></div><p>ซึ่งหาเท่าไรใน Stackoverflow ก็หาไม่เจอ (ลองแล้วทุก link) แต่วิธีแก้คือให้ไปเพิ่ม liquibase property ใน <code>application.properties</code> ของเทสก็จะได้ละ</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">spring.liquibase.enabled=true
</span></span><span class="line"><span class="cl">spring.liquibase.parameters.EnumDataType=ENUM(&#39;VALUE1&#39;, &#39;VALUE2&#39;, &#39;VALUE3&#39;)
</span></span></code></pre></div><h2 id="further-reading"><strong>Further reading</strong></h2>
<p>จริงๆ อยากให้มันสร้าง changeLog จาก Entity ได้เลยแต่ยังหาวิธีไม่ได้ แล้วก็ liquibase-maven-plugin ใช้กับ intelliJ ก็ยังเอ๋อๆ อยู่</p>
<p>อีกอย่างคือ ในตัวอย่างที่ยกมามีแต่ forward migration ยังไม่มี rollback ซึ่งจริงๆ มัน support rollback นะ แล้วก็เขียนอยู่ใน changeSet นะแหละ แต่ยังไม่ได้ดูเพิ่ม</p>
<h2 id="note"><strong>Note</strong></h2>
<h3 id="liquibase-maven-plugin">Liquibase Maven plugin</h3>
<p>เอาไว้สั่ง liquibase แบบไม่ต้อง start Spring แต่ต้องลง dependencies ของมันเองด้วย</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="cl"><span class="nt">&lt;dependencies&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;dependency&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;groupId&gt;</span>org.liquibase<span class="nt">&lt;/groupId&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;artifactId&gt;</span>liquibase-maven-plugin<span class="nt">&lt;/artifactId&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;version&gt;</span>3.6.2<span class="nt">&lt;/version&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/dependency&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/dependencies&gt;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nt">&lt;build&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;plugins&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;plugin&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;groupId&gt;</span>org.liquibase<span class="nt">&lt;/groupId&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;artifactId&gt;</span>liquibase-maven-plugin<span class="nt">&lt;/artifactId&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;version&gt;</span>3.6.2<span class="nt">&lt;/version&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;configuration&gt;</span>
</span></span><span class="line"><span class="cl">                <span class="nt">&lt;propertyFile&gt;</span>src/main/resources/liquibase.properties<span class="nt">&lt;/propertyFile&gt;</span>
</span></span><span class="line"><span class="cl">            <span class="nt">&lt;/configuration&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&lt;/plugin&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="nt">&lt;/plugins&gt;</span>
</span></span><span class="line"><span class="cl"><span class="nt">&lt;/build&gt;</span>
</span></span></code></pre></div><h3 id="liquibaseproperties"><a href="http://liquibase.properties/">liquibase.properties</a></h3>
<p>อันนี้เอาไว้ให้ <code>liquibase-maven-plugin</code> คุยกับ database ได้โดยไม่ต้องง้อ Spring เพราะปกติ Spring จะเป็นคนรัน Liquibase ให้อยู่แล้ว</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="n">changeLogFile</span><span class="o">=</span><span class="n">src</span><span class="o">/</span><span class="n">main</span><span class="o">/</span><span class="n">resources</span><span class="o">/</span><span class="n">db</span><span class="o">/</span><span class="n">changelog</span><span class="o">/</span><span class="n">db</span><span class="p">.</span><span class="na">changelog</span><span class="o">-</span><span class="n">master</span><span class="p">.</span><span class="na">yaml</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">url</span><span class="o">=</span><span class="n">jdbc</span><span class="p">:</span><span class="n">postgresql</span><span class="p">:</span><span class="c1">//localhost:5432/exampledb</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">driver</span><span class="o">=</span><span class="n">org</span><span class="p">.</span><span class="na">postgresql</span><span class="p">.</span><span class="na">Driver</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">username</span><span class="o">=</span><span class="n">user</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="n">password</span><span class="o">=</span><span class="n">password</span><span class="w">
</span></span></span></code></pre></div><h2 id="reference"><strong>Reference</strong></h2>
<ul>
<li>ถ้า setup ง่ายๆ ดูตามนี้ได้ แต่ไฟล์ changelog, changeset เค้าใช้เป็น XML ซึ่งอ่านยากกว่า YAML <a href="https://www.baeldung.com/liquibase-refactor-schema-of-java-app">https://www.baeldung.com/liquibase-refactor-schema-of-java-app</a></li>
<li>วิธีเพิ่ม unique constraint ให้ table <a href="https://docs.liquibase.com/change-types/community/add-unique-constraint.html">https://docs.liquibase.com/change-types/community/add-unique-constraint.html</a></li>
<li>อันนี้ช่วยหาคำตอบตอน Spring context, profile หาไม่เจอกับ liquibase <a href="https://stackoverflow.com/questions/40088915/using-spring-boot-profiles-with-liquibase-changeset-context-attribute-to-manage">https://stackoverflow.com/questions/40088915/using-spring-boot-profiles-with-liquibase-changeset-context-attribute-to-manage</a></li>
<li>อันนี้ช่วยหาคำตอบตอนหาไฟล์ master changelog ไม่เจอ <a href="https://stackoverflow.com/questions/41990295/java-illegalstateexception-cannot-find-changelog-location-class-path-resourc">https://stackoverflow.com/questions/41990295/java-illegalstateexception-cannot-find-changelog-location-class-path-resourc</a></li>
<li>ตัวอย่างนี้ดีเพราะใช้ YAML แล้วก็รู้เรื่อง liquibase.parameters จากที่นี่แหละ <a href="https://reflectoring.io/database-migration-spring-boot-liquibase/">https://reflectoring.io/database-migration-spring-boot-liquibase/</a></li>
<li>อันนี้ช่วยหาคำตอบให้ตอนงงๆ <a href="http://liquibase.properties/">liquibase.properties</a> ว่าตกลงใช้ไม่ใช้ <a href="https://watson.earth/2018/09/21/combining-spring-boot-and-liquibase/">https://watson.earth/2018/09/21/combining-spring-boot-and-liquibase/</a></li>
<li>อันนี้ดูค้างไว้เรื่อง Rollback, pre-condition ของ changeset ว่าใช้ยังไง <a href="https://docs.liquibase.com/concepts/advanced/preconditions.html">https://docs.liquibase.com/concepts/advanced/preconditions.html</a></li>
<li>อันนี้ดีมาก เป็น Best Practice ของ Liquibase เองเลย หลายๆ อย่างได้มาจากอันนี้แหละ <a href="https://www.liquibase.org/get-started/best-practices">https://www.liquibase.org/get-started/best-practices</a></li>
<li>อันนี้พูดถึง liquibase-maven-plugin โดยเฉพาะ <a href="https://docs.liquibase.com/tools-integrations/maven/home.html">https://docs.liquibase.com/tools-integrations/maven/home.html</a></li>
</ul>
]]></content:encoded></item></channel></rss>