<?xml-stylesheet href="/rss.xsl" type="text/xsl"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Howard Tech Note</title><link>https://blog.idontwannarock.dev/</link><description>Recent content on Howard Tech Note</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><copyright>This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</copyright><lastBuildDate>Fri, 07 Feb 2025 09:39:10 +0800</lastBuildDate><atom:link href="https://blog.idontwannarock.dev/index.xml" rel="self" type="application/rss+xml"/><item><title>如何在 MySQL 中進行 insert if not exists</title><link>https://blog.idontwannarock.dev/2025/02/how_to_insert_if_not_exists_in_mysql/</link><pubDate>Fri, 07 Feb 2025 09:39:10 +0800</pubDate><guid>https://blog.idontwannarock.dev/2025/02/how_to_insert_if_not_exists_in_mysql/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2025/02/how_to_insert_if_not_exists_in_mysql/ -&lt;p&gt;相信很多 Developer 都會真心的 &lt;em&gt;&lt;del&gt;幹譙&lt;/del&gt;&lt;/em&gt; 懷疑為什麼 MySQL 經過這麼多年都沒有 PostgreSQL 的 &lt;code&gt;INSERT … ON DUPLICATE KEY DO NOTHING&lt;/code&gt; 語法，偏偏這真的是一個很常會碰到的使用情境&lt;/p&gt;
&lt;p&gt;所以接下來探討一些單一 SQL 語法方面可行的做法&lt;/p&gt;
&lt;h2 id="準備"&gt;準備&lt;/h2&gt;
&lt;p&gt;環境為 MySQL server 8.0.37&lt;/p&gt;
&lt;p&gt;首先建立以下 table (只是為了情境驗證，合理性就先不計較)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;drop&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;create&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt; test_user (
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id &lt;span style="color:#8be9fd;font-style:italic"&gt;int&lt;/span&gt; unsigned auto_increment &lt;span style="color:#ff79c6"&gt;primary&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; user_id &lt;span style="color:#8be9fd;font-style:italic"&gt;int&lt;/span&gt; unsigned &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; is_active &lt;span style="color:#8be9fd;font-style:italic"&gt;boolean&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;default&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;constraint&lt;/span&gt; uk_user_id_user &lt;span style="color:#ff79c6"&gt;unique&lt;/span&gt; (user_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;) engine &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; innodb charset &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; utf8mb4 &lt;span style="color:#ff79c6"&gt;collate&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; utf8mb4_0900_ai_ci;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;情境就是要在表中新增一筆 user 紀錄，如果該 user id 已經存在，就不做任何變更&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;每個情境測試前都會重新建立整個 table，以避免其他問題影響測試結果&lt;/p&gt;
&lt;p&gt;取得 auto increment 數字前，可能需要 &lt;code&gt;analyze table&lt;/code&gt; 以校正數據&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="insert"&gt;&lt;code&gt;insert&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;第一個方法就是直接 &lt;code&gt;insert&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;01&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ERROR &lt;span style="color:#bd93f9"&gt;1062&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;23000&lt;/span&gt;): Duplicate entry &lt;span style="color:#f1fa8c"&gt;&amp;#39;1&amp;#39;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;for&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user.uk_user_id_user&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;3&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到功能完全可以達成&lt;/p&gt;
&lt;p&gt;但是有以下兩個問題&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;第二次以後的 &lt;code&gt;insert&lt;/code&gt; 就會直接噴 duplicate key error&lt;/li&gt;
&lt;li&gt;即使第二次實際上並沒有變更資料，auto increment 還是會 +1&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;因為 &lt;code&gt;insert&lt;/code&gt; 實際上會先按照原有的 auto increment 準備一個新的 row，然後 auto increment 會 +1，接著在檢查到 duplicate key 後，直接拋 duplicate key error 並 reject statement 的執行，auto increment 並不會 rollback&lt;/p&gt;
&lt;p&gt;如果這些問題可以處理或忽略，也不妨考慮這個方式，因為語意上比較明確，也可以讓 client 針對各種可能拋出的錯誤做不同的處理&lt;/p&gt;
&lt;p&gt;但有時候在某些考量下，例如 Java 的 catch exception 有其 overhead，還是希望有不會 raise duplicate key error 的作法&lt;/p&gt;
&lt;h2 id="insert-ignore"&gt;&lt;code&gt;insert ignore&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;如果不希望 raise duplicate key error，首先可以考慮 &lt;code&gt;insert ignore&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;ignore&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;01&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;ignore&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;rows&lt;/span&gt; affected, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; warning (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;show&lt;/span&gt; warnings;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;---------+------+---------------------------------------------------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;Level&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; Code &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; Message &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;---------+------+---------------------------------------------------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; Warning &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1062&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; Duplicate entry &lt;span style="color:#f1fa8c"&gt;&amp;#39;1&amp;#39;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;for&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user.uk_user_id_user&amp;#39;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;---------+------+---------------------------------------------------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;3&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到 &lt;code&gt;insert ignore&lt;/code&gt; 的結果基本上與 &lt;code&gt;insert&lt;/code&gt; 幾乎一樣，只差在原本第二次 &lt;code&gt;insert&lt;/code&gt; 的時候會拋 duplicate key error，現在會被 &lt;code&gt;ignore&lt;/code&gt; 關鍵字 suppress 成 duplicate key warning&lt;/p&gt;
&lt;p&gt;這在使用上需要特別小心，因為 &lt;code&gt;ignore&lt;/code&gt; 是真的會把所有 error 都忽略，可能會有些你意料之外的錯誤不應該忽略的也會被忽略&lt;/p&gt;
&lt;p&gt;而且如果是 insert multiple rows 而且只有其中一些 row 有問題的狀況下，沒有問題的 rows 仍然會被成功 insert 不會整個 insert statement 被 reject，當然這個看狀況可能是缺點也有可能是優點&lt;/p&gt;
&lt;h2 id="replace"&gt;&lt;code&gt;replace&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;不希望 raise duplicate key error 另一個選擇是採用 &lt;code&gt;replace&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;replace&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;01&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;replace&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;rows&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;04&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;3&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以看到 &lt;code&gt;replace&lt;/code&gt; 跟 &lt;code&gt;insert&lt;/code&gt; 也幾乎一樣，只差在第二次 &lt;code&gt;replace&lt;/code&gt; 時是 2 rows affected 並且不會有 error 或 warning，並且在第二次 &lt;code&gt;replace&lt;/code&gt; 後，該筆資料的 &lt;code&gt;id&lt;/code&gt; 被變更為 2&lt;/p&gt;
&lt;p&gt;這是因為 &lt;code&gt;replace&lt;/code&gt; 實際上是先做 &lt;code&gt;delete&lt;/code&gt; 再做 &lt;code&gt;insert&lt;/code&gt;，因此被影響的是 2 個 row，因此也不會有 duplicate key error，因為實際 insert 的時候，原本會 duplicate 的 row 已經被刪掉了&lt;/p&gt;
&lt;p&gt;&lt;code&gt;replace&lt;/code&gt; 除了 id 會被改變、auto increment 還是會 kind of 浪費一個以外，最大的問題是如果 id 被其他 table 設為 foreign key 的時候，可能會產生你意料之外的結果，例如 cascade delete&lt;/p&gt;
&lt;h2 id="insert--on-duplicate-key-update"&gt;&lt;code&gt;insert … on duplicate key update&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;但如果我們既不想 id 被改變，也不希望 raise duplicate key error/warning 呢？&lt;/p&gt;
&lt;p&gt;那可以先考慮 &lt;code&gt;insert ... on duplicate key update&lt;/code&gt; 的做法&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;on&lt;/span&gt; duplicate &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;update&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;new&lt;/span&gt;.user_id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;05&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;on&lt;/span&gt; duplicate &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;update&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;new&lt;/span&gt;.user_id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;rows&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;3&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如此就達成既不會改變 id，也不會 raise duplicate key error/warning&lt;/p&gt;
&lt;p&gt;但是仍然會造成 auto increment 被多 +1&lt;/p&gt;
&lt;p&gt;另外，因為在 8.0.20 之後的 MySQL，&lt;code&gt;values()&lt;/code&gt; 功能已 deprecated，所以改用 8.0.19 引入的 &lt;code&gt;alias&lt;/code&gt; 語法以避免 warning&lt;/p&gt;
&lt;p&gt;如果是 8.0.20 以前的版本，可以改採用 &lt;code&gt;values()&lt;/code&gt; 寫法以避免 syntax error&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;on&lt;/span&gt; duplicate &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;update&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;values&lt;/span&gt;(user_id)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id="insert--on-duplicate-key-update-id--id"&gt;&lt;code&gt;insert … on duplicate key update id = id&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;這個方式還有一個變種，網路流傳這樣寫，MySQL 會自動辨認如果有 duplicate key 要改作 &lt;code&gt;update&lt;/code&gt; 也不會造成 auto increment +1，立刻就來做流言驗證！&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;on&lt;/span&gt; duplicate &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;update&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;06&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) value (&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;on&lt;/span&gt; duplicate &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;update&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;rows&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;3&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;結論不用多說了，MYTH BUSTED!&lt;/p&gt;
&lt;h2 id="insert--select--not-exists"&gt;&lt;code&gt;insert … select … not exists&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;如果我們既不想 raise duplicate key error/warning，也不想改變 id，還不想造成 auto increment waste 呢？&lt;em&gt;(小孩子才做選擇，我全都要)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;直接上驗證結果&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; temp.&lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; user_id) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; temp &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;limit&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;06&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Records: &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; Duplicates: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Warnings: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; temp.&lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; user_id) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; temp &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;limit&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;rows&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Records: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Duplicates: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Warnings: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; table_schema &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;database&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;and&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;在正常情況下，這個方式差不多已經是最優解，既不會 raise duplicate key error/warning，也不改變 id，還不造成 auto increment 浪費&lt;/p&gt;
&lt;p&gt;但這是正常情況下，當你有 high concurrent 的情況下就不見得如此了&lt;/p&gt;
&lt;p&gt;簡單來說是因為 &lt;code&gt;insert ... select&lt;/code&gt; 不是保證 atomic 的，&lt;code&gt;select&lt;/code&gt; 跟 &lt;code&gt;insert&lt;/code&gt; 是分段進行&lt;/p&gt;
&lt;p&gt;如果剛好有相同 unique key 的兩次 &lt;code&gt;insert … select … not exists&lt;/code&gt; 在幾乎相同的時間執行，可能還是會形成兩個 &lt;code&gt;select&lt;/code&gt; 都認為沒有 duplicate row 而執行 &lt;code&gt;insert&lt;/code&gt;，因而造成其中一個 statement 噴出 duplicate key error&lt;/p&gt;
&lt;h3 id="insert--select--not-exists--on-duplicate-key"&gt;&lt;code&gt;insert … select … not exists … on duplicate key&lt;/code&gt;&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; temp.&lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; user_id) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; temp &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;limit&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;on&lt;/span&gt; duplicate &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;update&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;04&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Records: &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; Duplicates: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Warnings: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; temp.&lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; user_id) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; temp &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;limit&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;on&lt;/span&gt; duplicate &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;update&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;rows&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Records: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Duplicates: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Warnings: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個方案算是結合 &lt;code&gt;insert … select … not exists&lt;/code&gt; 跟 &lt;code&gt;insert … on duplicate key&lt;/code&gt;，也就是當 &lt;code&gt;insert … select … not exists&lt;/code&gt; 因為 concurrent 的關係準備要 &lt;code&gt;insert&lt;/code&gt; 一個 duplicate row 的時候，就會改為執行 on duplicate key 的部分，而因為 &lt;code&gt;insert … on duplicate key&lt;/code&gt; 是 atomic 的，所以這樣的操作不會拋出 duplicate key error/warning&lt;/p&gt;
&lt;p&gt;在正常的情況下這個 statement 不會拋出 duplicate key error，id 不會變化，也不會造成 auto increment 浪費&lt;/p&gt;
&lt;p&gt;但是在 concurrent 而形成其中一個 statement 會 insert duplicate row 的情況下，其實跟 &lt;code&gt;insert … on duplicate key&lt;/code&gt; 的問題是相同的，就是有可能會造成 auto increment 浪費&lt;/p&gt;
&lt;h3 id="insert-ignore--select--not-exists"&gt;&lt;code&gt;insert ignore … select … not exists&lt;/code&gt;&lt;/h3&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;ignore&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; temp.&lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; user_id) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; temp &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;limit&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;05&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Records: &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; Duplicates: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Warnings: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;01&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;insert&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;ignore&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;into&lt;/span&gt; test_user (user_id) &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; temp.&lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; user_id) &lt;span style="color:#ff79c6"&gt;as&lt;/span&gt; temp &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;exists&lt;/span&gt; (&lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;limit&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Query OK, &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;rows&lt;/span&gt; affected (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;01&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Records: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Duplicates: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; Warnings: &lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; test_user;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; user_id &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; is_active &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;----+---------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;select&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt;, auto_increment &lt;span style="color:#ff79c6"&gt;from&lt;/span&gt; information_schema.tables &lt;span style="color:#ff79c6"&gt;where&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table_name&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;test_user&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE_NAME&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; AUTO_INCREMENT &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; test_user &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt; &lt;span style="color:#bd93f9"&gt;2&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;&lt;span style="color:#6272a4"&gt;------------+----------------+
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#bd93f9"&gt;1&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;row&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;in&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;set&lt;/span&gt; (&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt;.&lt;span style="color:#bd93f9"&gt;00&lt;/span&gt; sec)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個方案是將 &lt;code&gt;insert … select … not exists&lt;/code&gt; 加上 &lt;code&gt;ingore&lt;/code&gt; 關鍵字，也就式在 concurrent 時，如果發生 duplicate 的狀況，duplicate key error 會被 suppress 成 duplicate key warning&lt;/p&gt;
&lt;p&gt;在一般狀況下同樣不會拋出 duplicate key error，造成 id 改變，或造成 auto increment 浪費&lt;/p&gt;
&lt;p&gt;但是在 concurrent 而形成其中一個 statement 會 insert duplicate row 的情況下，其實跟 &lt;code&gt;insert ignore&lt;/code&gt; 的問題是相同的，就是有可能會造成 auto increment 浪費，而且萬一有其他應該被處理的 error 也會同樣被 suppress&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;直接做表格幫大家整理結論，MySQL 並沒有一個完美解決方案，只能根據狀況做取捨&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;statement&lt;/th&gt;
&lt;th style="text-align: left"&gt;raise duplicate key error/warning&lt;/th&gt;
&lt;th style="text-align: left"&gt;auto increment burn&lt;/th&gt;
&lt;th style="text-align: left"&gt;id change&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;insert&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;error and reject&lt;/td&gt;
&lt;td style="text-align: left"&gt;N&lt;/td&gt;
&lt;td style="text-align: left"&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;insert ignore&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;warning only&lt;/td&gt;
&lt;td style="text-align: left"&gt;Y&lt;/td&gt;
&lt;td style="text-align: left"&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;replace&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;NA&lt;/td&gt;
&lt;td style="text-align: left"&gt;Y&lt;/td&gt;
&lt;td style="text-align: left"&gt;Y&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;insert … on duplicate key update&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;NA&lt;/td&gt;
&lt;td style="text-align: left"&gt;Y&lt;/td&gt;
&lt;td style="text-align: left"&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;insert … select ... not exists&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;possible error and reject when concurrent&lt;/td&gt;
&lt;td style="text-align: left"&gt;possible when concurrent&lt;/td&gt;
&lt;td style="text-align: left"&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;insert … select ... not exists … on duplicate key update id = id&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;NA&lt;/td&gt;
&lt;td style="text-align: left"&gt;possible when concurrent&lt;/td&gt;
&lt;td style="text-align: left"&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;&lt;code&gt;insert ignore … select … not exists&lt;/code&gt;&lt;/td&gt;
&lt;td style="text-align: left"&gt;possible warning when concurrent&lt;/td&gt;
&lt;td style="text-align: left"&gt;possible when concurrent&lt;/td&gt;
&lt;td style="text-align: left"&gt;N&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;另外，本文僅針對各種單一 statement 的方法的硬限制作討論，並不涉及各 statement 速度、空間利用，也不比較 multiple statement 甚至牽涉 client 端的解決方案，而且僅針對預設設定做比較，不涉及改動 isolation level 或 MySQL server config&lt;/p&gt;
&lt;h2 id="reference"&gt;Reference&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/insert.html"&gt;15.2.7 INSERT Statement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html"&gt;15.2.7.2 INSERT &amp;hellip; ON DUPLICATE KEY UPDATE Statement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/insert-select.html"&gt;15.2.7.1 INSERT &amp;hellip; SELECT Statement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-locks-set.html"&gt;17.7.3 Locks Set by Different SQL Statements in InnoDB&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-auto-increment-handling.html"&gt;17.6.1.6 AUTO_INCREMENT Handling in InnoDB&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2025/02/how_to_insert_if_not_exists_in_mysql/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>Percona XtraDB Cluster MySQL with Docker Compose</title><link>https://blog.idontwannarock.dev/2024/10/pxc_mysql_docker_compose/</link><pubDate>Wed, 16 Oct 2024 11:06:41 +0800</pubDate><guid>https://blog.idontwannarock.dev/2024/10/pxc_mysql_docker_compose/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2024/10/pxc_mysql_docker_compose/ -&lt;p&gt;因為公司 production 採用 Percona XtraDB Cluster 去架設 MySQL cluster，而 cluster mode 有很多預設的環境設定與非 cluster mode 的設定不同，例如 &lt;code&gt;pxc_strict_mode&lt;/code&gt; 在 cluster mode 下，預設為 &lt;code&gt;ENFORCING&lt;/code&gt; 或 &lt;code&gt;MASTER&lt;/code&gt;，但非 cluster mode 的 &lt;code&gt;pxc_strict_mode&lt;/code&gt; 預設為 &lt;code&gt;DISABLE&lt;/code&gt; 而且還不能調整為 &lt;code&gt;ENFORCING&lt;/code&gt;，導致有些 SQL 語法在 production 不支援&lt;/p&gt;
&lt;p&gt;為了能夠在本機直接比照 production 環境配置去測試 SQL，所以研究如何快速在本機搭建出 Percona XtraDB Cluster cluster mode 的 MySQL&lt;/p&gt;
&lt;h1 id="環境"&gt;環境&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;WSL2 Ubuntu&lt;/li&gt;
&lt;li&gt;Docker and Docker Compose installed and accessible from terminal&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="作法"&gt;作法&lt;/h1&gt;
&lt;p&gt;先在同一個資料夾建立以下幾個文件&lt;/p&gt;
&lt;h2 id="composeyml"&gt;compose.yml&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;services&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;pxc-mysql&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;image&lt;/span&gt;: percona/percona-xtradb-cluster:8.0
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;container_name&lt;/span&gt;: pxc-mysql
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;restart&lt;/span&gt;: always
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;privileged&lt;/span&gt;: &lt;span style="color:#ff79c6"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;environment&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - MYSQL_ROOT_PASSWORD=root
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - MYSQL_DATABASE=mmschat
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - CLUSTER_NAME=pxc-cluster
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - XTRABACKUP_PASSWORD=root
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;ports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f1fa8c"&gt;&amp;#34;3306:3306&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f1fa8c"&gt;&amp;#34;pxc-mysql-data:/var/lib/mysql&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f1fa8c"&gt;&amp;#34;./cert:/cert&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f1fa8c"&gt;&amp;#34;./conf:/etc/percona-xtradb-cluster.conf.d&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#f1fa8c"&gt;&amp;#34;./init.sql:/docker-entrypoint-initdb.d/init.sql&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;pxc-mysql-data&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這份 docker compose 檔建立 pxc cluster mode 的 MySQL&lt;/p&gt;
&lt;h2 id="confcustomcnf"&gt;conf/custom.cnf&lt;/h2&gt;
&lt;p&gt;在同位置建立 &lt;code&gt;conf&lt;/code&gt; 資料夾，並在 &lt;code&gt;conf&lt;/code&gt; 資料夾底下建立 &lt;code&gt;custom.cnf&lt;/code&gt;&lt;/p&gt;
&lt;pre tabindex="0"&gt;&lt;code class="language-conf" data-lang="conf"&gt;[mysqld]
ssl-ca = /cert/ca.pem
ssl-cert = /cert/server-cert.pem
ssl-key = /cert/server-key.pem
[client]
ssl-ca = /cert/ca.pem
ssl-cert = /cert/client-cert.pem
ssl-key = /cert/client-key.pem
[sst]
encrypt = 4
ssl-ca = /cert/ca.pem
ssl-cert = /cert/server-cert.pem
ssl-key = /cert/server-key.pem
&lt;/code&gt;&lt;/pre&gt;&lt;p&gt;用來配置 SSL 相關檔案位置&lt;/p&gt;
&lt;h2 id="initsql"&gt;init.sql&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;GRANT&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;ALL&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;ON&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;*&lt;/span&gt;.&lt;span style="color:#ff79c6"&gt;*&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TO&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;root&amp;#39;&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;@&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#39;%&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FLUSH &lt;span style="color:#ff79c6"&gt;PRIVILEGES&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;讓 MySQL 啟動的時候，自動執行此 SQL 設定 root 帳號可以遠端存取&lt;/p&gt;
&lt;h2 id="initsh"&gt;init.sh&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-bash" data-lang="bash"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;rm -rf ./cert
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mkdir -m &lt;span style="color:#bd93f9"&gt;777&lt;/span&gt; -p ./cert
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name pxc-cert --rm -v ./cert:/cert percona/percona-xtradb-cluster:8.0 mysql_ssl_rsa_setup -d /cert
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這份 script 的步驟如下&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;清除 &lt;code&gt;./cert&lt;/code&gt; 資料夾以及其中的所有檔案&lt;/li&gt;
&lt;li&gt;重新建立 &lt;code&gt;./cert&lt;/code&gt; 資料夾&lt;/li&gt;
&lt;li&gt;利用 pxc docker image 內建的功能產生 SSL 相關檔案到 &lt;code&gt;./cert&lt;/code&gt; 資料夾&lt;/li&gt;
&lt;/ol&gt;
&lt;h1 id="使用"&gt;使用&lt;/h1&gt;
&lt;p&gt;第一次啟動前需要先執行 &lt;code&gt;sh init.sh&lt;/code&gt; 以建立 SSL 檔案，之後就可以用 docker compose 指令建立、啟動、關閉或銷毀服務&lt;/p&gt;
&lt;p&gt;啟動後可以進到 MySQL 使用以下 SQL 指令確認 cluster 狀態&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;show&lt;/span&gt; status &lt;span style="color:#ff79c6"&gt;like&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;wsrep%&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然後檢查以下這幾項的設定，就可以確認 cluster 是否有建立成功&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wsrep_local_state_comment&lt;/code&gt;: &lt;code&gt;Synced&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wsrep_cluster_size&lt;/code&gt;: &lt;code&gt;1&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wsrep_cluster_status&lt;/code&gt;: &lt;code&gt;Primary&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wsrep_connected&lt;/code&gt;: &lt;code&gt;ON&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;wsrep_ready&lt;/code&gt;: &lt;code&gt;ON&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;另外可以使用以下 SQL 指令確認 pxc strict mode，若是 &lt;code&gt;ENFORCING&lt;/code&gt; 或 &lt;code&gt;MASTER&lt;/code&gt; 就是跟 production 集群相同&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;show&lt;/span&gt; variables &lt;span style="color:#ff79c6"&gt;like&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#39;pxc_strict_mode&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="reference"&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.percona.com/percona-xtradb-cluster/8.0/docker.html"&gt;Running Percona XtraDB Cluster in a Docker Container&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.csdn.net/alwaysbefine/article/details/110748717"&gt;使用docker-compose 快速搭建Percona XtraDB Cluster（pxc集群）&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://piaohua.github.io/post/mysql/20210523-mysql-pxc/"&gt;[MySQL] Percona XtraDB Cluster on docker-compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/how-tos/startup-order/"&gt;Control startup and shutdown order in Compose&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2024/10/pxc_mysql_docker_compose/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>.gitattributes 引發的慘案</title><link>https://blog.idontwannarock.dev/2024/05/gitattributes_glowroot_error/</link><pubDate>Wed, 22 May 2024 09:25:13 +0800</pubDate><guid>https://blog.idontwannarock.dev/2024/05/gitattributes_glowroot_error/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2024/05/gitattributes_glowroot_error/ -&lt;h1 id="情況描述"&gt;情況描述&lt;/h1&gt;
&lt;p&gt;最近在嘗試建立新專案的基礎專案架構，因為剛好看到常用的 APM 工具 &lt;a href="https://github.com/glowroot/glowroot/releases/tag/v0.14.2"&gt;Glowroot 0.14.2 版 release&lt;/a&gt; 開始支援 Java 21，讓我產生使用 &lt;a href="https://blog.idontwannarock.dev/2022/12/maven_docker_multi_stage_build/"&gt;multi-staged build&lt;/a&gt; + &lt;a href="https://docs.oracle.com/en/java/javase/11/tools/jlink.html"&gt;jlink&lt;/a&gt; 盡量縮小 image 大小，並且掛上 &lt;a href="https://glowroot.org/"&gt;Glowroot&lt;/a&gt; 作為 Java Agent 來運行專案的念頭&lt;/p&gt;
&lt;p&gt;因為以往已經將 Glowroot 使用在 Java 8, 11, 17 等版本的專案上，所以 Dockerfile 的基本架構已經確立，理論上只需要調整 base image 為 Java 21 應該就沒問題&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;FROM&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;eclipse-temurin:21&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENV&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;CONFIG_java_opts&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;-server -XX:+UseG1GC&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; extras/glowroot /extras/glowroot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; target/*jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENTRYPOINT&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;exec&lt;/span&gt; java &lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_java_opts&lt;/span&gt; -javaagent:/extras/glowroot/glowroot.jar -Dglowroot.collector.address&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_glowroot_address&lt;/span&gt; -Dglowroot.agent.id&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_app_name&lt;/span&gt;:: -jar /app.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;extras/glowroot&lt;/code&gt; 資料夾包含我直接從 &lt;a href="https://github.com/glowroot/glowroot/releases/tag/v0.14.2"&gt;Glowroot 0.14.2 Release&lt;/a&gt; 這邊下載後解壓縮出來的 &lt;code&gt;glowroot.jar&lt;/code&gt; 檔，並且直接 commit 到 git 上&lt;/li&gt;
&lt;li&gt;&lt;code&gt;$CONFIG_java_opts&lt;/code&gt;、&lt;code&gt;$CONFIG_glowroot_address&lt;/code&gt; 及 &lt;code&gt;$CONFIG_app_name&lt;/code&gt; 都是定義在 K8S config map 中的變數，會在 K8S 運行這個 image 的時候從 pod 的環境變數取得&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;這樣的 Dockerfile 產出的 image 大約是 520MB，&lt;code&gt;app.jar&lt;/code&gt; 檔大約 65MB，所以代表 image 的其他部分就有 455MB，其實有點大&lt;/p&gt;
&lt;p&gt;如果我專案複雜一點可能光 &lt;code&gt;app.jar&lt;/code&gt; 就會來到 4-500MB，那 image 甚至可能會將近 1GB，這樣在 CICD 頻繁的情況下對於 image 傳輸的壓力太大，而且 K8S rolling start pod 的速度也會很慢，所以才要盡量改成 multi-staged build 來縮小 image 大小&lt;/p&gt;
&lt;p&gt;另外因為這套寫法已經在公司太多專案上運行了，所以我並沒有特別測試，就直接開始調整為 multi-staged build + jlink&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;FROM&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;eclipse-temurin:21&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;AS&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;jre-build&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;WORKDIR&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;/app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; target/*.jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6272a4"&gt;# Analyze dependent modules of the app&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;RUN&lt;/span&gt; jar xf app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;RUN&lt;/span&gt; jdeps &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --ignore-missing-deps &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --print-module-deps &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --multi-release &lt;span style="color:#bd93f9"&gt;21&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --recursive &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --class-path &lt;span style="color:#f1fa8c"&gt;&amp;#39;BOOT-INF/lib/*&amp;#39;&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; app.jar &amp;gt; modules.info
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6272a4"&gt;# Create a custom Java runtime&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;RUN&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;$JAVA_HOME&lt;/span&gt;/bin/jlink &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --add-modules &lt;span style="color:#ff79c6"&gt;$(&lt;/span&gt;cat modules.info&lt;span style="color:#ff79c6"&gt;)&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --strip-debug &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --no-man-pages &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --no-header-files &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --compress&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#bd93f9"&gt;0&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; --output /javaruntime
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6272a4"&gt;# Define base image&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;FROM&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;debian:stable-slim&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENV&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;JAVA_HOME&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;/opt/java/openjdk
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENV&lt;/span&gt; PATH &lt;span style="color:#f1fa8c"&gt;&amp;#34;&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;${&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;JAVA_HOME&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;}&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;/bin:&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;${&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;PATH&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;}&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;jre-build /javaruntime &lt;span style="color:#8be9fd;font-style:italic"&gt;$JAVA_HOME&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#6272a4"&gt;# Continue with app deployment&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENV&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;CONFIG_java_opts&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;-server -XX:+UseG1GC&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; extras/glowroot /extras/glowroot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; target/*.jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENTRYPOINT&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;exec&lt;/span&gt; java &lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_java_opts&lt;/span&gt; -javaagent:/extras/glowroot/glowroot.jar -Dglowroot.collector.address&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_glowroot_address&lt;/span&gt; -Dglowroot.agent.id&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_app_name&lt;/span&gt;:: -jar /app.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這邊 &lt;code&gt;app.jar&lt;/code&gt; 檔一樣大約 65MB，&lt;code&gt;debian:stable-slim&lt;/code&gt; image 本身大約 75MB，而我最終產出的 image 大約 252MB，也就是說 jlink 產出的 JRE 大約 112MB 而已 (原生的 Java 21 JRE 約 143MB)，這代表除去 &lt;code&gt;app.jar&lt;/code&gt; 檔以外，image 本身只有 187MB，相較於前一份 Dockerfile 產出的 455MB，大幅縮減了約 288MB&lt;/p&gt;
&lt;p&gt;但是當我很開心的運行這個 image 的時候，卻碰到以下的錯誤訊息，導致程式完全無法運行&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Error occurred during initialization of VM
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;agent library failed Agent_OnLoad: instrument
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Error opening zip file or JAR manifest missing : /extras/glowroot/glowroot.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="問題原因"&gt;問題原因&lt;/h1&gt;
&lt;p&gt;我整個矇，因為 Glowroot 0.14.2 這版 release 已經出了一個多月，如果 jar 檔有問題，應該很多人都會碰到才對，而且搜尋了一下，還真沒有人開 issue，表示真的沒有其他人碰到這個問題，我只好反求諸己&lt;/p&gt;
&lt;h2 id="權限問題"&gt;權限問題？&lt;/h2&gt;
&lt;p&gt;我一開始的思路是權限問題，因為 stackoverflow 上很多碰到 &lt;code&gt;Error opening zip file or JAR manifest missing&lt;/code&gt; 錯誤訊息的關鍵都是 jar 檔是否存在以及 jar 檔權限問題&lt;/p&gt;
&lt;p&gt;首先 image 裡面 &lt;code&gt;glowroot.jar&lt;/code&gt; 檔肯定是存在的，所以我開始查是不是權限問題，這裡我直接把 Dockerfile 精簡做測試&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;FROM&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;eclipse-temurin:21&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; target/*jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENTRYPOINT&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;exec&lt;/span&gt; java -jar /app.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這樣運行完全沒有問題&lt;/p&gt;
&lt;p&gt;於是簡單加上 Glowroot&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;FROM&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;eclipse-temurin:21&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; extras/glowroot /extras/glowroot
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; target/*jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENTRYPOINT&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;exec&lt;/span&gt; java -javaagent:/extras/glowroot/glowroot.jar -Dglowroot.collector.address&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_glowroot_address&lt;/span&gt; -Dglowroot.agent.id&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_app_name&lt;/span&gt;:: -jar /app.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;很抱歉，立刻就報 &lt;code&gt;Error opening zip file or JAR manifest missing&lt;/code&gt; 錯誤&lt;/p&gt;
&lt;p&gt;這就很奇怪了，這邊 &lt;code&gt;glowroot.jar&lt;/code&gt; 複製進 image 的方式跟 &lt;code&gt;app.jar&lt;/code&gt; 一模一樣，&lt;code&gt;app.jar&lt;/code&gt; 可以執行，沒有道理 &lt;code&gt;glowroot.jar&lt;/code&gt; 會不行才對呀？&lt;/p&gt;
&lt;p&gt;挖賽，光在這邊我就卡關了兩天，一直查 Dockerfile 權限問題想找出兩個 jar 檔的差異&lt;/p&gt;
&lt;p&gt;直到我看到某一篇解答 &lt;code&gt;Error opening zip file or JAR manifest missing&lt;/code&gt; 錯誤的時候提到&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;除了檢查 jar 檔是否存在及權限以外，也要檢查 jar 檔本身是否毀損&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="jar-檔毀損"&gt;Jar 檔毀損？&lt;/h2&gt;
&lt;p&gt;我原本認為我是直接從官方 URL 下載下來的 jar 檔不可能有問題，但死馬當作活馬醫，於是我將 Dockerfile 改成每次都重新抓 Glowroot 的 jar 檔下來試試看&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-dockerfile" data-lang="dockerfile"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;FROM&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;eclipse-temurin:21&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;RUN&lt;/span&gt; apt-get update &lt;span style="color:#ff79c6"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get install unzip
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ADD&lt;/span&gt; https://github.com/glowroot/glowroot/releases/download/v0.14.2/glowroot-0.14.2-dist.zip /tmp/glowroot.zip
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;RUN&lt;/span&gt; mkdir /extras &lt;span style="color:#ff79c6"&gt;&amp;amp;&amp;amp;&lt;/span&gt; unzip /tmp/glowroot.zip -d /extras
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;COPY&lt;/span&gt; target/*jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ENTRYPOINT&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;exec&lt;/span&gt; java -javaagent:/extras/glowroot/glowroot.jar -Dglowroot.collector.address&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_glowroot_address&lt;/span&gt; -Dglowroot.agent.id&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;$CONFIG_app_name&lt;/span&gt;:: -jar /app.jar
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這樣還真的就可以運行了！！！&lt;/p&gt;
&lt;p&gt;看來是我 commit 上去的 &lt;code&gt;glowroot.jar&lt;/code&gt; 檔真的是有問題，但我是從同一個 URL 下載下來的，怎麼可能有什麼問題呢？&lt;/p&gt;
&lt;h2 id="gitattributes-4-ni"&gt;&lt;code&gt;.gitattributes&lt;/code&gt; 4 ni？！&lt;/h2&gt;
&lt;p&gt;於是我回到專案本身去查有沒有哪裡有可能動到 jar 檔，最後還真的讓我想到可能的兇手：&lt;code&gt;.gitattributes&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;.gitattributes&lt;/code&gt; 檔是用來方便 Git 幫我們管理文件的屬性，Git 會在檔案在 repository, staging area 跟 working area 間轉換時，依照 &lt;code&gt;.gitattributes&lt;/code&gt; 的設定調整對應的檔案，常用於統一設定檔案的換行符號&lt;/p&gt;
&lt;p&gt;例如我的專案當中的 &lt;code&gt;.gitattributes&lt;/code&gt; 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* text=auto
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.java text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.xml text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.md text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.yml text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.yaml text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;就我的理解，除了從第三行開始特別設定的檔案類型以外，Git 會先自己判斷檔案類型是 &lt;code&gt;text&lt;/code&gt; 或 &lt;code&gt;binary&lt;/code&gt;，如果是 &lt;code&gt;text&lt;/code&gt; 類型的檔案，就會自動設定檔案的 eol(end-of-line) 為 &lt;code&gt;lf&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而一般人應該很自然地就會認為 jar 檔會被判斷為 &lt;code&gt;binary&lt;/code&gt; 類型而不會受到影響&lt;/p&gt;
&lt;p&gt;BUT，就是這個 BUT&lt;/p&gt;
&lt;p&gt;我的第三行就設定了所有檔案都預設為 &lt;code&gt;text&lt;/code&gt;&amp;hellip;&lt;/p&gt;
&lt;p&gt;而且 Git 在自動判斷檔案是 &lt;code&gt;text&lt;/code&gt; 或 &lt;code&gt;binary&lt;/code&gt; 的時候是採用 heuristics 的方式，所以就算讓 Git 自行判斷也不能保證絕對正確&lt;/p&gt;
&lt;p&gt;而在我的情況，以往的 &lt;code&gt;glowroot.jar&lt;/code&gt; 被視為 binary 檔因此沒有被改動，而這次卻被視為 &lt;code&gt;text&lt;/code&gt; 類型的檔案，因此被 Git 改動而毀損&lt;/p&gt;
&lt;h1 id="解決辦法"&gt;解決辦法&lt;/h1&gt;
&lt;p&gt;知道問題後就簡單了，在 &lt;code&gt;.gitattributes&lt;/code&gt; 當中註明 jar 檔不是 text 檔如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* text=auto
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;* text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.java text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.xml text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.md text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.yml text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.yaml text eol=lf
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;*.jar -text
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;所以剛好藉此機會提醒自己，往後設定這種會變動到檔案的部分都要特別注意，在 &lt;code&gt;.gitattributes&lt;/code&gt; 的部分，最好將所有會被 commit 的檔案類型都列出來，以避免 Git 判斷檔案類型會有出乎意料之外的問題&lt;/p&gt;
&lt;h1 id="後記"&gt;後記&lt;/h1&gt;
&lt;p&gt;我在查資料的時候，還找到 &lt;a href="https://github.com/gitattributes/gitattributes"&gt;gitattributes&lt;/a&gt; 這個 repo，裡面紀錄了各種 &lt;code&gt;.gitattributes&lt;/code&gt; 的樣板，裡面就有 Java 的版本，裡面就有特別將 &lt;code&gt;*.jar&lt;/code&gt; 設定為 &lt;code&gt;binary&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;還有這個 &lt;a href="https://gitattributes.com/"&gt;網頁版的 &lt;code&gt;.gitattributes&lt;/code&gt; 產生器&lt;/a&gt;，類似 &lt;a href="https://gitignore.io/"&gt;gitignore.io&lt;/a&gt; 的用法，輸入語系就可以依照樣板產出對應的 &lt;code&gt;.gitattributes&lt;/code&gt; 檔&lt;/p&gt;
&lt;h1 id="reference"&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/glowroot/glowroot/releases/tag/v0.14.2"&gt;Release Version 0.14.2 · glowroot/glowroot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/en/java/javase/11/tools/jlink.html"&gt;jlink&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://glowroot.org/"&gt;Glowroot&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/_/eclipse-temurin"&gt;eclipse-temurin - Official Image | Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/@snyksec/using-jlink-to-create-smaller-docker-images-for-your-spring-boot-java-application-5e21a3377965"&gt;Using JLink to create smaller Docker images for your Spring Boot Java application&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dzone.com/articles/ways-to-reduce-jvm-docker-image-size"&gt;Ways To Reduce JVM Docker Image Size&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/docs/gitattributes"&gt;Git - gitattributes Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/gitattributes/gitattributes"&gt;GitHub - gitattributes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitattributes.com/"&gt;gitattributes.io - Create .gitattributes file for your project&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gitignore.io/"&gt;gitignore.io&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2024/05/gitattributes_glowroot_error/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>Java Time 保存到 MySQL DATETIME 時區問題</title><link>https://blog.idontwannarock.dev/2023/12/java_time_mapping_mysql_datetime_timezone/</link><pubDate>Wed, 27 Dec 2023 09:26:23 +0800</pubDate><guid>https://blog.idontwannarock.dev/2023/12/java_time_mapping_mysql_datetime_timezone/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2023/12/java_time_mapping_mysql_datetime_timezone/ -&lt;p&gt;最近開發遇到一個對我的系統需求來說很合適，但我感覺不合理的狀況&lt;/p&gt;
&lt;p&gt;目前有 Spring Boot 系統在 JPA 是用 &lt;code&gt;OffsetDateTime&lt;/code&gt; 在 Entity 中對應 MySQL 表中的 &lt;code&gt;DATETIME&lt;/code&gt; 欄位，JVM 時區以及 MySQL 的系統時區都是 +8，沒有特別在任何地方進行時區設定，也就是所有地方應該都是預設設定&lt;/p&gt;
&lt;p&gt;然而當 &lt;code&gt;OffsetDateTime&lt;/code&gt; 保存到 &lt;code&gt;DATETIME&lt;/code&gt; 當中時，卻會被自動轉換成 UTC 時區的時間保存，並且之後再用 JPA 重新取出後，&lt;code&gt;OffsetDateTime&lt;/code&gt; 也依然是 UTC 時區&lt;/p&gt;
&lt;p&gt;這就讓人感覺很奇怪了，感覺要馬就應該是 MySQL &lt;code&gt;DATETIME&lt;/code&gt; 保存時用 JVM 跟 MySQL 相同的系統 +8 時區時間保存，取出也是 +8 時間；要馬就是保存時會轉成 UTC，但取出應該會自動轉換回 JVM 的 +8 才合理，畢竟 &lt;code&gt;DATETIME&lt;/code&gt; 欄位資料格式不保存時區資訊，所以要馬就是完全不轉換，要馬就是會依照某種機制保存時用 UTC，但使用時會自動轉換成 JVM 時區之類的模式才對&lt;/p&gt;
&lt;p&gt;雖然我原本就希望在系統中盡量都使用 UTC 時區而不要隨著 MySQL/JVM 時區變動以避免造成資料錯誤的問題，但機制不如預期就應該要弄清楚，免得其實是有什麼未知的狀況造成日後踩到坑&lt;/p&gt;
&lt;h1 id="pre-requisite"&gt;Pre-requisite&lt;/h1&gt;
&lt;p&gt;首先要先說明這篇文章的研究都是基於下列版本，不同的版本「可能」會有不同的行為，請查詢官網文件或自行測試&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Spring Boot Data JPA 3.1.4&lt;/li&gt;
&lt;li&gt;Hibernate 6.2.9.Final&lt;/li&gt;
&lt;li&gt;MySQL JDBC Driver 8.0.33&lt;/li&gt;
&lt;li&gt;MySQL server 8.0.33&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="資料類型選擇的考量"&gt;資料類型選擇的考量&lt;/h1&gt;
&lt;p&gt;首先說明為什麼要選擇用 &lt;code&gt;OffsetDateTime&lt;/code&gt; 對應 MySQL &lt;code&gt;DATETIME&lt;/code&gt; 的考量&lt;/p&gt;
&lt;p&gt;在 MySQL 當中可以同時保存日期及時間的格式只有 &lt;code&gt;TIMESTAMP&lt;/code&gt; 及 &lt;code&gt;DATETIME&lt;/code&gt; 兩種，其中 &lt;code&gt;TIMESTAMP&lt;/code&gt; 資料格式只支援到 &lt;code&gt;2038-01-19 03:14:07&lt;/code&gt; UTC，而 &lt;code&gt;DATETIME&lt;/code&gt; 格式可以支援到 &lt;code&gt;9999-12-31 23:59:59&lt;/code&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MySQL 因為用 signed 32-bit integer 來保存 &lt;code&gt;TIMESTAMP&lt;/code&gt; 資料格式包含秒以上的資料，因此最多只能記錄到從 &lt;code&gt;1970-01-01 00:00:01&lt;/code&gt; UTC 開始後的 2147483647 秒，也就是 &lt;code&gt;2038-01-19 03:14:07&lt;/code&gt; UTC，超過一秒就會 stackoverflow；而 &lt;code&gt;DATETIME&lt;/code&gt; 則採用完全不同的保存策略，所以可以支援到 &lt;code&gt;9999-12-31 23:59:59&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;另外 &lt;code&gt;TIMESTAMP&lt;/code&gt; 資料格式可能不見得適合在高併發的情況下使用，因為在 MySQL 官方文件的「&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html#time-zone-variables"&gt;5.1.15 MySQL Server Time Zone Support#Time Zone Variables&lt;/a&gt;」中有以下這段&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If set to SYSTEM, every MySQL function call that requires a time zone calculation makes a system library call to determine the current system time zone. This call may be protected by a global mutex, resulting in contention.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;這段是在說 MySQL 系統的 &lt;code&gt;time_zone&lt;/code&gt; 變數如果是預設的 &lt;code&gt;SYSTEM&lt;/code&gt;，在每次需要做時區運算的時候，MySQL 系統就會取得全域鎖 (global mutex) 來做資料正確性的確保，導致鎖爭用 (lock contention)&lt;/p&gt;
&lt;p&gt;而因為 &lt;code&gt;TIMESTAMP&lt;/code&gt; 的時區自動轉換功能雖然能確保時間時區的正確性，卻容易導致每次存取都需要做時區轉換運算，進而導致鎖爭用發生，而對效能有影響&lt;/p&gt;
&lt;p&gt;但另一方面來說，雖然 &lt;code&gt;DATETIME&lt;/code&gt; 並不記錄時區而避免鎖爭用的問題，但卻相對容易衍生時區相關的問題&lt;/p&gt;
&lt;p&gt;因為我的系統流量不小，加上不想在未來面對資料搬遷的問題，所以才選擇 &lt;code&gt;DATETIME&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;而說到在 Java 這邊，因為舊的 &lt;code&gt;java.sql.Timestamp&lt;/code&gt; 等時間相關的類別有許多問題，所以從 Java 8 開始推出了 &lt;code&gt;java.time&lt;/code&gt; 系列的類別來處理時間相關的資料&lt;/p&gt;
&lt;p&gt;當遇到要處理帶時區的時間時，比較常用到的類別就是 &lt;code&gt;Instant&lt;/code&gt; 及 &lt;code&gt;OffsetDateTime&lt;/code&gt;，而 Hibernate 官方文件中「&lt;a href="https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#basic-temporal"&gt;2.2.48. Handling temporal data&lt;/a&gt;」有列出帶時區時間支援的類別只有 &lt;code&gt;java.time.OffsetDateTime&lt;/code&gt; 及 &lt;code&gt;java.time.ZonedDateTime&lt;/code&gt; 這兩個&lt;/p&gt;
&lt;p&gt;而因為我的系統並沒有需要針對不同地方的時間做運算的需求，尤其是不需要計算日光節約時間，而只需要用 UTC 計算即可，所以才選用 &lt;code&gt;OffsetDateTime&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;談完選用資料型態的考量後，就開始研究時區轉換的機制吧&lt;/p&gt;
&lt;h1 id="時區轉換機制"&gt;時區轉換機制&lt;/h1&gt;
&lt;p&gt;在研究時區轉換機制之前，要先了解 JPA 是怎麼跟 MySQL 互動的&lt;/p&gt;
&lt;p&gt;首先 Spring Boot Data JPA 是 Spring Boot 實作方便整合到 Spring Boot 系統並且符合 JPA 規範的介面，主要機制還是圍繞在操作預設使用的 Hibernate 實作，而 Hibernate 這個 ORM 則會透過內部機制去操作 JDBC Driver 最終跟 MySQL server 進行互動&lt;/p&gt;
&lt;p&gt;在這個流程中，MySQL &lt;code&gt;DATETIME&lt;/code&gt; 並不保存時區也不對保存的時間進行時區轉換，而 Spring Boot Data JPA 只是介接符合 JPA 實作的 ORM 框架以及 Spring Boot 系統，所以時區轉換的機制比較大的可能發生在 Hibernate 或 MySQL JDBC Driver 這兩者當中&lt;/p&gt;
&lt;h2 id="mysql-jdbc-driver-的時區轉換"&gt;MySQL JDBC Driver 的時區轉換&lt;/h2&gt;
&lt;p&gt;這邊我首先懷疑的是 JDBC Driver，因為 MySQL 的官網上很容易就能找到這份文件「&lt;a href="https://dev.mysql.com/doc/connector-j/en/connector-j-time-instants.html"&gt;6.6.1 Preserving Time Instants&lt;/a&gt;」有以下說明&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The situation is less straightforward with the &lt;code&gt;DATETIME&lt;/code&gt; data type: it does not represent an instant and, when no time zone offset is specified, there is no time zone conversion for &lt;code&gt;DATETIME&lt;/code&gt; values, so they are stored and retrieved as they are. However, with a specified time zone offset, the input value is converted to the session time zone before it is stored; the result is that, when retrieved in a different session with a different time zone offset as the specified one, the &lt;code&gt;DATETIME&lt;/code&gt; value becomes different from the original input value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;也就是說 &lt;code&gt;DATETIME&lt;/code&gt; 資料格式不會做時區轉換，所以怎麼保存就怎麼取出來；但是如果保存的時候有帶上時區，會先將時間轉換成 session timezone 後再保存，因此當取出時，是在不同 timezone 的 session 中，取得的值會跟原先保存的原始值不同&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;MySQL 依照 session timezone 轉換時區的效果可以參考官網這份文件「&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/date-and-time-literals.html"&gt;9.1.3 Date and Time Literals&lt;/a&gt;」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;這聽起來好像有可能符合我遇到的狀況，所以問題就在於 session timezone 是怎麼決定的&lt;/p&gt;
&lt;p&gt;MySQL server timezone 的設定可以在這份文件「&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html"&gt;5.1.15 MySQL Server Time Zone Support&lt;/a&gt;」，但在使用 ORM 的時候這裡面提到的方式都不是合適的方法，因為其中要不是全域設定就是要額外下 sql 去變動，而且我也沒有做任何類似的設定&lt;/p&gt;
&lt;p&gt;另外還有一種方式就是在 JDBC Driver connection url 上加上設定參數去調整連線的 timezone，如這份文件「&lt;a href="https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-datetime-types-processing.html"&gt;6.3.11 Datetime types processing&lt;/a&gt;」說的 &lt;code&gt;connectionTimeZone&lt;/code&gt; 及 &lt;code&gt;forceConnectionTimeZoneToSession&lt;/code&gt; 參數也可以調整 session timezone，但是我也沒有做這個設定&lt;/p&gt;
&lt;p&gt;這邊我透過 &lt;code&gt;select @@system_time_zone, @@time_zone, @@global.time_zone, @@session.time_zone;&lt;/code&gt; 實際去查 timezone 的結果如下&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th style="text-align: left"&gt;@@system_time_zone&lt;/th&gt;
&lt;th style="text-align: left"&gt;@@time_zone&lt;/th&gt;
&lt;th style="text-align: left"&gt;@@global.time_zone&lt;/th&gt;
&lt;th style="text-align: left"&gt;@@session.time_zone&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td style="text-align: left"&gt;CST&lt;/td&gt;
&lt;td style="text-align: left"&gt;SYSTEM&lt;/td&gt;
&lt;td style="text-align: left"&gt;SYSTEM&lt;/td&gt;
&lt;td style="text-align: left"&gt;SYSTEM&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;可以看到基本上全部 timezone 都是比照 &lt;code&gt;SYSTEM&lt;/code&gt; 也就是 MySQL 的系統時區 CST(+8)&lt;/p&gt;
&lt;p&gt;那就很奇怪了，因為按照前面文件描述，&lt;code&gt;DATETIME&lt;/code&gt; 欄位型態在面對插入的時間帶有時區的時候會按照 session timezone 來轉換時區後保存，但我面對的 session timezone 明明就是 +8，它到底是怎麼保存成 UTC 的呢？&lt;/p&gt;
&lt;h2 id="hibernate-的時區轉換"&gt;Hibernate 的時區轉換&lt;/h2&gt;
&lt;p&gt;於是我們轉向另外一個嫌疑人 Hibernate，Hibernate 到底是怎麼做時區轉換的呢？或者說有沒有做時區轉換呢？&lt;/p&gt;
&lt;p&gt;首先就是 Hibernate 有提供一個參數 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 可以設定，這會讓 Hibernate 在將時間傳送到 DB 之前就先將時區轉換成參數設定的時區，取出來的時候也是會將時間轉回參數設定的時區&lt;/p&gt;
&lt;p&gt;如果我設定這個參數為 UTC 的話，其行為就與我觀測到的相同，但是我並沒有設定這個參數，而這個參數的預設值是 JVM 時區，所以看來應該還有別的機制在作用&lt;/p&gt;
&lt;h3 id="jdbc-42-時間格式規範"&gt;JDBC 4.2 時間格式規範&lt;/h3&gt;
&lt;p&gt;在講到 Hibernate 另外一個機制之前，要先提一下 JDBC 4.2 對於時間的規範&lt;/p&gt;
&lt;p&gt;JDBC 4.2 新增了 &lt;a href="https://docs.oracle.com/javase/8/docs/api/java/sql/JDBCType.html"&gt;JDBCType&lt;/a&gt;，其中對於時間資料規範了以下四種類型&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;DATE&lt;/code&gt;: 代表日期，保存年月日&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIME&lt;/code&gt;: 代表時間，保存時分秒&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIMESATMP&lt;/code&gt;: 代表日期及時間，保存年月日時分秒加上奈秒 (nanosecond)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TIMESTAMP_WITH_TIMEZONE&lt;/code&gt;: 代表日期及時間以及時區，保存年月日時分秒加上奈秒 (nanosecond) 以及時區 id 或 offset&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="mysql-jdbc-driver-時間格式支援"&gt;MySQL JDBC Driver 時間格式支援&lt;/h3&gt;
&lt;p&gt;但是就如我們所知道的，MySQL 對於保存完整日期及時間的資料格式只有兩種，&lt;code&gt;TIMESTAMP&lt;/code&gt; 及 &lt;code&gt;DATETIME&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;TIMESATMP&lt;/code&gt; 會把所有時間轉成 UTC 後再保存成 unix timestamp，而我們也知道 &lt;code&gt;DATETIME&lt;/code&gt; 則是不保存時區的，所以 MySQL JDBC Driver 實際上是不支援 &lt;code&gt;TIMESTAMP_WITH_TIMEZONE&lt;/code&gt; 這個 jdbc type 的，它實際上都會先處理好時間的時區後，再用 &lt;code&gt;TIMESTAMP&lt;/code&gt; 這個 jdbc type 來傳送時間資料&lt;/p&gt;
&lt;p&gt;至於 driver 內部怎麼處理時區，我們前面一段也已經講過，並不如我觀測到的狀況，所以現在才會要來看 Hibernate 是怎麼處理這個問題&lt;/p&gt;
&lt;h3 id="hibernate-5-的-normalization"&gt;Hibernate 5 的 normalization&lt;/h3&gt;
&lt;p&gt;但畢竟 Hibernate 還是要透過 JDBC Driver 才能真正跟 DB 互動，所以針對 MySQL JDBC Driver 不支援 &lt;code&gt;TIMESTAMP_WITH_TIMEZONE&lt;/code&gt; 的問題，從 Hibernate 5 開始採取了一種策略，就是先將帶有時區資訊的時間先 normalize 成沒有時區的時間後再保存，normalize 的過程其實就是會將時間轉換成 JVM 的時區，而取出來的無時區時間再加上 JVM 時區做還原&lt;/p&gt;
&lt;p&gt;例如今天 JVM 時區為 +8，而你要將時間為 &lt;code&gt;2023-12-25 11:30:00+6&lt;/code&gt; 的 &lt;code&gt;OffsetDateTime&lt;/code&gt; 保存起來，Hibernate 5 會先將 &lt;code&gt;2023-12-25 11:30:00+6&lt;/code&gt; 轉換成 JVM 的時區並去掉時區資訊，也就是變成 &lt;code&gt;2023-12-25 13:30:00&lt;/code&gt; 後再保存起來；而取出來後的 &lt;code&gt;2023-12-25 13:30:00&lt;/code&gt; 則會直接加上 JVM 的時區 +8 後還原成時間為 &lt;code&gt;2023-12-25 13:30:00+8&lt;/code&gt; 的 &lt;code&gt;OffsetDateTime&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;採取這樣的機制的確可以避開 MySQL JDBC Driver 不支援 &lt;code&gt;TIMESTAMP_WITH_TIMEZONE&lt;/code&gt; 的問題，但除非符合以下三個條件，否則時區資訊還是有可能會混亂&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;不使用有日光節約 (daylight saving) 的時區&lt;/li&gt;
&lt;li&gt;所有 JVM instance 的時區都一致&lt;/li&gt;
&lt;li&gt;永遠不會改變時區&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="hibernate-6-的-timezonestoragetype-設定"&gt;Hibernate 6 的 &lt;code&gt;TimeZoneStorageType&lt;/code&gt; 設定&lt;/h3&gt;
&lt;p&gt;這三個條件其實某種程度上是很嚴苛的，畢竟仍然有很多地區在使用日光節約時間，所以 Hibernate 6 就提出更進階的設定，讓使用者可以自己決定要如何將帶時區的時間 mapping 到資料庫的無時區時間資料欄位上&lt;/p&gt;
&lt;p&gt;這裡我們可以直接參考 &lt;code&gt;org.hibernate.annotations.TimeZoneStorageType&lt;/code&gt; 這個 enum，其中提供了六種設定選項&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;NATIVE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NORMALIZE&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NORMALIZE_UTC&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;COLUMN&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;AUTO&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DEFAULT&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;首先看名稱就知道是預設值的 &lt;code&gt;DEFAULT&lt;/code&gt;，而 &lt;code&gt;DEFAULT&lt;/code&gt; 到底是採取怎樣的策略呢？這邊我們可以直接參考 &lt;code&gt;DEFAULT&lt;/code&gt; 的說明&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Stores the time zone either with &lt;code&gt;NATIVE&lt;/code&gt; if &lt;code&gt;Dialect.getTimeZoneSupport()&lt;/code&gt; is &lt;code&gt;org.hibernate.dialect.TimeZoneSupport.NATIVE&lt;/code&gt;, otherwise uses the &lt;code&gt;NORMALIZE_UTC&lt;/code&gt; strategy.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;簡單來說就是除非有特別設定，預設就是 &lt;code&gt;NORMALIZE_UTC&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;那 &lt;code&gt;NORMALIZE_UTC&lt;/code&gt; 又是怎麼運作呢？這邊先講 &lt;code&gt;NORMALIZE&lt;/code&gt; 是怎麼運作的&lt;/p&gt;
&lt;p&gt;&lt;code&gt;NORMALIZE&lt;/code&gt; 的機制基本上就跟 Hibernate 5 的處理方式一樣，只差在可以透過 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 這個參數設定 normalize 過程中要轉換的時區，也就是將帶時區時間轉換成無時區時間時，如果沒有設定 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 則一樣轉成 JVM 時區；若是有設定 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 就轉成 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 設定的時區；反過來要將無時區時間轉回帶時區時間的時候，沒有設定 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 就一樣直接帶上 JVM 時區，若是有設定則直接帶上 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 設定的時區&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;這就是設定 &lt;code&gt;hibernate.jdbc.time_zone&lt;/code&gt; 這個參數後的實際運作機制，如果使用 Spring Boot Data JPA 則是設定 &lt;code&gt;spring.jpa.properties.hibernate.jdbc.time_zone&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;但這樣的機制仍然不能避免處理日光節約時間以及換時區的問題&lt;/p&gt;
&lt;p&gt;所以就有 &lt;code&gt;NORMALIZE_UTC&lt;/code&gt; 的出現，其實機制跟 &lt;code&gt;NORMALIZE&lt;/code&gt; 非常相似，只差在它會強制將帶時區時間轉成 UTC 時間後再去掉時區資訊，轉回來的時候同樣也是直接將無時區資訊加上 UTC 時區&lt;/p&gt;
&lt;p&gt;而因為任何時間在保存的時候都是保存成 UTC 的時間，取出來的時候也是一律轉換成 UTC 時間，所以就解決了日光節約時間以及換時區的問題&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;注意 &lt;code&gt;NORMALIZE_UTC&lt;/code&gt; 直到 Hibernate 6.0.1.Final 才修正成為以上機制，並且直到 Hibernate 6.2+ 後才成為預設處理 database 不支援帶時區時間的機制&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;至此，這個機制的描述已經完全符合我觀測到的現象，但是盡信書不如無書，這邊還是要寫個測試來驗證一下&lt;/p&gt;
&lt;h1 id="測試"&gt;測試&lt;/h1&gt;
&lt;p&gt;首先建立測試的 schema 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;create&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table&lt;/span&gt; dt
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; id &lt;span style="color:#8be9fd;font-style:italic"&gt;bigint&lt;/span&gt; auto_increment &lt;span style="color:#ff79c6"&gt;primary&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;timestamp&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;timestamp&lt;/span&gt;(&lt;span style="color:#bd93f9"&gt;3&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; datetime datetime(&lt;span style="color:#bd93f9"&gt;3&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;設定 Entity 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; jakarta.persistence.*;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.hibernate.annotations.CreationTimestamp;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; java.time.OffsetDateTime;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Entity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Table(name &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;dt&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;DtEntity&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Id
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @GeneratedValue(strategy &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; GenerationType.&lt;span style="color:#50fa7b"&gt;IDENTITY&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; Long id;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @CreationTimestamp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; OffsetDateTime timestamp;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @CreationTimestamp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; OffsetDateTime datetime;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#6272a4"&gt;// getter, setter omitted...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Repository&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; com.example.lab.spring.boot.jpa.persistent.data.DtEntity;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.data.jpa.repository.JpaRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;interface&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;DtRepository&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;extends&lt;/span&gt; JpaRepository&lt;span style="color:#ff79c6"&gt;&amp;lt;&lt;/span&gt;DtEntity, Long&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然後透過正常的 JPA &lt;code&gt;save()&lt;/code&gt; 方式新增記錄&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;var&lt;/span&gt; newDt &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; DtEntity();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dtRepository.&lt;span style="color:#50fa7b"&gt;save&lt;/span&gt;(newDt);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;也透過正常的 JPA &lt;code&gt;findById()&lt;/code&gt; 方式取得資料&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;dtRepository.&lt;span style="color:#50fa7b"&gt;findById&lt;/span&gt;(id).&lt;span style="color:#50fa7b"&gt;ifPresent&lt;/span&gt;(dt &lt;span style="color:#ff79c6"&gt;-&amp;gt;&lt;/span&gt; System.&lt;span style="color:#50fa7b"&gt;out&lt;/span&gt;.&lt;span style="color:#50fa7b"&gt;println&lt;/span&gt;(dt));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;我在執行的時候 JVM 跟 MySQL 的時區都是 +8，但這邊印出取得的資料確定是 UTC 時間如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;DtEntity(id=1, timestamp=2023-12-25T03:30:27.660Z, datetime=2023-12-25T03:30:27.660Z)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但我們想更進一步確定運作得是我們前面提到的機制，所以要在 &lt;code&gt;application.properties&lt;/code&gt; 中再加上以下 log 設定&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-properties" data-lang="properties"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#50fa7b"&gt;logging.level.org.hibernate.SQL&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;DEBUG&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#50fa7b"&gt;logging.level.org.hibernate.type&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;TRACE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#50fa7b"&gt;logging.level.org.hibernate.orm.jdbc.bind&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;DEBUG&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#50fa7b"&gt;logging.level.org.hibernate.orm.results&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;DEBUG&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#50fa7b"&gt;logging.level.org.hibernate.resource.jdbc.internal.ResourceRegistryStandardImpl&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;TRACE&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;於是我們可以在 console 中看到以下這行 log 可以證實在 Hibernate 這層處理的時候就已經將時間轉換成 UTC 時間並轉成字串做 insert&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:52.418+08:00 TRACE 29184 --- [nio-8080-exec-1] o.h.r.j.i.ResourceRegistryStandardImpl : Releasing statement [HikariProxyPreparedStatement@1053261670 wrapping com.mysql.cj.jdbc.ClientPreparedStatement: insert into dt (datetime,timestamp) values (&amp;#39;2023-12-25 03:31:52.399456&amp;#39;,&amp;#39;2023-12-25 03:31:52.399456&amp;#39;)]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;另外還可以看到以下幾行 log，此處即可證明 Hibernate 取出來的時候同樣是取得 UTC 時間做綁定&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:56.682+08:00 DEBUG 29184 --- [nio-8080-exec-2] org.hibernate.SQL : select d1_0.id,d1_0.datetime,d1_0.timestamp from dt d1_0 where d1_0.id=?
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:56.682+08:00 TRACE 29184 --- [nio-8080-exec-2] o.h.r.j.i.ResourceRegistryStandardImpl : Registering statement [HikariProxyPreparedStatement@2074824531 wrapping com.mysql.cj.jdbc.ClientPreparedStatement: select d1_0.id,d1_0.datetime,d1_0.timestamp from dt d1_0 where d1_0.id=** NOT SPECIFIED **]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:56.684+08:00 TRACE 29184 --- [nio-8080-exec-2] o.h.r.j.i.ResourceRegistryStandardImpl : Registering result set [HikariProxyResultSet@310872211 wrapping com.mysql.cj.jdbc.result.ResultSetImpl@5b842ce6]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:56.685+08:00 DEBUG 29184 --- [nio-8080-exec-2] o.hibernate.orm.results.loading.entity : (EntityResultInitializer) Hydrated EntityKey (com.example.lab.spring.boot.jpa.persistent.data.DtEntity): 2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:56.686+08:00 DEBUG 29184 --- [nio-8080-exec-2] o.hibernate.orm.results.loading.entity : (EntityResultInitializer) Created new entity instance [com.example.lab.spring.boot.jpa.persistent.data.DtEntity#2] : 1223328396
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:56.686+08:00 DEBUG 29184 --- [nio-8080-exec-2] org.hibernate.orm.results : Extracted JDBC value [1] - [2023-12-25T03:31:52.399Z]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;2023-12-25T11:31:56.686+08:00 DEBUG 29184 --- [nio-8080-exec-2] org.hibernate.orm.results : Extracted JDBC value [2] - [2023-12-25T03:31:52.399Z]
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;可以參考我寫的 &lt;a href="https://github.com/idontwannarock/lab-spring-boot-datetime-with-timezone"&gt;lab-spring-boot-datetime-with-timezon&lt;/a&gt; 範例專案&lt;/p&gt;
&lt;h1 id="結論"&gt;結論&lt;/h1&gt;
&lt;p&gt;原則上最好的情況就是 server 端不論接收、傳送或是計算時間都採用 UTC 時區為準，才能最大程度保證時間資料的正確性，而不同時區的變換則在 client 端依照需求自行轉換；除非有需要依照不同 client 端時區做相對時間 (例如本週、本月等) 的計算才會需要依照 client 提供的時區做某種程度的轉換，但也應該最終都要轉成 UTC 時區時間做最終計算，因此不論在後端或 DB 保存上，也都最好一致採用 UTC 做交互及保存&lt;/p&gt;
&lt;p&gt;而由前面的討論可以得知原則上 Hibernate 預設已經是採取這樣的方案，但如果有需要越過 JPA/Hibernate 去使用到 JDBC API 的話，建議依照 MySQL JDBC Driver 不同的版本，在 JDBC Url 上加上設定 session timezone 的參數，如在 MySQL JDBC Driver 8.0.23 之後的版本可以加上 &lt;code&gt;connectionTimeZone=UTC&lt;/code&gt; 及 &lt;code&gt;forceConnectionTimeZoneToSession=TRUE&lt;/code&gt; 兩項參數，來確保 JDBC 在保存及取得 &lt;code&gt;TIMESTAMP&lt;/code&gt; 或 &lt;code&gt;DATETIME&lt;/code&gt; 資料型態時都會維持 UTC 時區時間&lt;/p&gt;
&lt;p&gt;而 server 端最好也能設定 JVM 時區為 UTC，而不要依賴 OS 的時區，比較能保證運行的穩定性&lt;/p&gt;
&lt;p&gt;另外就是要考量是否會直接使用到 JDBC API 或只會只用 JPA 介面，如果要使用到 JDBC API 則考慮使用 &lt;code&gt;ZonedDateTime&lt;/code&gt; 或 &lt;code&gt;OffsetDateTime&lt;/code&gt; 來做 MySQL &lt;code&gt;TIMESTAMP&lt;/code&gt; 或 &lt;code&gt;DATETIME&lt;/code&gt; 的 mapping，如果只使用到 JPA，那可以考慮選用 &lt;code&gt;Instant&lt;/code&gt; 會讓時區轉換更單純&lt;/p&gt;
&lt;p&gt;但以上設定最好都是在專案或環境剛開始的時候就能設定好，如果是 legacy 專案環境，就只能視狀況做不同的調整，而不能一概而論&lt;/p&gt;
&lt;h1 id="reference"&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/datetime.html"&gt;11.2.2 The DATE, DATETIME, and TIMESTAMP Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html#time-zone-variables"&gt;5.1.15 MySQL Server Time Zone Support#Time Zone Variables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#basic-temporal"&gt;2.2.48. Handling temporal data&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/connector-j/en/connector-j-time-instants.html"&gt;6.6.1 Preserving Time Instants&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/date-and-time-literals.html"&gt;9.1.3 Date and Time Literals&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/time-zone-support.html"&gt;5.1.15 MySQL Server Time Zone Support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-datetime-types-processing.html"&gt;6.3.11 Datetime types processing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.jboss.org/hibernate/orm/6.2/userguide/html_single/Hibernate_User_Guide.html#settings-jdbc"&gt;A.3 JDBC Settings&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.jboss.org/hibernate/orm/6.2/javadocs/org/hibernate/cfg/JdbcSettings.html#JDBC_TIME_ZONE"&gt;JDBC_TIME_ZONE&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.oracle.com/javase/8/docs/api/java/sql/JDBCType.html"&gt;JDBCType&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://in.relation.to/2016/09/12/jdbc-time-zone-configuration-property/"&gt;How to store timestamps in UTC using the new hibernate.jdbc.time_zone configuration property&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thorben-janssen.com/hibernate-jpa-date-and-time/#Working_with_ZonedDateTime"&gt;Working with ZonedDateTime&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://thorben-janssen.com/hibernate-6-offsetdatetime-and-zoneddatetime/"&gt;TimezoneStorageType – Hibernate’s improved timezone mapping&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2023/12/java_time_mapping_mysql_datetime_timezone/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>MySQL 8.0 使用 UTC_TIMESTAMP() 當作 DATETIME 欄位預設值 bug</title><link>https://blog.idontwannarock.dev/2023/10/mysql_8_utc_timestamp_as_datetime_default_bug/</link><pubDate>Tue, 31 Oct 2023 11:59:03 +0800</pubDate><guid>https://blog.idontwannarock.dev/2023/10/mysql_8_utc_timestamp_as_datetime_default_bug/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2023/10/mysql_8_utc_timestamp_as_datetime_default_bug/ -&lt;h1 id="情境描述"&gt;情境描述&lt;/h1&gt;
&lt;p&gt;最近在處理 MySQL 的時候採到一個坑&lt;/p&gt;
&lt;p&gt;我的使用情境是需要一個 create date 紀錄資料產生時的 UTC 時間精準到 millisecond&lt;/p&gt;
&lt;p&gt;首先考慮到 MySQL 的 &lt;code&gt;TIMESTAMP&lt;/code&gt; 資料型態是採用 epoch second 加上額外的 fractional seconds 精準到 microseconds，也就是最多記錄到 &amp;lsquo;2038-01-19 03:14:07.999999&amp;rsquo; 就會 overflow，如果採用 &lt;code&gt;DATETIME(3)&lt;/code&gt; 可以記錄到 &amp;lsquo;9999-12-31 23:59:59.999&amp;rsquo;&lt;/p&gt;
&lt;p&gt;接著因為欄位必填，所以考慮設定預設值，此時有幾種選擇，&lt;code&gt;NOW()&lt;/code&gt;、&lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt;、&lt;code&gt;UTC_TIMESTAMP&lt;/code&gt;，其中 &lt;code&gt;NOW()&lt;/code&gt; 與 &lt;code&gt;CURRENT_TIMESTAMP&lt;/code&gt; 都會依照 MySQL 系統時區產生時間，但是 &lt;code&gt;DATETIME&lt;/code&gt; 並不保存時區資訊，所以為了避免時區問題，所以選擇預設值設定為 &lt;code&gt;(UTC_TIMESTAMP(3))&lt;/code&gt;，剛好在 8.0.13 開始可以將 &lt;code&gt;DATETIME&lt;/code&gt; 資料型態的欄位預設值設為 function &lt;code&gt;(UTC_TIMESTAMP())&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;但是設定完後發生了很奇怪的問題，後續進行的 &lt;code&gt;ALTER TABLE&lt;/code&gt; DDL 操作都會失敗並拋出 &lt;code&gt;ERROR 1067: Invalid default value for &amp;lt;columnName&amp;gt;&lt;/code&gt; 錯誤，例如執行以下 sql&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;ALTER&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE&lt;/span&gt; tableName &lt;span style="color:#ff79c6"&gt;ADD&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;COLUMN&lt;/span&gt; newColumnName &lt;span style="color:#8be9fd;font-style:italic"&gt;BIGINT&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;NULL&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;明明新增的是 &lt;code&gt;BIGINT&lt;/code&gt; 而且沒有預設值，卻拋出 &lt;code&gt;ERROR 1067: Invalid default value for createdDateColumn&lt;/code&gt; 的錯誤，簡直莫名其妙讓人丈二和尚摸不著頭腦&lt;/p&gt;
&lt;h1 id="bug-狀況"&gt;Bug 狀況&lt;/h1&gt;
&lt;p&gt;經過確認是 MySQL 在 8.0.23 以前的 bug，在 8.0.24 之後已經修正，可以參考這張 &lt;a href="https://bugs.mysql.com/bug.php?id=101486"&gt;bug report&lt;/a&gt; 的描述&lt;/p&gt;
&lt;p&gt;簡單來說就是如果在 table 中有欄位的預設值不是 constant，例如我使用的 &lt;code&gt;(UTC_TIMESTAMP(3))&lt;/code&gt;，則後續的 &lt;code&gt;ALTER TABLE&lt;/code&gt; DDL 操作都會失敗並拋出 1067 錯誤&lt;/p&gt;
&lt;h1 id="重現步驟"&gt;重現步驟&lt;/h1&gt;
&lt;p&gt;建議使用 docker 啟動一個 8.0.13-8.0.23 之間版本的 MySQL&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker run --name some-mysql -e &lt;span style="color:#8be9fd;font-style:italic"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;my-secret-pw -d mysql:8.0.23
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然後連上 MySQL&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;docker &lt;span style="color:#8be9fd;font-style:italic"&gt;exec&lt;/span&gt; -it some-mysql bash
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;mysql -uroot -p
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;建立 table&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;use test;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;create&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table&lt;/span&gt; test_timestamp (id &lt;span style="color:#8be9fd;font-style:italic"&gt;int&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt; auto_increment, created datetime &lt;span style="color:#ff79c6"&gt;not&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;default&lt;/span&gt; (utc_timestamp()), &lt;span style="color:#ff79c6"&gt;primary&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;key&lt;/span&gt; (id));
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此時執行 &lt;code&gt;ALTER TABLE&lt;/code&gt; DDL&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;alter&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;table&lt;/span&gt; test_timestamp &lt;span style="color:#ff79c6"&gt;add&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;column&lt;/span&gt; test &lt;span style="color:#8be9fd;font-style:italic"&gt;varchar&lt;/span&gt;(&lt;span style="color:#bd93f9"&gt;63&lt;/span&gt;) &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;應該就會看到 &lt;code&gt;ERROR 1067: Invalid default value for created&lt;/code&gt; 的錯誤訊息&lt;/p&gt;
&lt;h1 id="解決方式"&gt;解決方式&lt;/h1&gt;
&lt;p&gt;以我碰到的情況而言其實有兩種解決方式&lt;/p&gt;
&lt;p&gt;第一種就是乖乖升級，bug 自解&lt;/p&gt;
&lt;p&gt;第二種就是 bug report 裡面有人提出來的 workaround 方式&lt;/p&gt;
&lt;p&gt;流程大致上就是把原本設定為 &lt;code&gt;UTC_TIMESTAMP()&lt;/code&gt; function 的欄位預設值改為 &lt;code&gt;CURRENT_TIMESTAMP()&lt;/code&gt; function，然後執行原本預定要執行的 &lt;code&gt;ALTER TABLE&lt;/code&gt; DDL，最後再把 &lt;code&gt;CURRENT_TIMESTAMP()&lt;/code&gt; 的欄位型態改回 &lt;code&gt;UTC_TIMESTAMP()&lt;/code&gt; function 即可&lt;/p&gt;
&lt;p&gt;這兩個方式我都實測過可行，但各有需要考慮的狀況&lt;/p&gt;
&lt;p&gt;第一種方式當然是怕升級過程有 downtime 或甚至其他升級失敗的狀況，這主要就考驗公司 SRE 之類單位的強度&lt;/p&gt;
&lt;p&gt;第二種方式則是要自己考量在改成 &lt;code&gt;CURRENT_TIMESTAMP()&lt;/code&gt; 期間產生髒資料的處理，例如可能新增的一筆資料時區可能不是 UTC 的問題，如果時區是 UTC+X 的還好處理，如果是 UTC-X 的就會很麻煩，可能要預先做其他處理，例如可能要額外新增時區欄位&lt;/p&gt;
&lt;h1 id="reference"&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/refman/8.0/en/datetime.html"&gt;11.2.2 The DATE, DATETIME, and TIMESTAMP Types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-13.html#mysqld-8-0-13-data-types"&gt;Changes in MySQL 8.0.13 (2018-10-22, General Availability)#Data Type Notes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bugs.mysql.com/bug.php?id=101486"&gt;Error when UTC_TIMESTAMP set as default value&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-24.html#mysqld-8-0-24-bug"&gt;Changes in MySQL 8.0.24 (2021-04-20, General Availability)#Bug Fixed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2023/10/mysql_8_utc_timestamp_as_datetime_default_bug/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>Spring JPA Projection with Converter</title><link>https://blog.idontwannarock.dev/2023/09/spring_jpa_projection_converter/</link><pubDate>Wed, 20 Sep 2023 09:37:21 +0800</pubDate><guid>https://blog.idontwannarock.dev/2023/09/spring_jpa_projection_converter/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2023/09/spring_jpa_projection_converter/ -&lt;h1 id="pre-requisite"&gt;Pre-requisite&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;JDK 11+&lt;/li&gt;
&lt;li&gt;Spring Boot 2+&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="情境描述"&gt;情境描述&lt;/h1&gt;
&lt;p&gt;使用 JPA 做單表查詢非常方便，但有時候因為效能考量，會出現 DB data type 與 Java data type 無法直接對應或需要做一些客製化調整&lt;/p&gt;
&lt;p&gt;例如在保存狀態時，因為狀態通常有固定且少量的值，所以會考慮採用 &lt;code&gt;int&lt;/code&gt; 而非 &lt;code&gt;varchar&lt;/code&gt; 來保存，在存儲大小跟查詢效能上都較優&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;CREATE&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE&lt;/span&gt; CHATROOM
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;`&lt;/span&gt;STATUS&lt;span style="color:#ff79c6"&gt;`&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;INT&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;NOT&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但在程式面都採用 Java 的 &lt;code&gt;int&lt;/code&gt; 來操作狀態，可讀性相對較低，所以可能會考慮採用 &lt;code&gt;enum&lt;/code&gt; 來操作，可讀性較佳且有 type check&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;enum&lt;/span&gt; ChatroomStatus {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CLOSED, ACTIVE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="設定-converter"&gt;設定 Converter&lt;/h1&gt;
&lt;p&gt;但 JPA 預設並不能將 Java &lt;code&gt;enum&lt;/code&gt; 與 MySQL &lt;code&gt;int&lt;/code&gt; 做 mapping，所以會需要提供 converter 讓 JPA 知道怎麼做轉換&lt;/p&gt;
&lt;p&gt;首先需要調整 &lt;code&gt;enum&lt;/code&gt; 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;enum&lt;/span&gt; ChatroomStatus {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; CLOSED(0),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ACTIVE(1);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;final&lt;/span&gt; &lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; value;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ChatroomStatus(&lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; value) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;this&lt;/span&gt;.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; value;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;value&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;this&lt;/span&gt;.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;static&lt;/span&gt; ChatroomStatus &lt;span style="color:#50fa7b"&gt;of&lt;/span&gt;(&lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; value) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;for&lt;/span&gt; (ChatroomStatus target : ChatroomStatus.&lt;span style="color:#50fa7b"&gt;values&lt;/span&gt;()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; (target.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;==&lt;/span&gt; value) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; target;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;接著要提供 converter&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; jakarta.persistence.AttributeConverter;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; jakarta.persistence.Converter;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Converter(autoApply &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;ChatroomStatusConverter&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;implements&lt;/span&gt; AttributeConverter&lt;span style="color:#ff79c6"&gt;&amp;lt;&lt;/span&gt;ChatroomStatus, Integer&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Override
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; Integer &lt;span style="color:#50fa7b"&gt;convertToDatabaseColumn&lt;/span&gt;(ChatroomStatus attribute) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; (attribute &lt;span style="color:#ff79c6"&gt;==&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; attribute.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Override
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; ChatroomStatus &lt;span style="color:#50fa7b"&gt;convertToEntityAttribute&lt;/span&gt;(Integer dbData) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; (dbData &lt;span style="color:#ff79c6"&gt;==&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; ChatroomStatus.&lt;span style="color:#50fa7b"&gt;of&lt;/span&gt;(dbData);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;最後提供 entity&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Entity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Table(name &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;CHATROOM&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;ChatroomDo&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @NotNull
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Column(name &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;STATUS&amp;#34;&lt;/span&gt;, nullable &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;false&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; ChatroomStatus status;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;就可以正常使用 repository 做 CRUD 操作，而同時在 DB 當中 STATUS 為 &lt;code&gt;0&lt;/code&gt; 代表 chatroom closed、&lt;code&gt;1&lt;/code&gt; 代表 chatroom active&lt;/p&gt;
&lt;h1 id="第二種情境描述"&gt;第二種情境描述&lt;/h1&gt;
&lt;p&gt;有時候我們會需要做一些複雜查詢，尤其是使用到其他 table 做 join 查詢，而且 select 的欄位不一定只選擇一個 entity 包含的欄位，此時就會需要利用 JPA 的 projection 功能&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;有些情況可以用關聯來處理，但以下假設並沒有在 JPA 設定關聯，而是直接用 native SQL join 的方式做查詢&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;例如以下 table&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sql" data-lang="sql"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;CREATE&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;TABLE&lt;/span&gt; MESSAGE
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;TYPE&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;INT&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;NOT&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;NULL&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; REPLIED_MESSAGE_ID &lt;span style="color:#8be9fd;font-style:italic"&gt;BIGINT&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;對應的 &lt;code&gt;MessageDo&lt;/code&gt; entity 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Entity
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Table(name &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;MESSAGE&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;MessageDo&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Column(name &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;TYPE&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; MessageType type;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Column(name &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;REPLIED_MESSAGE_ID&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; Long repliedMessageId;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;其中 &lt;code&gt;MessageType&lt;/code&gt; 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;enum&lt;/span&gt; MessageType {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; TEXT(1),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; IMAGE(2),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;final&lt;/span&gt; &lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; value;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MessageType(&lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; value) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;this&lt;/span&gt;.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; value;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;value&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;this&lt;/span&gt;.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;static&lt;/span&gt; MessageType &lt;span style="color:#50fa7b"&gt;of&lt;/span&gt;(&lt;span style="color:#8be9fd"&gt;int&lt;/span&gt; value) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;for&lt;/span&gt; (MessageType target : MessageType.&lt;span style="color:#50fa7b"&gt;values&lt;/span&gt;()) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; (target.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt;() &lt;span style="color:#ff79c6"&gt;==&lt;/span&gt; value) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; target;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;按照慣例，提供 converter 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; jakarta.persistence.AttributeConverter;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; jakarta.persistence.Converter;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Converter(autoApply &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;MessageTypeConverter&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;implements&lt;/span&gt; AttributeConverter&lt;span style="color:#ff79c6"&gt;&amp;lt;&lt;/span&gt;MessageType, Integer&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Override
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; Integer &lt;span style="color:#50fa7b"&gt;convertToDatabaseColumn&lt;/span&gt;(MessageType attribute) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; (attribute &lt;span style="color:#ff79c6"&gt;==&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; attribute.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Override
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; MessageType &lt;span style="color:#50fa7b"&gt;convertToEntityAttribute&lt;/span&gt;(Integer dbData) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; (dbData &lt;span style="color:#ff79c6"&gt;==&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; MessageType.&lt;span style="color:#50fa7b"&gt;of&lt;/span&gt;(dbData);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此時若需要取得一個 message 以及其回覆 message 的內容，則 JPA query method 就無法 return MessageDo，所以此時我們採用 JPA interface-based projection 來達成此操作，首先按照官方 doc 提供 interface 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;interface&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;MessageViewDo&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MessageType &lt;span style="color:#50fa7b"&gt;getType&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Long &lt;span style="color:#50fa7b"&gt;getRepliedMessageId&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; MessageType &lt;span style="color:#50fa7b"&gt;getRepliedMessageType&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;然後是 query 如下&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;interface&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;MessageDao&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;extends&lt;/span&gt; JpaRepository&lt;span style="color:#ff79c6"&gt;&amp;lt;&lt;/span&gt;MessageDo, Long&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Query(value &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;SELECT &amp;#34;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34; m.TYPE, &amp;#34;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34; rm.ID AS repliedMessageId, &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34; rm.TYPE AS repliedMessageType, &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ...
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;FROM MESSAGE m &amp;#34;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;LEFT JOIN MESSAGE rm ON m.REPLIED_MESSAGE_ID = rm.ID &amp;#34;&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;+&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;WHERE m.ID = :id&amp;#34;&lt;/span&gt;, nativeQuery &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;true&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Optional&lt;span style="color:#ff79c6"&gt;&amp;lt;&lt;/span&gt;MessageViewDo&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;findMessageViewById&lt;/span&gt;(Long id);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;此時當我們使用的時候，會開心地發現取出來的 &lt;code&gt;MessageType&lt;/code&gt; 值有問題，要不是取出來的值不對，就是會報 &lt;code&gt;org.springframework.core.convert.ConversionFailedException&lt;/code&gt; 的錯誤，仔細看 cause 還是 &lt;code&gt;Caused by: java.lang.ArrayIndexOutOfBoundsException&lt;/code&gt;？&lt;/p&gt;
&lt;p&gt;這是因為 JPA 的 interface-based projection 實際上是用 proxy 來包住取得的 result，等到使用者要使用到物件的時候才做轉換，而且因為我們做的是 open projection (projection 的結果並非基於 entity class)，所以 JPA 無法像 closed projection (projection 結果基於 entity) 那樣從原始的 entity 身上取得 metadata 做轉換，所以只能用 Spring 預設的 converter 來做轉換&lt;/p&gt;
&lt;p&gt;是的，因為無法取得 entity 的 metadata，所以此時使用的不是 JPA 的 converter 而是 Spring Core 自帶的 converter，而我們並沒有將我們自定義的 Spring converter 註冊給 Spring DI container，所以它只會用預設的方式去做 &lt;code&gt;int&lt;/code&gt; -&amp;gt; &lt;code&gt;enum&lt;/code&gt; 的轉換&lt;/p&gt;
&lt;p&gt;而 Spring 預設對 &lt;code&gt;int&lt;/code&gt; -&amp;gt; &lt;code&gt;enum&lt;/code&gt; 的轉換是依照 &lt;code&gt;enum&lt;/code&gt; 的 ordinal (順序) 做轉換，而且 ordinal 是從 0 開始，所以你可能會很容易看到轉換出來的 &lt;code&gt;MessageType&lt;/code&gt; 比 DB 中的剛好大一號，例如 DB 中的 &lt;code&gt;TYPE&lt;/code&gt; 是 &lt;code&gt;1&lt;/code&gt; 應該代表的是 &lt;code&gt;TEXT&lt;/code&gt;，但取出來卻變成 &lt;code&gt;IMAGE&lt;/code&gt; 這樣的情況；更甚者，如果 DB 的值剛好大於等於 &lt;code&gt;enum&lt;/code&gt; 的 size 就會報 &lt;code&gt;org.springframework.core.convert.ConversionFailedException&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;因此，很自然的我們就應該要提供 converter 給 Spring&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.core.convert.converter.Converter;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.lang.NonNull;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; java.util.Arrays;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;MessageTypeConverter&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;implements&lt;/span&gt; Converter&lt;span style="color:#ff79c6"&gt;&amp;lt;&lt;/span&gt;Integer, MessageType&lt;span style="color:#ff79c6"&gt;&amp;gt;&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Override
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; MessageType &lt;span style="color:#50fa7b"&gt;convert&lt;/span&gt;(@NonNull Integer source) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;if&lt;/span&gt; (Arrays.&lt;span style="color:#50fa7b"&gt;stream&lt;/span&gt;(MessageType.&lt;span style="color:#50fa7b"&gt;values&lt;/span&gt;()).&lt;span style="color:#50fa7b"&gt;anyMatch&lt;/span&gt;(target &lt;span style="color:#ff79c6"&gt;-&amp;gt;&lt;/span&gt; target.&lt;span style="color:#50fa7b"&gt;value&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;==&lt;/span&gt; source)) {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; MessageType.&lt;span style="color:#50fa7b"&gt;of&lt;/span&gt;(source);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;但這時候問題就來了，要怎麼註冊這個 converter？這個時候就要看 Spring Boot 的版本了&lt;/p&gt;
&lt;h2 id="spring-boot-240-以前"&gt;Spring Boot 2.4.0 以前&lt;/h2&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.boot.context.event.ApplicationReadyEvent;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.context.annotation.Configuration;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.context.event.EventListener;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.core.convert.support.DefaultConversionService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Configuration
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;ConverterConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @EventListener(ApplicationReadyEvent.&lt;span style="color:#50fa7b"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd"&gt;void&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;config&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; DefaultConversionService conversionService &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; (DefaultConversionService) DefaultConversionService.&lt;span style="color:#50fa7b"&gt;getSharedInstance&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; conversionService.&lt;span style="color:#50fa7b"&gt;addConverter&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; MessageTypeConverter());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="spring-boot-240-以後"&gt;Spring Boot 2.4.0 以後&lt;/h2&gt;
&lt;p&gt;因為 Spring Boot 2.4.0 以後沒辦法用 &lt;code&gt;DefaultConversionService.getSharedInstance()&lt;/code&gt; 的方式，所以最後參考了 &lt;a href="https://stackoverflow.com/a/65355897"&gt;這裡&lt;/a&gt; 提供的方式，用反射來註冊 converter&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.boot.context.event.ApplicationReadyEvent;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.context.annotation.Configuration;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.context.event.EventListener;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; org.springframework.core.convert.support.GenericConversionService;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;import&lt;/span&gt; java.lang.reflect.Field;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Configuration
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;JpaProjectionConverterConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @EventListener(ApplicationReadyEvent.&lt;span style="color:#50fa7b"&gt;class&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd"&gt;void&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;offsetDateTimeConversion&lt;/span&gt;() &lt;span style="color:#8be9fd;font-style:italic"&gt;throws&lt;/span&gt; ClassNotFoundException, NoSuchFieldException, IllegalAccessException {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Class&lt;span style="color:#ff79c6"&gt;&amp;lt;?&amp;gt;&lt;/span&gt; aClass &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; Class.&lt;span style="color:#50fa7b"&gt;forName&lt;/span&gt;(&lt;span style="color:#f1fa8c"&gt;&amp;#34;org.springframework.data.projection.ProxyProjectionFactory&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; Field field &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; aClass.&lt;span style="color:#50fa7b"&gt;getDeclaredField&lt;/span&gt;(&lt;span style="color:#f1fa8c"&gt;&amp;#34;CONVERSION_SERVICE&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; field.&lt;span style="color:#50fa7b"&gt;setAccessible&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;true&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; GenericConversionService service &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; (GenericConversionService) field.&lt;span style="color:#50fa7b"&gt;get&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;null&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; service.&lt;span style="color:#50fa7b"&gt;addConverter&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; MessageTypeConverter());
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;如此就可以利用框架自動 mapping Java &lt;code&gt;enum&lt;/code&gt; 與 MySQL &lt;code&gt;int&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;另外提一下，Java 的 &lt;code&gt;Instant&lt;/code&gt; 或 &lt;code&gt;OffsetDateTime&lt;/code&gt; 與 MySQL &lt;code&gt;datetime&lt;/code&gt; 之間也有類似的問題，所以也需要提供一個 converter 做 &lt;code&gt;Instant&lt;/code&gt; 或 &lt;code&gt;OffsetDateTime&lt;/code&gt; 與 &lt;code&gt;java.sql.Timestamp&lt;/code&gt; 之間做轉換&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;JPA 會把 MySQL 的 &lt;code&gt;datetime&lt;/code&gt; 以 &lt;code&gt;java.sql.Timestamp&lt;/code&gt; 取出來&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h1 id="reference"&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#projections"&gt;Spring Data JPA - Reference Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/a/65355897"&gt;Spring JPA - &amp;ldquo;java.lang.IllegalArgumentException: Projection type must be an interface!&amp;rdquo; (using native query)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2023/09/spring_jpa_projection_converter/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>Scoop 介紹及用法</title><link>https://blog.idontwannarock.dev/2023/06/scoop/</link><pubDate>Tue, 20 Jun 2023 10:51:39 +0800</pubDate><guid>https://blog.idontwannarock.dev/2023/06/scoop/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2023/06/scoop/ -&lt;p&gt;Linux 有 &lt;a href="https://salsa.debian.org/apt-team/apt"&gt;apt&lt;/a&gt;, &lt;a href="http://yum.baseurl.org/gitweb/"&gt;yum&lt;/a&gt;、MacOS 有 &lt;a href="https://brew.sh/"&gt;Homebrew&lt;/a&gt;、Windows 有 &lt;a href="https://chocolatey.org/"&gt;Chocolatey&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但今天要介紹 Windows 環境的另外一個 command line 套件管理工具 &lt;a href="https://scoop.sh/"&gt;Scoop&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;在 Windows 環境，比較有名的 command line 套件管理工具是 Chocolatey，但今天要介紹的 Scoop 有一項 Chocolatey 沒辦法做到的好處，就是可以切換 Path Environment Variable&lt;/p&gt;
&lt;h1 id="安裝"&gt;安裝&lt;/h1&gt;
&lt;p&gt;Scoop 的安裝非常簡單，打開 &lt;strong&gt;非&lt;/strong&gt; Administrator (系統管理員) 的 Powershell，並輸入以下指令即可安裝&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;Set-ExecutionPolicy&lt;/span&gt; RemoteSigned -Scope CurrentUser
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;irm &lt;/span&gt;get.scoop.sh | &lt;span style="color:#8be9fd;font-style:italic"&gt;iex
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="使用"&gt;使用&lt;/h1&gt;
&lt;p&gt;Scoop 既然是套件管理工具，那重要的工作就是安裝套件，而 Scoop 有支援什麼套件固然可以直接 Google 的到，但其實也不是沒有方式直接在 command line 搜尋&lt;/p&gt;
&lt;p&gt;例如想安裝 7zip 壓縮工具，可以在 powershell 當中這樣搜尋&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop search 7zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;應該會出現如下的結果&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230278/Tech%20Blog/Scoop/image-20230620-014307.png" alt="Scoop Search Result"&gt;&lt;/p&gt;
&lt;p&gt;第一行說明代表這次結果是在本地的 bucket 當中搜尋到的，表格當中的 Name 就是套件名稱，之後所有操作都要輸入 Name 當中顯示的完整名稱，Source 就是指套件所在的 bucket&lt;/p&gt;
&lt;p&gt;這邊如果你不確定是哪個套件名稱，也可以用以下指令看一下簡介，例如&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop info 7zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230369/Tech%20Blog/Scoop/image-20230620-014436.png" alt="Scoop Info Result"&gt;&lt;/p&gt;
&lt;p&gt;這時候確定要安裝哪一個套件名稱，這時候就可以輸入以下指令做安裝&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop install 7zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這時候 console 會顯示經過一連串的操作，最後看到這一行就代表安裝成功&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230433/Tech%20Blog/Scoop/image-20230620-014915.png" alt="Scoop Installation Result"&gt;&lt;/p&gt;
&lt;p&gt;如果要解除安裝也是簡單輸入以下指令&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop uninstall 7zip
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230487/Tech%20Blog/Scoop/image-20230620-015010.png" alt="Scoop Uninstallation Result"&gt;&lt;/p&gt;
&lt;p&gt;如果想查詢本地安裝了哪些套件&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230536/Tech%20Blog/Scoop/image-20230620-021418.png" alt="Scoop Local Installed App Result"&gt;&lt;/p&gt;
&lt;p&gt;也可以搜尋本地安裝的套件名稱&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop list maven
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230599/Tech%20Blog/Scoop/image-20230620-021507.png" alt="Scoop Search Local Installed App Result"&gt;&lt;/p&gt;
&lt;p&gt;最後如果要更新某個套件，可以輸入&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop update maven
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="bucket"&gt;Bucket&lt;/h2&gt;
&lt;p&gt;接著這邊要了解的是 Scoop 並不像 Chocolatey 的搜尋是在網路上對所有套件做即時搜尋，Scoop 有 bucket 的概念，每個 bucket 都像是一組套件的資料夾&lt;/p&gt;
&lt;p&gt;在安裝 Scoop 時就會預設安裝 main bucket，裡面應該預設就有一千多種常用套件，而 Scoop 的搜尋也是優先在本地已安裝的 bucket 裡面做搜尋，如果本地的 bucket 搜尋不到，才會到網路上其他已知 (known) 的 bucket 中做搜尋&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230679/Tech%20Blog/Scoop/image-20230620-020010.png" alt="Scoop Remote Search Result"&gt;&lt;/p&gt;
&lt;p&gt;所以首先想要確認本地安裝了哪些 bucket 就輸入以下指令&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop bucket list
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230729/Tech%20Blog/Scoop/image-20230620-015142.png" alt="Scoop Local Installed Bucket Result"&gt;&lt;/p&gt;
&lt;p&gt;如果想查詢有哪些已知的 bucket (本地及遠端)&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop bucket known
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230786/Tech%20Blog/Scoop/image-20230620-015400.png" alt="Scoop Known Bucket Result"&gt;&lt;/p&gt;
&lt;p&gt;但這個列表也許已過時，這時候可以輸入以下指令更新 Scoop 後，再做查詢&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop update
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230834/Tech%20Blog/Scoop/image-20230620-015513.png" alt="Scoop Update Result"&gt;&lt;/p&gt;
&lt;p&gt;如果想安裝指定 bucket 到本地&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop bucket add games
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230885/Tech%20Blog/Scoop/image-20230620-020309.png" alt="Scoop Add Bucket Result"&gt;&lt;/p&gt;
&lt;p&gt;移除 bucket&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop bucket &lt;span style="color:#8be9fd;font-style:italic"&gt;rm &lt;/span&gt;games
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以上差不多就是 Scoop 常見的基本用法&lt;/p&gt;
&lt;h1 id="jdk-切換"&gt;JDK 切換&lt;/h1&gt;
&lt;p&gt;有一種情境我們會安裝某一種套件，但要同時安裝多種版本，使用的時候需要切換不同的版本，例如安裝 JDK，可能會需要同時安裝 1.8、11 甚至 17，但使用的時候會需要按照需要執行的專案來切換不同版本的環境變數&lt;/p&gt;
&lt;p&gt;在 Unix-like 的環境，大多都可以使用 &lt;a href="https://sdkman.io/"&gt;SDKMAN&lt;/a&gt;，但偏偏 SDKMAN 原生並不支援在 Powershell 操作，只能透過 &lt;a href="https://www.cygwin.com/"&gt;Cygwin&lt;/a&gt; 等方式 workaround，但同時也限制環境變數只在 Cygwin 當中生效，而無法使用 Powershell 操作&lt;/p&gt;
&lt;p&gt;如果要在 Powershell 中操作，以往只能安裝好 JDK 後，再設定環境變數 JAVA_HOME 指定到安裝好的位置，然後每次要使用不同 JDK 版本，就要手動修改 JAVA_HOME 環境變數，或輸入不太好記的指令去操作，如果 JDK 版本更新，又要整個重頭再來，相當的麻煩&lt;/p&gt;
&lt;p&gt;但 Scoop 本身提供了相當方便的指令，可以如 SDKMAN 一樣簡單的安裝多種不同版本的 JDK，並且在需要的時候非常簡單的切換不同的版本&lt;/p&gt;
&lt;p&gt;首先我們可以先安裝幾種不同版本的 JDK&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop bucket add java
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop install &lt;span style="color:#8be9fd;font-style:italic"&gt;zulu-jdk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop install &lt;span style="color:#8be9fd;font-style:italic"&gt;zulu11-jdk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687230995/Tech%20Blog/Scoop/image-20230620-021710.png" alt="Scoop List Local JDK Result"&gt;&lt;/p&gt;
&lt;p&gt;此時我們本地已安裝了 11 及 20 兩種版本的 JDK，在安裝的時候就會自動設定好環境變數&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687231043/Tech%20Blog/Scoop/image-20230620-021833.png" alt="Java Version after JDKs Installation"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687231095/Tech%20Blog/Scoop/image-20230620-022019.png" alt="JDK Position after JDKs Installation"&gt;&lt;/p&gt;
&lt;p&gt;接著如果我們要切換使用 JDK 20&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-powershell" data-lang="powershell"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;scoop reset &lt;span style="color:#8be9fd;font-style:italic"&gt;zulu-jdk&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687231153/Tech%20Blog/Scoop/image-20230620-022207.png" alt="Scoop Reset JDK Version Result"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687231188/Tech%20Blog/Scoop/image-20230620-022226.png" alt="Java Version after Scoop Reset"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1687231222/Tech%20Blog/Scoop/image-20230620-022245.png" alt="JDK Position after Scoop Reset"&gt;&lt;/p&gt;
&lt;p&gt;這時我們就可以看到 Java 的版本被改變了&lt;/p&gt;
&lt;h1 id="reference"&gt;Reference&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://scoop.sh/"&gt;Scoop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://adamtheautomator.com/scoop-windows/"&gt;How to Install and Use the Scoop Windows Package Manager&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sdkman.io/"&gt;SDKMAN&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.cygwin.com/"&gt;Cygwin&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.thisfaner.com/p/install-and-switch-versions-of-java-python-ruby-via-scoop/"&gt;通过Scoop安装和切换Java(JDK)、Python、Ruby的版本&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2023/06/scoop/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>Gradle + Docker Multi-stage build</title><link>https://blog.idontwannarock.dev/2022/12/gradle_docker_multi_stage_build/</link><pubDate>Thu, 15 Dec 2022 10:18:10 +0800</pubDate><guid>https://blog.idontwannarock.dev/2022/12/gradle_docker_multi_stage_build/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2022/12/gradle_docker_multi_stage_build/ -&lt;p&gt;Java 專案整合 Docker multi-stage build 系列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.idontwannarock.dev/2022/12/maven_docker_multi_stage_build/"&gt;Maven + Docker Multi-stage build&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Gradle + Docker Multi-stage build&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;前一篇講過 Maven 配合 Docker multi-stage build 的優化概念跟實作，這篇順便來講一下 Gradle 的部分&lt;/p&gt;
&lt;h1 id="docker-multi-stage-build"&gt;Docker Multi-stage Build&lt;/h1&gt;
&lt;p&gt;前一篇已經介紹過建置 Java 專案配合 Docker multi-stage build 的優化概念，這邊就不再贅述，直接上 Dockerfile：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM gradle:7.6-alpine AS Cache
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENV GRADLE_USER_HOME /cache
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY build.gradle ./
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN gradle --no-daemon dependencies --stacktrace
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM gradle:7.6-jdk11-alpine AS Builder
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;Cache /cache /home/gradle/.gradle
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY build.gradle settings.gradle ./
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src /opt/app/src
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN gradle --no-daemon build --stacktrace --offline
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM azul/zulu-openjdk-alpine:11-jre-headless
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;Builder /opt/app/build/libs/*.jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#bd93f9"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT &lt;span style="color:#ff79c6"&gt;[&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;app.jar&amp;#34;&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;步驟同樣大致如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;依照 &lt;code&gt;build.gradle&lt;/code&gt; 下載所有 dependencies&lt;/li&gt;
&lt;li&gt;利用前一階段下載好的 dependencies 離線建置專案&lt;/li&gt;
&lt;li&gt;利用前一階段建置好的 jar 檔運行應用程式&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Gradle 單就配合 Docker multi-stage build 的實作方面比 Maven 方便得多，最起碼就沒有 &lt;a href="https://issues.apache.org/jira/browse/MDEP-82"&gt;default lifecycle plugin 的 dependencies 不會自動下載的 bug&lt;/a&gt;，所以直接使用 &lt;code&gt;gradle dependencies&lt;/code&gt; 的指令就可以直接下載到所有建置時需要用到的 dependencies，完全不需要像 Maven 那樣為了要下載 plugin 抓不到的 dependencies 而去做 workaround&lt;/p&gt;
&lt;p&gt;另外同樣要注意盡量選用較精簡的 base image，在 Gradle 這邊就盡量都選用 alpine 結尾的 image&lt;/p&gt;
&lt;h1 id="參考"&gt;參考&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hub.docker.com/_/gradle"&gt;Gradle Docker Hub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://we-are.bookmyshow.com/leveraging-multi-stage-docker-builds-for-your-compiled-applications-c997d17cf4d4"&gt;Leveraging Multi-Stage Docker Builds for Your Compiled Applications&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/a/59022743/7605040"&gt;Slow gradle build in Docker. Caching gradle build&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/a/59133542/7605040"&gt;Gradle dependency caching during docker multi stage build?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/a/50219857/7605040"&gt;gradle - task to only download maven dependencies&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2022/12/gradle_docker_multi_stage_build/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>Maven + Docker Multi-stage Build</title><link>https://blog.idontwannarock.dev/2022/12/maven_docker_multi_stage_build/</link><pubDate>Wed, 14 Dec 2022 14:14:44 +0800</pubDate><guid>https://blog.idontwannarock.dev/2022/12/maven_docker_multi_stage_build/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2022/12/maven_docker_multi_stage_build/ -&lt;p&gt;Java 專案整合 Docker multi-stage build 系列：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Maven + Docker Multi-stage build&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.idontwannarock.dev/2022/12/gradle_docker_multi_stage_build/"&gt;Gradle + Docker Multi-stage build&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;p&gt;現代常見的 CI/CD 流程經常會使用容器化 (containerized) 的方式來幫助建置環境及部署&lt;/p&gt;
&lt;p&gt;以 Java + Maven 專案舉例，一個比較通用的 Dockerfile 可能長這樣：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM maven:3.8-openjdk-11
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/resources /opt/app/src/main/resources
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/java /opt/app/src/main/java
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY pom.xml .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mvn -B -e clean package
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#bd93f9"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT &lt;span style="color:#ff79c6"&gt;[&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;demo.jar&amp;#34;&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這個 Dockerfile 其實沒有問題，完全可以正常運行，但它使用上有一個地方不太方便，就是每次運行到 &lt;code&gt;RUN mvn -B -e clean package&lt;/code&gt; 這行的時候，除非程式碼跟 &lt;code&gt;pom.xml&lt;/code&gt; 都沒有變動，否則所有 dependencies 都會重新下載一遍，如果專案比較大型，那光下載 dependencies 可能就會要花很久的時間&lt;/p&gt;
&lt;h1 id="docker-multi-stage-build"&gt;Docker Multi-stage Build&lt;/h1&gt;
&lt;p&gt;於是就有人使出了 Docker multi-stage build 這招&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM maven:3.8-openjdk-11 as DEPENDENCIES
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY pom.xml .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mvn -B -e -C org.apache.maven.plugins:maven-dependency-plugin:3.3.0:go-offline
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM maven:3.8-openjdk-11 as BUILDER
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;DEPENDENCIES /root/.m2 /root/.m2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/resources /opt/app/src/main/resources
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/java /opt/app/src/main/java
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY pom.xml .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mvn -B -e clean package
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM openjdk:11-jre
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;BUILDER /opt/app/target/*.jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#bd93f9"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT &lt;span style="color:#ff79c6"&gt;[&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;app.jar&amp;#34;&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;簡單來說，在過程中會分成三段建置，第一段會依照 &lt;code&gt;pom.xml&lt;/code&gt; 將所有 dependencies 下載下來；第二段會利用第一段下載的 dependencies 來建置專案；第三段則是運行第二段建置好的專案 jar 檔。&lt;/p&gt;
&lt;p&gt;這樣做有什麼好處呢？主要差別在於「建置時間」跟「最終 image 的大小」&lt;/p&gt;
&lt;h2 id="建置時間"&gt;建置時間&lt;/h2&gt;
&lt;p&gt;Docker 的 image 是由一層一層的 layer 疊加而成，每一層其實都是一個 image，只是中間層的 image 是 read only 而且一般指令是找不到的，只有最上層的 image 是 read/write 都可以的 layer，也才能被操作&lt;/p&gt;
&lt;p&gt;&lt;img src="https://cdn.buttercms.com/CLQJN3yRRcS7oGqm7yKb" alt="Relationship between Dockerfile and layered images"&gt;
&lt;em&gt;source: MetricFire &lt;a href="https://www.metricfire.com/blog/how-to-build-optimal-docker-images/"&gt;How to Build Optimal Docker Images&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;在 Dockerfile 當中的每一行都是一層 layer，每層 layer 都是一個 image，而 Docker 在 build 的過程是會針對 image 做 cache，也就是 Dockerfile 當中每一行運行的指令相關的資源沒有變動，Docker 就會直接使用已有的 image&lt;/p&gt;
&lt;p&gt;以前面的 Dockerfile 來說，如果第三行 &lt;code&gt;COPY pom.xml .&lt;/code&gt; 複製進來的 &lt;code&gt;pom.xml&lt;/code&gt; 沒有變動，則第四行 &lt;code&gt;RUN mvn -B -e -C org.apache.maven.plugins:maven-dependency-plugin:3.3.0:go-offline&lt;/code&gt; 也不會實際運行，而是直接使用前一次建置出來對應 layer 的 image&lt;/p&gt;
&lt;p&gt;利用 Docker 這樣的特性，加上一般開發的時候 &lt;code&gt;pom.xml&lt;/code&gt; 不太會變動的前提下，在大部分使用這份 Dockerfile 做建置的時候，第一階段的步驟都會使用 cache，從而節省每次建置都要重新下載 dependencies 的時間&lt;/p&gt;
&lt;h2 id="image-大小"&gt;Image 大小&lt;/h2&gt;
&lt;p&gt;不知道有多少人留意過你 build 出來的 image 大小？&lt;/p&gt;
&lt;p&gt;前面第一種作法 build 出來的 image 本身會包含了 base image (本身就包含 maven 及 jdk)、程式碼、下載下來的所有 dependencies，以及建置出來的 jar 檔&lt;/p&gt;
&lt;p&gt;其中運行 jar 檔時並不需要 maven，也不需要 JDK 而只需要 JRE，以 Spring Boot 來說，所有運行需要的 dependencies 都已經包在 jar 檔裡面 (fat jar)，所以 image 裡面的 .m2 資料夾也是重複的&lt;/p&gt;
&lt;p&gt;這樣 build 出來的 image 動輒幾百 mb，雖然現在硬碟便宜，但有串 CI/CD 的開發流程經常 dev 分支每 push 一次就建置一個 image，那占用的空間就大了，如果是用雲端服務，那燒的就是老闆的錢&lt;/p&gt;
&lt;p&gt;所以 image 大小 matters！&lt;/p&gt;
&lt;p&gt;先重新順一下前面這份 multi-stage build 的 Dockerfile 流程：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;依照 &lt;code&gt;pom.xml&lt;/code&gt; 下載所有 dependencies&lt;/li&gt;
&lt;li&gt;直接使用前一階段下載好的 dependencies 來建置專案 jar 檔&lt;/li&gt;
&lt;li&gt;直接使用前一階段建置好的 jar 檔來運行&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;從流程中我們可以看到最終產出的 image 本身只帶有 base image 以及建置好的 jar 檔、base image 只包含 headless JRE、layer 只有五層、也不帶有前面階段使用到的 dependencies，甚至也不包含建置需要的 maven，因此最終的 image 的大小就會明顯的下降&lt;/p&gt;
&lt;p&gt;&lt;img src="https://res.cloudinary.com/dcvgho2zc/image/upload/v1671002088/Tech%20Blog/single-multi-stage-build-image-size.png" alt="Comparison between single-stage build and multi-stage build"&gt;&lt;/p&gt;
&lt;h1 id="優化"&gt;優化&lt;/h1&gt;
&lt;h2 id="base-image"&gt;Base Image&lt;/h2&gt;
&lt;p&gt;但其實前面這份 Dockerfile 還有優化空間，比較簡單的就是使用更精簡的 base image，也就是盡量選擇只包含運行該 stage 需要的最少功能的 image&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM maven:3.8-openjdk-11-slim as DEPENDENCIES
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY pom.xml .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mvn -B -e -C org.apache.maven.plugins:maven-dependency-plugin:3.3.0:go-offline
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM maven:3.8-openjdk-11-slim as BUILDER
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;DEPENDENCIES /root/.m2 /root/.m2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/resources /opt/app/src/main/resources
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/java /opt/app/src/main/java
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY pom.xml .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mvn -B -e clean package
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM azul/zulu-openjdk-alpine:11-jre-headless
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;BUILDER /opt/app/target/*.jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#bd93f9"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT &lt;span style="color:#ff79c6"&gt;[&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;app.jar&amp;#34;&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;一般來說帶有 &lt;code&gt;alpine&lt;/code&gt; 或 &lt;code&gt;slim&lt;/code&gt; 結尾的 image 都會是相對比較好的選擇；如果只是要用來運行無 GUI 的 Java 應用程式的話，盡量選擇 &lt;code&gt;headless&lt;/code&gt;&lt;/p&gt;
&lt;h2 id="maven"&gt;Maven&lt;/h2&gt;
&lt;p&gt;如果你有仔細觀察建置過程的 log 的話，應該會發現在運行第 12 行 &lt;code&gt;RUN mvn -B -e clean package&lt;/code&gt; 的時候，maven 還是會下載一些 dependencies&lt;/p&gt;
&lt;p&gt;這就很奇怪啊，照理來說前一階段應該就把全部的 dependencies 都下載下來啦，畢竟這就是 &lt;code&gt;dependency:go-offline&lt;/code&gt; 這個 goal 的目的不是嗎？&lt;/p&gt;
&lt;p&gt;但很抱歉，你要說這是 plugin 的 bug 或是原本的功能都好，總之，它只會下載 &lt;code&gt;pom.xml&lt;/code&gt; 上面有明示列出的 artifact，而且甚至連 &lt;code&gt;&amp;lt;plugin&amp;gt;&lt;/code&gt; 標籤內部的 &lt;code&gt;&amp;lt;dependencies&amp;gt;&lt;/code&gt; 標籤當中的 artifact 都不會下載&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;這個 issue 從 2007 年開到現在都還沒解決，可以參考 &lt;a href="https://issues.apache.org/jira/browse/MDEP-82"&gt;MDEP-82&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;再加上有些 maven 預設 lifecycle 或 goal 需要動態載入的 dependencies 也並不在下載的目標當中，就會造成在第二階段真正開始建置專案的時候會需要下載缺少的 dependencies&lt;/p&gt;
&lt;p&gt;某些 plugin 的 dependencies 甚至不是列在 &lt;code&gt;pom.xml&lt;/code&gt; 當中，而是利用 class loader 的方式超級動態載入，這也是 &lt;code&gt;maven-dependency-plugin&lt;/code&gt; 無能為力的部分&lt;/p&gt;
&lt;p&gt;如果建置過程都有連網還好，有些 CI/CD 的流程在建置這個階段是會跟網路隔離的，或是會利用建置這個步驟順便做測試，但為了測試環境的「乾淨」而限制連網功能，這個情況就很尷尬了，各種建置失敗&lt;/p&gt;
&lt;p&gt;在 plugin 修正之前，只能採取各種 workaround，以下介紹一種比較麻煩，但相對不會變更太多建置步驟或在建置出來的 jar 檔或甚至 git repo 中增加不必要 dependencies 的方式&lt;/p&gt;
&lt;h3 id="workaround"&gt;Workaround&lt;/h3&gt;
&lt;p&gt;首先將 Dockerfile 調整如下：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-sh" data-lang="sh"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM maven:3.8-openjdk-11-slim as DEPENDENCIES
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY pom.xml .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mvn -B -e -C clean org.apache.maven.plugins:maven-dependency-plugin:3.3.0:go-offline
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM maven:3.8-openjdk-11-slim as BUILDER
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;DEPENDENCIES /root/.m2 /root/.m2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/resources /opt/app/src/main/resources
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY src/main/java /opt/app/src/main/java
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY pom.xml .
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;RUN mvn -B -e -o clean package
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;FROM azul/zulu-openjdk-alpine:11-jre-headless
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;WORKDIR /opt/app
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;COPY --from&lt;span style="color:#ff79c6"&gt;=&lt;/span&gt;BUILDER /opt/app/target/*.jar app.jar
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;EXPOSE &lt;span style="color:#bd93f9"&gt;8080&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;ENTRYPOINT &lt;span style="color:#ff79c6"&gt;[&lt;/span&gt;&lt;span style="color:#f1fa8c"&gt;&amp;#34;java&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;-jar&amp;#34;&lt;/span&gt;, &lt;span style="color:#f1fa8c"&gt;&amp;#34;app.jar&amp;#34;&lt;/span&gt;&lt;span style="color:#ff79c6"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;主要改動有兩個部分，一是在第一階段也運行 clean lifecycle 以下載相關的 dependencies；二是在第二階段建置加上 &lt;code&gt;-o&lt;/code&gt; 參數，這是為了讓 maven 在 offline 的狀況下進行建置，也就是強制 maven 不連網&lt;/p&gt;
&lt;p&gt;第一次 build 的時候應該會發現在 &lt;code&gt;mvn clean package&lt;/code&gt; 階段報錯，這時候往前找一下，應該會找到類似以下這段 log：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-plaintext" data-lang="plaintext"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 1.770 [INFO] --------------------------------[ jar ]---------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.025 [WARNING] The POM for org.apache.tomcat:tomcat-annotations-api:jar:9.0.65 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.083 [WARNING] The POM for net.minidev:json-smart:jar:2.4.8 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.131 [WARNING] The POM for net.bytebuddy:byte-buddy:jar:1.12.13 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.131 [WARNING] The POM for net.bytebuddy:byte-buddy-agent:jar:1.12.13 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.528 [WARNING] The POM for com.rabbitmq:amqp-client:jar:5.14.2 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.589 [WARNING] The POM for org.glassfish.jaxb:jaxb-runtime:jar:2.3.6 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.823 [WARNING] The POM for com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:jar:2.13.3 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.830 [WARNING] The POM for org.webjars:webjars-locator-core:jar:0.50 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;#16 2.840 [WARNING] The POM for org.apache.httpcomponents:httpcore:jar:4.4.15 is missing, no dependency information available
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;以上這段就是在說明 maven 在執行到 jar 這個 goal 的時候缺少哪些 dependencies&lt;/p&gt;
&lt;p&gt;這時候我們就會需要回到 &lt;code&gt;pom.xml&lt;/code&gt;，依照上面那段 log 缺少的 artifact，在 &lt;code&gt;&amp;lt;build&amp;gt;&lt;/code&gt; 標籤裡面加入以下這段：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;&amp;lt;pluginManagement&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;plugins&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;plugin&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.maven.plugins&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;maven-clean-plugin&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.tomcat&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;tomcat-annotations-api&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;9.0.65&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;net.minidev&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;json-smart&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.4.8&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;net.bytebuddy&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;byte-buddy&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.12.13&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;net.bytebuddy&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;byte-buddy-agent&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.12.13&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.rabbitmq&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;amqp-client&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;5.14.2&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.glassfish.jaxb&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jaxb-runtime&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.3.6&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;com.fasterxml.jackson.dataformat&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;jackson-dataformat-yaml&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;2.13.3&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.webjars&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;webjars-locator-core&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;0.50&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.apache.httpcomponents&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;httpcore&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;4.4.15&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/dependencies&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/plugin&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;/plugins&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;&amp;lt;/pluginManagement&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;這段是為了告訴 maven &lt;code&gt;maven-clean-plugin&lt;/code&gt; 需要指定的這些 dependencies&lt;/p&gt;
&lt;p&gt;請注意一定要掛在 &lt;code&gt;maven-clean-plugin&lt;/code&gt; 底下，因為這就是 clean lifecycle 對應的 plugin，這樣我們在第一階段執行 clean 的時候才會下載這些缺少的 artifact&lt;/p&gt;
&lt;h1 id="參考"&gt;參考&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.metricfire.com/blog/how-to-build-optimal-docker-images/"&gt;How to Build Optimal Docker Images&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.virtual7.de/pre-download-maven-dependencies-and-work-offline/"&gt;Pre-download Maven dependencies and work offline&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/1245076/maven-offline-problem-with-mvn-plugins/58767388#58767388"&gt;Maven offline - problem with mvn-plugins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://issues.apache.org/jira/browse/MDEP-82"&gt;MDEP-82 go-offline / resolve-plugins does not resolve all plugin dependencies&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://issues.apache.org/jira/browse/MNG-6965"&gt;MNG-6965 Extensions suddenly have org.codehaus.plexus:plexus-utils:jar:1.1 on their classpath&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/qaware/go-offline-maven-plugin"&gt;Go Offline Maven Plugin&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2022/12/maven_docker_multi_stage_build/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item><item><title>SpringFox to SpringDoc</title><link>https://blog.idontwannarock.dev/2022/12/springfox_to_springdoc/</link><pubDate>Mon, 12 Dec 2022 09:24:17 +0800</pubDate><guid>https://blog.idontwannarock.dev/2022/12/springfox_to_springdoc/</guid><description>Howard Tech Note https://blog.idontwannarock.dev/2022/12/springfox_to_springdoc/ -&lt;p&gt;以 Spring Boot 生態來說，比較主流的 documentation 方式應該就是 Swagger，以往應該多數人都是使用 SpringFox library，但近幾年也漸漸興起使用 SpringDoc，所以就做了一點筆記，紀錄要怎麼從 SpringFox 轉移到 SpringDoc&lt;/p&gt;
&lt;h1 id="比較"&gt;比較&lt;/h1&gt;
&lt;p&gt;這兩者同樣都有實作 OpenAPI 3.0，為什麼我們需要從 SpringFox 轉移使用 SpringDoc？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;springfox-boot-starter&lt;/code&gt; 包含許多 Spring 的 transitive dependencies；&lt;code&gt;springdoc-openapi-ui&lt;/code&gt; 則是 standalone 的 library，並沒有跟 Spring 的依賴&lt;/li&gt;
&lt;li&gt;&lt;code&gt;springfox-boot-starter&lt;/code&gt; 需要額外 dependency 才有 Swagger UI 的呈現；&lt;code&gt;springdoc-openapi-ui&lt;/code&gt; 則已經內含 Swagger UI&lt;/li&gt;
&lt;li&gt;SpringDoc 在一些細節功能上更強大，例如提供陣列參數更好的 UI 做輸入&lt;/li&gt;
&lt;li&gt;最重要的一點，SpringFox 幾乎已經不再更新；而 SpringDoc 還有在常態維護，因此也能跟較新版本的 Spring 有更好的整合，也能更即時的修正 bug&lt;/li&gt;
&lt;/ul&gt;
&lt;h1 id="升級步驟"&gt;升級步驟&lt;/h1&gt;
&lt;p&gt;相當簡單，參考 &lt;a href="https://springdoc.org/#migrating-from-springfox"&gt;官方文件&lt;/a&gt; 即可&lt;/p&gt;
&lt;p&gt;因為都是實作 OpenAPI，就算 SpringFox 有實作 2.0 版，但跟 3.0 其實差異也不大&lt;/p&gt;
&lt;p&gt;主要就是改一下 dependency，然後 annotation 調整一下，其他幾乎沒有不同&lt;/p&gt;
&lt;h1 id="注意事項"&gt;注意事項&lt;/h1&gt;
&lt;h2 id="spring-security"&gt;Spring Security&lt;/h2&gt;
&lt;p&gt;若有整合 Spring Security，則可以在 controller 透過加註 &lt;code&gt;@AuthenticationPrincipal&lt;/code&gt; 註解注入驗證過的 principal 物件，但這個物件不需要呈現在 Swagger 上&lt;/p&gt;
&lt;p&gt;以往 SpringFox 的作法是用 &lt;code&gt;@ApiIgnore&lt;/code&gt; 註解來隱藏；SpringDoc 也可以透過 &lt;code&gt;@Parameter(hidden = true)&lt;/code&gt; 來隱藏&lt;/p&gt;
&lt;p&gt;不過 SpringDoc 還有提供另外一種做法，就是加上以下 dependency，Swagger 上就會自動忽略 &lt;code&gt;@AuthenticationPrincipal&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-xml" data-lang="xml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;&amp;lt;dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;groupId&amp;gt;&lt;/span&gt;org.springdoc&lt;span style="color:#ff79c6"&gt;&amp;lt;/groupId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;artifactId&amp;gt;&lt;/span&gt;springdoc-openapi-security&lt;span style="color:#ff79c6"&gt;&amp;lt;/artifactId&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;&amp;lt;version&amp;gt;&lt;/span&gt;1.6.13&lt;span style="color:#ff79c6"&gt;&amp;lt;/version&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff79c6"&gt;&amp;lt;/dependency&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id="反向代理"&gt;反向代理&lt;/h2&gt;
&lt;p&gt;如果有使用反向代理，&lt;a href="https://springdoc.org/#how-can-i-deploy-springdoc-openapi-ui-behind-a-reverse-proxy"&gt;官方文件&lt;/a&gt; 中有建議可以設定 &lt;code&gt;server.forward-headers-strategy&lt;/code&gt; 為 &lt;code&gt;framework&lt;/code&gt; 或 &lt;code&gt;native&lt;/code&gt; 的方式；但要注意的是，SpringDoc 是從以下 headers 當中取得 url，並只在第一次產生 ui 的時候取得該值：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;RFC7239 規範的 &lt;a href="https://www.rfc-editor.org/rfc/rfc7239"&gt;Forwarded Headers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Forwarded-Host&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Forwarded-Port&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Forwarded-Proto&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Forwarded-Ssl&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;X-Forwarded-Prefix&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;但萬一反向代理做 upstream 的時候沒有帶上這些 headers，或我們可能需要從 Referer header 取得真實來源的 domain，好讓 Swagger UI 做 HTTP 呼叫的時候可以使用&lt;/p&gt;
&lt;p&gt;以往在 SpringFox 我們需要從 request 當中取得 &lt;code&gt;Referer&lt;/code&gt; header 並解析；但 SpringDoc 可以有更簡單的作法，也就是直接設定 &lt;code&gt;OpenAPI&lt;/code&gt; 的 servers 為 &lt;code&gt;/&lt;/code&gt; 或是 Spring Boot 的 &lt;code&gt;server.servlet.context-path&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;以下為 annotation 方式設定範例&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Configuration
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@OpenAPIDefinition(info &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; @Info(
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; title &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;Product API&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; version &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; servers &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; @Server(url &lt;span style="color:#ff79c6"&gt;=&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;${server.servlet.context-path}/&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;SwaggerConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;或以 programmatic 方式設定：&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-java" data-lang="java"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;@Configuration
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#8be9fd;font-style:italic"&gt;public&lt;/span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;class&lt;/span&gt; &lt;span style="color:#50fa7b"&gt;SwaggerConfig&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Value(&lt;span style="color:#f1fa8c"&gt;&amp;#34;${server.servlet.context-path}&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#8be9fd;font-style:italic"&gt;private&lt;/span&gt; String serversUrl;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; @Bean
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; OpenAPI &lt;span style="color:#50fa7b"&gt;openAPI&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff79c6"&gt;return&lt;/span&gt; &lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; OpenAPI()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#50fa7b"&gt;info&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; Info()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#50fa7b"&gt;title&lt;/span&gt;(&lt;span style="color:#f1fa8c"&gt;&amp;#34;Product API&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#50fa7b"&gt;version&lt;/span&gt;(&lt;span style="color:#f1fa8c"&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#50fa7b"&gt;servers&lt;/span&gt;(List.&lt;span style="color:#50fa7b"&gt;of&lt;/span&gt;(&lt;span style="color:#ff79c6"&gt;new&lt;/span&gt; Server()
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; .&lt;span style="color:#50fa7b"&gt;url&lt;/span&gt;(serversUrl &lt;span style="color:#ff79c6"&gt;+&lt;/span&gt; &lt;span style="color:#f1fa8c"&gt;&amp;#34;/&amp;#34;&lt;/span&gt;)));
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h1 id="參考"&gt;參考&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://springdoc.org/"&gt;官方文件&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/a/72480207/7605040"&gt;Are there any advantages of using/migrating to springdoc-openapi from Springfox?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/a/65672670/7605040"&gt;Spring Boot REST API: SpringDoc + OpenAPI 3 (springdoc-openapi-ui) or Swagger2 v3 (springfox-boot-starter)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/springfox/springfox"&gt;SpringFox GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
- https://blog.idontwannarock.dev/2022/12/springfox_to_springdoc/ - This website by Howard Wang is licensed under a Creative Common Attribution-ShareAlike 4.0 International License.</description></item></channel></rss>