

<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>mssql &#8211; Max的程式語言筆記</title>
	<atom:link href="https://stackoverflow.max-everyday.com/tag/mssql/feed/" rel="self" type="application/rss+xml" />
	<link>https://stackoverflow.max-everyday.com</link>
	<description>我要當一個豬頭，快樂過每一天</description>
	<lastBuildDate>Mon, 25 May 2026 06:05:27 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0</generator>

<image>
	<url>https://stackoverflow.max-everyday.com/wp-content/uploads/2017/02/max-stackoverflow-256.png</url>
	<title>mssql &#8211; Max的程式語言筆記</title>
	<link>https://stackoverflow.max-everyday.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>不用密碼連 Azure SQL：用 Token 驗證搭配 fast_executemany 解決效能瓶頸</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-sql-token-fast_executemany/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-sql-token-fast_executemany/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 25 May 2026 06:05:26 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8468</guid>

					<description><![CDATA[前言 在將本地資料同步到 Azure SQL D...]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">前言</h2>



<p class="wp-block-paragraph">在將本地資料同步到 Azure SQL Database 的專案中，我遇到了兩個問題：</p>



<ol class="wp-block-list">
<li><strong>安全性</strong>：連線字串裡不想寫死帳號密碼</li>



<li><strong>效能</strong>：用 <code>pyodbc</code> 逐筆 UPDATE 速度慢到不行</li>
</ol>



<p class="wp-block-paragraph">這篇文章紀錄我怎麼用 <strong>Azure Managed Identity + Access Token</strong> 取代密碼驗證，並透過 <code>fast_executemany</code> 大幅提升批次寫入效能。同時也整理了幾個常見疑問：有沒有 Token 差在哪、<code>ActiveDirectoryMsi</code> 與手動 Token 有何不同、兩者效能是否有差。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">環境</h2>



<ul class="wp-block-list">
<li>Python 3.11+</li>



<li><code>pyodbc</code> + ODBC Driver 18 for SQL Server</li>



<li><code>azure-identity</code></li>



<li>Azure SQL Database（已啟用 Entra ID / Managed Identity）</li>
</ul>



<pre class="wp-block-code"><code>pip install pyodbc azure-identity</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">問題一：不想在程式裡放密碼</h2>



<h3 class="wp-block-heading">有無 Access Token 的差別</h3>



<p class="wp-block-paragraph">傳統帳密驗證的連線字串長這樣：</p>



<pre class="wp-block-code"><code># 傳統帳密驗證
conn_str = (
    "Driver={ODBC Driver 18 for SQL Server};"
    "Server=your-server.database.windows.net;"
    "Database=your-db;"
    "UID=myuser;"
    "PWD=mypassword;"
)
conn = pyodbc.connect(conn_str)</code></pre>



<p class="wp-block-paragraph">Access Token 驗證則是拿掉帳密，改用 <code>attrs_before</code> 注入 Token：</p>



<pre class="wp-block-code"><code># Access Token 驗證
conn_str = (
    "Driver={ODBC Driver 18 for SQL Server};"
    "Server=your-server.database.windows.net;"
    "Database=your-db;"
    "Encrypt=yes;"
    # 沒有 UID / PWD
)
conn = pyodbc.connect(conn_str, attrs_before={1256: token_struct})</code></pre>



<p class="wp-block-paragraph">兩者的核心差異：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th>帳密</th><th>Access Token</th></tr></thead><tbody><tr><td>驗證來源</td><td>SQL Server 本地帳號</td><td>Azure Entra ID (AAD)</td></tr><tr><td>密碼管理</td><td>要自己管、要輪換</td><td>由 Azure 管，短效 Token 自動刷新</td></tr><tr><td>程式碼安全</td><td>密碼容易外洩</td><td>無密碼，較安全</td></tr><tr><td>CI/CD / Managed Identity</td><td>需要把密碼塞進環境變數</td><td>直接用 Managed Identity，不需任何密碼</td></tr><tr><td>本機開發</td><td>帳密寫死或讀 <code>.env</code></td><td><code>az login</code> 後自動生效</td></tr></tbody></table></figure>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>一句話總結</strong>：帳密是「你知道什麼」，Token 是「你是誰（身份）」，在雲端環境推薦用 Token，省去密碼管理的麻煩。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">解法：用 Azure Identity 取得 Access Token</h3>



<p class="wp-block-paragraph"><code>azure-identity</code> 套件提供的 <code>DefaultAzureCredential</code> 可以依序嘗試多種驗證方式（環境變數、Managed Identity、Azure CLI 登入等），在本機開發和正式環境都能無縫切換。</p>



<pre class="wp-block-code"><code>import struct
import pyodbc
from azure.identity import DefaultAzureCredential

# 在程式啟動時初始化一次，讓 Token 可以被快取重複使用
azure_credential = DefaultAzureCredential()

def get_mssql_connection(server, database, driver):
    # 每次建立連線時呼叫 get_token()，有快取就回快取，快到期會自動刷新
    token_object = azure_credential.get_token("https://database.windows.net/.default")

    # pyodbc 要求 Token 必須以特定格式打包
    token_bytes = token_object.token.encode("utf-16-le")
    token_struct = struct.pack(f"&lt;I{len(token_bytes)}s", len(token_bytes), token_bytes)

    # 連線字串不帶 UID/PWD，改用 attrs_before 注入 Token
    # SQL_COPT_SS_ACCESS_TOKEN = 1256
    conn_str = f"Driver={driver};Server={server};Database={database};Encrypt=yes;TrustServerCertificate=no;"
    conn = pyodbc.connect(conn_str, attrs_before={1256: token_struct})
    return conn</code></pre>



<p class="wp-block-paragraph"><strong>關鍵細節：</strong></p>



<ul class="wp-block-list">
<li>Token 必須先用 <code>utf-16-le</code> 編碼，再用 <code>struct.pack</code> 包成 <code>&lt;I{n}s</code> 格式</li>



<li><code>attrs_before={1256: token_struct}</code> 是 pyodbc 注入 <code>SQL_COPT_SS_ACCESS_TOKEN</code> 的方式</li>



<li>連線字串裡<strong>不能</strong>加 <code>Authentication=...</code>，否則會與 Token 驗證衝突</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">補充：<code>Authentication=ActiveDirectoryMsi</code> vs 手動 Access Token</h2>



<p class="wp-block-paragraph">Azure SQL 還有另一種無密碼寫法：在連線字串直接加 <code>Authentication=ActiveDirectoryMsi</code>，讓 ODBC Driver 自己去拿 Token：</p>



<pre class="wp-block-code"><code># ActiveDirectoryMsi 寫法：Driver 自己處理 Token
conn_str = (
    "Driver={ODBC Driver 18 for SQL Server};"
    "Server=your-server.database.windows.net;"
    "Database=your-db;"
    "Authentication=ActiveDirectoryMsi;"
    "Encrypt=yes;"
)
conn = pyodbc.connect(conn_str)</code></pre>



<p class="wp-block-paragraph">兩種方式的比較：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th><code>ActiveDirectoryMsi</code></th><th>手動 Access Token</th></tr></thead><tbody><tr><td>程式碼複雜度</td><td>簡單，Driver 全包</td><td>需要自己打包 <code>struct</code></td></tr><tr><td>套件依賴</td><td>只需 <code>pyodbc</code></td><td>需要 <code>azure-identity</code></td></tr><tr><td>Token 快取</td><td>Driver 自己管，不透明</td><td>自己控制（全域 credential 物件）</td></tr><tr><td>彈性</td><td>僅限 MSI</td><td>可用任何 Credential（CLI、SP、環境變數…）</td></tr><tr><td>本機開發</td><td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 只能在 Azure 環境跑</td><td><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <code>az login</code> 本機也能用</td></tr><tr><td>User-Assigned MSI</td><td>需加 <code>UID=client-id</code></td><td><code>ManagedIdentityCredential(client_id=...)</code></td></tr></tbody></table></figure>



<h3 class="wp-block-heading">效能有差嗎？</h3>



<p class="wp-block-paragraph"><strong>連線建立後的 SQL 執行效能：完全一樣。</strong> 差別只在取 Token 的開銷，且僅影響建立連線那一刻：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th></th><th><code>ActiveDirectoryMsi</code></th><th>手動 <code>DefaultAzureCredential</code></th></tr></thead><tbody><tr><td>誰去打 IMDS</td><td>ODBC Driver（每次 <code>connect()</code> 都可能觸發）</td><td><code>azure-identity</code>（有記憶體快取）</td></tr><tr><td>Token 快取</td><td>Driver 內部管理，<strong>不透明</strong>，無法控制</td><td>全域 credential 物件自動快取，<strong>明確可控</strong></td></tr><tr><td>快取失效前重連</td><td>可能再打一次 IMDS</td><td>直接回傳快取 Token，<strong>不打網路</strong></td></tr></tbody></table></figure>



<h3 class="wp-block-heading">該選哪個？</h3>



<p class="wp-block-paragraph">實務上幾乎不用思考，<strong>直接選手動管理</strong>就對了——只要記住一個原則：</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>每次 <code>connect()</code> 都重新呼叫 <code>get_token()</code>，不要把打包好的 <code>token_struct</code> 存起來重複用。</strong></p>
</blockquote>



<pre class="wp-block-code"><code># &#x274c; 危險：token_struct 是靜態 bytes，1 小時後 Token 過期，重連時會爆
token_struct = pack_token(azure_credential.get_token(...))
# ... 之後某處重新 connect() 但沿用舊的 token_struct

# &#x2705; 正確：每次建立連線時才呼叫 get_token()
def get_conn():
    token = azure_credential.get_token(...)  # 有快取就回快取，快到期自動刷新
    return pyodbc.connect(conn_str, attrs_before={1256: pack_token(token)})</code></pre>



<p class="wp-block-paragraph"><code>azure-identity</code> 的 <code>get_token()</code> 本身有快取，不用擔心每次都打網路，讓它去判斷就好。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">問題二：逐筆 UPDATE 太慢</h2>



<p class="wp-block-paragraph">初版程式是這樣跑的：</p>



<pre class="wp-block-code"><code>for row in rows:
    cursor.execute("UPDATE users SET name = ?, email = ? WHERE account = ?", row)
conn.commit()</code></pre>



<p class="wp-block-paragraph">幾百筆資料就要花好幾秒，每一筆都是一次來回的 network round-trip。</p>



<h3 class="wp-block-heading">解法：<code>executemany</code> + <code>fast_executemany = True</code></h3>



<pre class="wp-block-code"><code>cursor = conn.cursor()

# 啟用 fast_executemany：改用 ODBC 的批次參數傳輸，大幅減少 round-trip
cursor.fast_executemany = True

sql = "UPDATE users SET name = ?, email = ? WHERE account = ?"
cursor.executemany(sql, rows)  # rows 是 list of tuples

conn.commit()</code></pre>



<p class="wp-block-paragraph"><code>fast_executemany</code> 是 pyodbc 的一個優化開關，啟用後會將所有參數打包成一個批次送出，而不是一筆一筆傳送，對幾百到幾千筆的批次操作效果非常明顯。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">完整範例</h2>



<pre class="wp-block-code"><code>import sqlite3
import pyodbc
import os
import struct
from azure.identity import DefaultAzureCredential

SERVER   = "your-server.database.windows.net"
DATABASE = "your-database"
DRIVER   = "{ODBC Driver 18 for SQL Server}"

# 全域初始化一次，Token 快取由 azure-identity 管理
azure_credential = DefaultAzureCredential()


def get_mssql_connection():
    # 每次連線都呼叫 get_token()，讓 azure-identity 決定要不要刷新
    token_object = azure_credential.get_token("https://database.windows.net/.default")
    token_bytes  = token_object.token.encode("utf-16-le")
    token_struct = struct.pack(f"&lt;I{len(token_bytes)}s", len(token_bytes), token_bytes)
    conn_str = (
        f"Driver={DRIVER};Server={SERVER};Database={DATABASE};"
        "Encrypt=yes;TrustServerCertificate=no;"
    )
    return pyodbc.connect(conn_str, attrs_before={1256: token_struct})


def sync_sqlite_to_mssql(sqlite_path: str):
    if not os.path.exists(sqlite_path):
        print(f"SQLite not found: {sqlite_path}")
        return

    with sqlite3.connect(sqlite_path) as sqlite_conn:
        rows = sqlite_conn.execute(
            "SELECT name, email, account FROM employee WHERE status = 'active'"
        ).fetchall()

    if not rows:
        print("No rows to sync.")
        return

    print(f"Syncing {len(rows)} rows...")

    with get_mssql_connection() as mssql_conn:
        cursor = mssql_conn.cursor()
        cursor.fast_executemany = True
        cursor.executemany(
            "UPDATE users SET name = ?, email = ? WHERE account = ?",
            rows,
        )
        mssql_conn.commit()

    print("Done.")


if __name__ == "__main__":
    sync_sqlite_to_mssql("portal.db")</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">前置設定：讓 Managed Identity 能存取 Azure SQL</h2>



<p class="wp-block-paragraph">在 Azure SQL 裡執行以下 T-SQL，把你的 Managed Identity 或 Entra ID 帳號加入：</p>



<pre class="wp-block-code"><code>-- 使用 Azure AD 帳號登入後執行
CREATE USER &#91;your-managed-identity-name] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER &#91;your-managed-identity-name];
ALTER ROLE db_datawriter ADD MEMBER &#91;your-managed-identity-name];</code></pre>



<p class="wp-block-paragraph">本機開發時，用 Azure CLI 登入後 <code>DefaultAzureCredential</code> 就會自動使用你的帳號：</p>



<pre class="wp-block-code"><code>az login</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">小結</h2>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>問題</th><th>解法</th></tr></thead><tbody><tr><td>連線字串含密碼</td><td><code>DefaultAzureCredential</code> + Access Token (<code>SQL_COPT_SS_ACCESS_TOKEN</code>)</td></tr><tr><td>逐筆 UPDATE 慢</td><td><code>cursor.fast_executemany = True</code> + <code>executemany()</code></td></tr><tr><td>本機 / 正式環境切換麻煩</td><td><code>DefaultAzureCredential</code> 自動偵測驗證來源</td></tr><tr><td><code>ActiveDirectoryMsi</code> vs 手動 Token</td><td>手動管理更靈活，本機開發也能用，直接選它</td></tr><tr><td>Token 過期問題</td><td>每次 <code>connect()</code> 都呼叫 <code>get_token()</code>，不要快取 <code>token_struct</code></td></tr></tbody></table></figure>



<p class="wp-block-paragraph">兩個改動加起來不超過 10 行，卻同時解決了安全性與效能問題，值得加進你的 Azure SQL 工具箱。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-sql-token-fast_executemany/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure MSSQL 無密碼 Python 連線</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-mssql-no-password-connect/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-mssql-no-password-connect/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Thu, 14 May 2026 12:04:32 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8412</guid>

					<description><![CDATA[在 Azure 上使用 系統指派的受控識別 (S...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">在 Azure 上使用 <strong>系統指派的受控識別 (System-Assigned Managed Identity)</strong> 是最推薦的做法，因為它完全不需要在程式碼裡寫死密碼，安全性最高，且生命週期與你的資源（如 VM 或 App Service）同步。</p>



<p class="wp-block-paragraph">要實現這個功能，你需要完成三個步驟：<strong>啟用身份識別</strong>、<strong>資料庫授權</strong>、以及<strong>修改 Python 連線字串</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第一步：在 Azure 資源上啟用受控識別</h3>



<p class="wp-block-paragraph">在你運行 Python 程式碼的資源（例如 Azure VM, App Service 或 Function）的頁面：</p>



<ol start="1" class="wp-block-list">
<li>點選左側選單的 <strong>「身分識別」(Identity)</strong>。</li>



<li>在 <strong>「系統指派」</strong> 頁籤下，將狀態改為 <strong>「開啟」</strong> 並儲存。</li>



<li>系統會產生一個 <strong>物件識別碼 (Object ID)</strong>，請記住這個資源的名稱。</li>
</ol>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="852" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-1024x852.jpg?v=1778758921" alt="" class="wp-image-8414" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-1024x852.jpg?v=1778758921 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-600x499.jpg?v=1778758921 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p-768x639.jpg?v=1778758921 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-19-41-7p.jpg?v=1778758921 1477w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">上面的截圖來看，你目前是在 <strong>「Azure SQL Database」</strong> 的介面裡。</p>



<p class="wp-block-paragraph"><strong>受控識別（Managed Identity）不是在資料庫（Database）上開啟的，而是在「跑 Python 程式碼的那個東西」上面開啟的。</strong></p>



<p class="wp-block-paragraph">請想像：</p>



<ul class="wp-block-list">
<li><strong>SQL Database</strong> 是「銀行」。</li>



<li><strong>你的 Python 程式碼</strong>（跑在 VM、App Service 或 Function 上）是「進去辦事的人」。</li>
</ul>



<p class="wp-block-paragraph">你現在需要在「辦事的人」身上發一張身分證（啟用受控識別），然後再去「銀行」告訴櫃檯（資料庫）說：「看到拿這張身分證的人就讓他進來」。</p>



<h3 class="wp-block-heading">接下來你該怎麼做：</h3>



<h4 class="wp-block-heading">1. 找到「發身分證」的地方</h4>



<p class="wp-block-paragraph">請在 Azure Portal 最上方的搜尋框，搜尋你<strong>跑 Python 程式碼的資源名稱</strong>。</p>



<ul class="wp-block-list">
<li>如果你是在 <strong>Azure VM</strong> 跑：去該 VM 的頁面，找左側選單的「身分識別」。</li>



<li>如果你是在 <strong>App Service</strong> 跑：去該 App Service 的頁面，找左側選單的「身分識別」。</li>



<li>如果你是在 <strong>Azure Function</strong> 跑：去該 Function 的頁面，找左側選單的「身分識別」。</li>
</ul>



<p class="wp-block-paragraph">進入後，你就會看到我上一個回覆說的「系統指派」狀態，將它改為 <strong>「開啟」</strong>。</p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="351" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-1024x351.jpg?v=1778760219" alt="" class="wp-image-8415" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-1024x351.jpg?v=1778760219 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-600x205.jpg?v=1778760219 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-768x263.jpg?v=1778760219 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-1536x526.jpg?v=1778760219 1536w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-14-20-03-7t-2048x701.jpg?v=1778760219 2048w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h4 class="wp-block-heading">2. 在資料庫（也就是你截圖的這個地方）授權</h4>



<p class="wp-block-paragraph">當你在上面的資源開啟受控識別後，回到你截圖的這個 SQL Database 介面：</p>



<ol start="1" class="wp-block-list">
<li>點擊左側選單的 <strong>「查詢編輯器 (預覽)」(Query editor)</strong>。</li>



<li>用你的管理員帳號登入。</li>



<li>執行以下指令（假設你的 VM 或 App Service 名字叫 <code>my-python-app</code>）：</li>
</ol>



<p class="wp-block-paragraph">SQL</p>



<pre class="wp-block-code"><code>CREATE USER &#91;my-python-app] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER &#91;my-python-app];
ALTER ROLE db_datawriter ADD MEMBER &#91;my-python-app];
</code></pre>



<h4 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 補充：截圖中的發現</h4>



<p class="wp-block-paragraph">從你的截圖中可以看到，在 <strong>「安全性」</strong> 區塊下，有一行：</p>



<ul class="wp-block-list">
<li><strong>系統指派的身分識別：已啟用</strong></li>
</ul>



<p class="wp-block-paragraph">這指的是 <strong>「SQL Server 自己」</strong> 的身分，通常是用來讓 SQL 去讀取 Key Vault 等其他資源用的。<strong>這不是給你的 Python 程式連線用的。</strong> <strong>總結：</strong> 請去尋找你運行 Python 的那個 Azure 資源（VM / App Service），在那裡開啟身分識別。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">了解更多: 利用客戶自控金鑰支援透明資料加密的受控識別<br><a href="https://learn.microsoft.com/zh-tw/azure/azure-sql/database/transparent-data-encryption-byok-identity?view=azuresql&amp;WT.mc_id=Portal-SqlAzureExtension">https://learn.microsoft.com/zh-tw/azure/azure-sql/database/transparent-data-encryption-byok-identity?view=azuresql&amp;WT.mc_id=Portal-SqlAzureExtension</a></p>



<p class="wp-block-paragraph">▋ 數據安全的大門與那把遺失的鑰匙</p>



<p class="wp-block-paragraph">在數位世界裡，我們使用「透明資料加密」技術，這就像是在資料庫門口裝了一道自動感應門，資料進出時會自動上鎖或解鎖。傳統做法是讓雲端平台幫你管理這把鑰匙，這對大多數人來說很方便。然而，對於需要極高掌控權的管理者來說，這就像是把身家財產的進出權限完全交給外人，一旦發生資安爭議，你可能無法第一時間主動切換鎖頭。</p>



<p class="wp-block-paragraph">▋ 拿回屬於你的主權鑰匙</p>



<p class="wp-block-paragraph">為了解決這個信任缺口，現在有一種「客戶管理金鑰」的機制。簡單來說，這就是讓你自備鎖頭與鑰匙。當你的資料庫要啟動加密功能時，系統不再使用預設的鑰匙，而是去你的私密保險庫中讀取你指定的「身份證明」。這確保了只有經過你授權的特定身份，才能啟動那把開啟資料的鑰匙。</p>



<p class="wp-block-paragraph">▋ 建立你的專屬數位防線</p>



<p class="wp-block-paragraph">如果你希望全面掌控數據的安全命運，可以嘗試以下步驟：</p>



<ul class="wp-block-list">
<li>在雲端環境中建立一個專屬的「身份識別」，這就像是為你的資料庫申請一張專屬的身分證。</li>



<li>設定嚴格的存取權限，確保只有這個「身份」能接觸到你的加密鑰匙。</li>



<li>定期檢查這把鑰匙的使用紀錄，確保沒有任何未經授權的嘗試。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第二步：在 SQL Database 中授權</h3>



<p class="wp-block-paragraph">SQL Server 預設不認識這個受控識別，你必須進到資料庫裡幫它「開門」：</p>



<ol start="1" class="wp-block-list">
<li>使用 <strong>Active Directory 管理員</strong> 帳號登入 SQL Database（可用 Azure Portal 的 Query Editor）。</li>



<li>執行以下 SQL 指令（將 <code>&lt;identity-name&gt;</code> 替換為你的 Azure 資源名稱）：</li>
</ol>



<p class="wp-block-paragraph">SQL</p>



<pre class="wp-block-code"><code>-- 建立資料庫使用者
CREATE USER &#91;&lt;identity-name&gt;] FROM EXTERNAL PROVIDER;

-- 授予讀取/寫入權限 (視需求調整角色)
ALTER ROLE db_datareader ADD MEMBER &#91;&lt;identity-name&gt;];
ALTER ROLE db_datawriter ADD MEMBER &#91;&lt;identity-name&gt;];
</code></pre>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">如果你是在 <strong>Azure App Service</strong> 或 <strong>Azure VM</strong> 上啟用的受控識別，該「顯示名稱」通常就是你的 <strong>資源名稱</strong>。</p>



<ul class="wp-block-list">
<li><strong>錯誤做法</strong>：<code>CREATE USER [1234-1234-1234-1234-1234] ...</code> (不可使用 ID)</li>



<li><strong>正確做法</strong>：<code>CREATE USER [你的AppService名稱] FROM EXTERNAL PROVIDER;</code></li>
</ul>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph">如果你不確定名稱，或者名稱有特殊字元，你可以直接使用 <code>SID</code> (這對應到你看到的物件識別碼) 來建立，這樣最準確：</p>



<p class="wp-block-paragraph"></p>



<pre class="wp-block-code"><code>-- 請將 '1234-1234-1234-1234-1234' 替換為你在身分識別頁面看到的 Object ID
CREATE USER &#91;MyManagedIdentityName] 
WITH OBJECT_ID = '1234-1234-1234-1234-1234';

-- 接著再授權
ALTER ROLE db_datareader ADD MEMBER &#91;MyManagedIdentityName];
</code></pre>



<figure class="wp-block-image size-full"><img decoding="async" width="707" height="285" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-54-7w.jpg?v=1778820896" alt="" class="wp-image-8425" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-54-7w.jpg?v=1778820896 707w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-54-7w-600x242.jpg?v=1778820896 600w" sizes="(max-width: 707px) 100vw, 707px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第三步：修改 Python 連線字串</h3>



<p class="wp-block-paragraph">使用受控識別時，連線字串不需要 <code>UID</code> 和 <code>PWD</code>，而是改用 <code>Authentication=ActiveDirectoryMsi</code>。</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>import pyodbc

# 設定基礎參數
server = 'your-server.database.windows.net'
database = 'your-database'
driver = '{ODBC Driver 18 for SQL Server}'

# 系統指派受控識別的連線字串
connection_string = (
    f"Driver={driver};"
    f"Server={server};"
    f"Database={database};"
    "Authentication=ActiveDirectoryMsi;"  # 關鍵設定
    "Encrypt=yes;"
    "TrustServerCertificate=no;"
    "Connection Timeout=30;"
)

def connect_and_query():
    try:
        print(f"Connecting to {server} using Managed Identity...")
        conn = pyodbc.connect(connection_string)
        print("連線成功！")
        return conn
    except Exception as e:
        print(f"連線失敗: {e}")

# 執行連線
conn = connect_and_query()
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 關鍵筆記：</h3>



<ul class="wp-block-list">
<li><strong>環境限制</strong>：<code>Authentication=ActiveDirectoryMsi</code> 只能在 <strong>Azure 環境內</strong> 運行（例如在 Azure VM 上跑）。如果你在自己的電腦（在地端）跑這段程式碼會報錯，因為地端環境沒有 Managed Identity 憑證。</li>



<li><strong>驅動程式</strong>：請確保你的環境中安裝了 <strong>ODBC Driver 17 或 18</strong>，舊版驅動程式可能不支援 <code>ActiveDirectoryMsi</code> 參數。</li>



<li><strong>優點</strong>：即使你更換了密碼，或者這台機器被刪除了，都不需要去改程式碼，這就是「無密碼連線」的強大之處。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">執行以下 SQL 指令時遇到 Error:</p>



<p class="wp-block-paragraph">A connection was successfully established with the server, but then an error occurred during the pre-login handshake. (provider: TCP Provider, error: 0 &#8211; An existing connection was forcibly closed by the remote host.)<br><br>Failed to execute the query</p>



<p class="wp-block-paragraph">這個錯誤「An existing connection was forcibly closed by the remote host」發生在 <strong>Pre-login handshake (預先登錄握手)</strong> 階段，通常不是程式碼邏輯問題，而是 <strong>網路層</strong> 或 <strong>防火牆</strong> 的攔截。</p>



<p class="wp-block-paragraph">這代表你的 Python 程式已經碰到了 SQL Server 的大門，但 SQL Server 因為某些安全規則直接把連線「踢掉」了。</p>



<p class="wp-block-paragraph">請檢查以下三個最常見的原因：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1. Azure SQL 防火牆未允許你的 IP</h3>



<p class="wp-block-paragraph">這是最常見的原因。即便你使用受控識別，網路還是要通。</p>



<ul class="wp-block-list">
<li><strong>如果你在 Azure 資源內執行</strong>：請到 SQL Server 的「網路 (Networking)」設定，確保 <strong>「允許 Azure 服務和資源存取此伺服器 (Allow Azure services and resources to access this server)」</strong> 有被勾選。</li>



<li><strong>如果你在公司內網執行</strong>：請確認你的公用 IP 已加入 SQL Server 的防火牆白名單。</li>
</ul>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="378" height="1024" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v-378x1024.jpg?v=1778820822" alt="" class="wp-image-8423" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v-378x1024.jpg?v=1778820822 378w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v-221x600.jpg?v=1778820822 221w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-15-12-53-7v.jpg?v=1778820822 434w" sizes="auto, (max-width: 378px) 100vw, 378px" /></figure>
</div>


<h3 class="wp-block-heading">2. 強制使用 TLS 1.2</h3>



<p class="wp-block-paragraph">Azure SQL 預設要求 TLS 1.2。如果你的 Python 環境或 ODBC 驅動程式版本太舊，握手時會因為加密協定不匹配而被中斷。</p>



<ul class="wp-block-list">
<li><strong>解決方法</strong>：確保你的連線字串中有 <code>Encrypt=yes;</code>（你已經有了）。</li>



<li><strong>檢查驅動</strong>：確認你安裝的是 <strong>ODBC Driver 18 for SQL Server</strong>。注意：Driver 18 預設會強制驗證證書，如果你沒有設定憑證，請試著在連線字串加上 <code>TrustServerCertificate=yes;</code> 測試看看。</li>
</ul>



<h3 class="wp-block-heading">3. ODBC Driver 18 的「預設加密」改變</h3>



<p class="wp-block-paragraph">如果你是從 Driver 17 升級到 18，Driver 18 的預設行為變更了。請嘗試將連線字串稍微修改如下：</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>connection_string = (
    f"DRIVER={driver};"
    f"SERVER={server};"
    f"DATABASE={database};"
    "Authentication=ActiveDirectoryMsi;"
    "Encrypt=yes;"
    "TrustServerCertificate=yes;"  # 先暫時改成 yes 測試是否為證書握手失敗
    "Connection Timeout=30;"
)
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">關鍵排除步驟（Debug Check）</h3>



<ol start="1" class="wp-block-list">
<li><strong>測試工具連線</strong>：你在 Azure Portal 上的 <strong>「查詢編輯器 (Query Editor)」</strong> 現在能正常進去嗎？
<ul class="wp-block-list">
<li>如果連 Query Editor 都進不去，那是資料庫本身的網路/防火牆設定問題。</li>
</ul>
</li>



<li><strong>確認受控識別是否已成功建立 User</strong>：
<ul class="wp-block-list">
<li>你剛才在嘗試 <code>CREATE USER</code> 時失敗了，這代表受控識別的身分還沒進到資料庫。</li>



<li><strong>重要</strong>：如果 <code>CREATE USER</code> 沒成功，<code>Authentication=ActiveDirectoryMsi</code> 就絕對連不上。</li>
</ul>
</li>
</ol>



<p class="wp-block-paragraph"><strong>目前的卡點建議：</strong></p>



<p class="wp-block-paragraph">先解決 <code>CREATE USER</code> 的問題。請確認你是用 <strong>「Microsoft Entra 管理員」</strong> 的帳號登入 Query Editor。只有管理員有權限執行 <code>FROM EXTERNAL PROVIDER</code> 的指令。</p>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-mssql-no-password-connect/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure SQL 啟用 Entra ID 驗證</title>
		<link>https://stackoverflow.max-everyday.com/2026/05/azure-sql-entra-id-pwd/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/05/azure-sql-entra-id-pwd/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 05 May 2026 05:04:46 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8380</guid>

					<description><![CDATA[如果你有在玩雲端資料庫，一定知道傳統的帳號密碼超...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">如果你有在玩雲端資料庫，一定知道傳統的帳號密碼超麻煩，不只難記，萬一被外流就慘了。</p>



<p class="wp-block-paragraph">微軟現在有個超讚的設定，可以把 Azure SQL 資料庫變成 僅限 Microsoft Entra ID 驗證。這聽起來很專業，其實就是把傳統的帳號密碼大門關起來，規定大家只能用正式的微軟雲端帳號登入。</p>



<p class="wp-block-paragraph">停用本地驗證方法並僅允許 Azure Active Directory 驗證，可確保只有 Azure Active Directory 識別身分才能存取 Azure SQL Database，以提升安全性。</p>



<p class="wp-block-paragraph">這樣做有什麼好處呢？</p>



<ul class="wp-block-list">
<li>第一個好處是統一管理。所有的權限都歸雲端帳號管，不用在資料庫裡建一堆帳號。</li>



<li>再來是安全性大升級！因為雲端帳號可以開啟手機簡訊或 APP 的多重驗證，駭客就算偷到密碼也進不來！</li>
</ul>



<p class="wp-block-paragraph">操作起來其實很簡單，不論你是習慣打指令還是用滑鼠點網頁都能搞定。</p>



<p class="wp-block-paragraph">如果你是用圖形網頁操作，只要在建立資料庫的時候，去 身份驗證 分頁點一下 僅限 Microsoft Entra 驗證。</p>



<p class="wp-block-paragraph">接著搜尋你自己的帳號，把它設成 管理員 就大功告成。</p>



<p class="wp-block-paragraph">設定完記得測試看看，這時候你會發現原本的 SQL 舊帳號已經被 封鎖 沒辦法用了，一定要改用微軟帳號才能進去。</p>



<p class="wp-block-paragraph">我覺得這功能真的超方便！以前要記一堆資料庫密碼真的很痛苦，現在只要保護好一個主帳號就好。</p>



<p class="wp-block-paragraph">把複雜的事情變簡單，而且還更安全，這絕對是現代開發者必學的懶人自保術呀！</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">既然程式是跑在 Docker 裡，沒辦法手動點選視窗，那我們就要幫程式申請一張「專屬通行證」，也就是 Client Secret <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f511.png" alt="🔑" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>



<p class="wp-block-paragraph">這張通行證要在 Azure 入口網站（Azure Portal）裡面產生，步驟其實很簡單，就像是在幫你的程式辦身分證一樣：</p>



<p class="wp-block-paragraph"><strong>第一步：註冊應用程式</strong></p>



<p class="wp-block-paragraph">首先去 Azure Portal 搜尋「應用程式註冊」（App Registrations）:<br><a href="https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade">https://portal.azure.com/#view/Microsoft_AAD_RegisteredApps/ApplicationsListBlade</a></p>



<p class="wp-block-paragraph">點進去後選「新註冊」。幫你的程式取個名字，然後按註冊就好。這時候你會看到一個「應用程式 (用戶端) 識別碼」，這就是連線字串裡的 Client ID <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f194.png" alt="🆔" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="678" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-00-7b-1024x678.jpg?v=1777957275" alt="" class="wp-image-8381" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-00-7b-1024x678.jpg?v=1777957275 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-00-7b-600x397.jpg?v=1777957275 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-00-7b-768x509.jpg?v=1777957275 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-00-7b.jpg?v=1777957275 1439w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>第二步：產生秘密金鑰</strong></p>



<p class="wp-block-paragraph">在左邊選單找到「憑證與密碼」，點進去後選「用戶端密碼」，接著按「新用戶端密碼」。你可以設定這組密碼多久會過期。</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="485" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-03-7c-1024x485.jpg?v=1777957435" alt="" class="wp-image-8382" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-03-7c-1024x485.jpg?v=1777957435 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-03-7c-600x284.jpg?v=1777957435 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-03-7c-768x363.jpg?v=1777957435 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-03-7c-1536x727.jpg?v=1777957435 1536w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/05/chrome_2026-05-05-13-03-7c.jpg?v=1777957435 2016w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph"><strong>第三步：立刻存檔</strong></p>



<p class="wp-block-paragraph">按下的那一刻，畫面會出現一串長長的字，那就是 Client Secret。記得這時候一定要趕快複製存起來！因為一旦離開這個畫面，系統就會把它遮起來，你再也看不到了，只能重新再產生一個 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" />。</p>



<p class="wp-block-paragraph">最後別忘了，還要回到你的 SQL Server 裡面，把這個應用程式的名字加進去並給它權限，你的程式才能順利進門。</p>



<p class="wp-block-paragraph">我覺得這種做法雖然多了一點步驟，但把權限跟程式碼分開管理，程式碼裡不再放死板板的密碼，而是改用這種可以隨時更換、受雲端監控的通行證。</p>



<p class="wp-block-paragraph">這樣一來，不只開發起來更優雅，安全等級也是直接拉滿，這才是專業開發者的懶人自保術呀 <img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">你知道除了手動複製，其實還有更安全的方法來管理這些秘密金鑰嗎？<br><br>既然都聊到這了，那一定要跟你分享進階版的絕招！</p>



<p class="wp-block-paragraph">雖然把 Client Secret 存在 Docker 的環境變數裡已經比以前進步，但對追求極致安全的工程師來說，這還不夠。因為金鑰還是存在某個地方，萬一主機被攻破，祕密還是會外流。</p>



<p class="wp-block-paragraph">這時候，我們可以用 Azure 提供的一種超神科技，叫做<strong>受控識別（Managed Identity）</strong>。</p>



<p class="wp-block-paragraph">這概念就像是你不用再幫程式辦身分證了，而是直接讓 Azure 認得這台 虛擬機 或 容器 服務本身。當程式要去連資料庫時，它會直接跟 Azure 說：「嘿，我是你家養的容器，讓我進去吧！」</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">簡單來說，更安全的作法有兩種：</p>



<p class="wp-block-paragraph"><strong>一、使用受控識別（最推薦）</strong></p>



<p class="wp-block-paragraph">這就像是 刷臉認證。你只要在 Azure 平台上把你的服務（例如 Azure Container Instances）開啟 受控識別 功能，並在 SQL 資料庫裡授權給它。</p>



<p class="wp-block-paragraph">這時候 Python 的連線字串就會變得超乾淨，完全不需要 UID 也不需要 PWD：</p>



<pre class="wp-block-code"><code>driver = {ODBC Driver 18 for SQL Server}
connection_string = (
　f"DRIVER={driver};"
　f"SERVER={server},{port};"
　f"DATABASE={database};"
　"Authentication=ActiveDirectoryMSI;"
　"Encrypt=yes;"
　"TrustServerCertificate=no;"
　"Connection Timeout=30;"
)</code></pre>



<p class="wp-block-paragraph">這真的是 懶人包 的極致，程式碼裡完全沒有任何祕密，安全性滿分！</p>



<p class="wp-block-paragraph"><strong>二、使用 Azure Key Vault</strong></p>



<p class="wp-block-paragraph">如果你還是需要用 Client Secret，可以把它藏在 雲端保險箱（Key Vault）裡面。</p>



<p class="wp-block-paragraph">程式執行時才去保險箱領鑰匙，而不是直接把鑰匙貼在門口。這樣就算別人看到你的程式碼或 Docker 設定，也拿不到真正的密碼。</p>



<pre class="wp-block-code"><code>範例程式碼：

driver = {ODBC Driver 18 for SQL Server}
connection_string = (
　f"DRIVER={driver};"
　f"SERVER={server},{port};"
　f"DATABASE={database};"
　f"UID={client_id};"
　f"PWD={client_secret};"
　"Authentication=ActiveDirectoryServicePrincipal;"
　"Encrypt=yes;"
　"TrustServerCertificate=no;"
　"Connection Timeout=30;"
)</code></pre>



<ul class="wp-block-list">
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f31f.png" alt="🌟" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 關鍵改版：我們把原本放「帳號」的地方換成 應用程式 ID（UID），把原本放「密碼」的地方換成 秘密金鑰（PWD）。</li>



<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f31f.png" alt="🌟" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 認證模式：這行 Authentication=<strong>ActiveDirectoryServicePrincipal </strong>是最關鍵的！它就像是跟資料庫打暗號，說：「嘿！我是用程式身分來敲門的，不是真人喔！」<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>



<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f31f.png" alt="🌟" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 安全提醒：連線字串裡的 Encrypt=yes 建議要留著，這樣資料在傳輸過程中才會被加密，就像是幫資料坐上防彈運鈔車一樣安全。<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f6e1.png" alt="🛡" class="wp-smiley" style="height: 1em; max-height: 1em;" /></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">我的小小建議：</p>



<p class="wp-block-paragraph">我覺得最高境界，就是根本不要持有秘密。</p>



<p class="wp-block-paragraph">當我們用<strong>受控識別（<strong>Managed Identity</strong>）</strong>的時候，身為開發者的我們連密碼長什麼樣子都不知道。不用擔心密碼過期、不用定期更換，更不用怕不小心把密碼推到 GitHub 上面去。</p>



<p class="wp-block-paragraph">這種把 安全性 融入到環境架構裡的作法，不只讓開發過程變得超清爽，心裡也會覺得非常踏實，這才是真正的優雅開發呀！<img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p class="wp-block-paragraph">這種 不用記密碼 的生活，是不是聽起來就很吸引人呢？</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/05/azure-sql-entra-id-pwd/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure Ubuntu VM 安裝 MSSQL Client</title>
		<link>https://stackoverflow.max-everyday.com/2026/04/azure-ubuntu-vm-odbc/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/04/azure-ubuntu-vm-odbc/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 29 Apr 2026 05:15:51 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8368</guid>

					<description><![CDATA[要在 Azure Ubuntu VM 上為 Py...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">要在 Azure Ubuntu VM 上為 Python 安裝 <strong>Microsoft ODBC Driver 18</strong>，你需要完成兩個主要部分：安裝系統層級的驅動程式，以及安裝 Python 介面庫（通常是 <code>pyodbc</code>）。</p>



<p class="wp-block-paragraph">請注意，<strong>ODBC Driver 18</strong> 預設啟用了加密連線（<code>Encrypt=yes</code>），這與舊版本（v17）不同，連線時可能需要調整連線字串。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第一步：安裝 Microsoft ODBC Driver 18</h3>



<p class="wp-block-paragraph">請根據你的 Ubuntu 版本執行以下指令（適用於 20.04, 22.04, 24.04）：</p>



<p class="wp-block-paragraph"><strong>導入 Microsoft 公鑰與來源清單</strong></p>



<pre class="wp-block-code"><code># 取得 Ubuntu 版本號 (如 22.04)
UBUNTU_VERSION=$(lsb_release -rs)

# 下載 Microsoft GPG 金鑰
curl https://packages.microsoft.com/keys/microsoft.asc | sudo tee /etc/apt/trusted.gpg.d/microsoft.asc

# 加入 Microsoft Ubuntu 套件庫
curl https://packages.microsoft.com/config/ubuntu/$UBUNTU_VERSION/prod.list | sudo tee /etc/apt/sources.list.d/mssql-release.list</code></pre>



<p class="wp-block-paragraph"><strong>安裝驅動程式與開發標頭檔</strong></p>



<p class="wp-block-paragraph">sudo apt-get update</p>



<h1 class="wp-block-heading">ACCEPT_EULA=Y 會自動同意授權合約</h1>



<pre class="wp-block-code"><code>sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev</code></pre>



<p class="wp-block-paragraph">上面這行在 Ubuntu 26.04 會出錯, 錯誤訊息:</p>



<pre class="wp-block-preformatted">E: The repository 'https://packages.microsoft.com/ubuntu/24.04/prod noble InRelease' is not signed.</pre>



<p class="wp-block-paragraph">This is a classic &#8220;security handshake&#8221; failure. Even though you added the repository link, <code>apt</code> is refusing to trust it because it can&#8217;t verify the digital signature. This usually happens if the GPG key wasn&#8217;t imported correctly or if there&#8217;s a conflict with how the key is stored in newer versions of Ubuntu (like 24.04 &#8220;Noble&#8221;).</p>



<p class="wp-block-paragraph">To fix this, we need to manually fetch the key and ensure it&#8217;s placed where <code>apt</code> expects it for that specific repository.</p>



<h3 class="wp-block-heading">1. Clear the &#8220;Bad&#8221; List</h3>



<p class="wp-block-paragraph">First, let&#8217;s remove the entry that is causing the error so we can start fresh:</p>



<p class="wp-block-paragraph">Bash</p>



<pre class="wp-block-code"><code>sudo rm /etc/apt/sources.list.d/mssql-release.list
</code></pre>



<h3 class="wp-block-heading">2. Properly Import the GPG Key</h3>



<p class="wp-block-paragraph">Ubuntu 24.04 is stricter about GPG keys. Instead of using <code>apt-key</code> (which is deprecated), use this method to download and de-armor the key:</p>



<p class="wp-block-paragraph">Bash</p>



<pre class="wp-block-code"><code>curl -fsSL https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft-prod.gpg
</code></pre>



<h3 class="wp-block-heading">3. Add the Repository with the Key Reference</h3>



<p class="wp-block-paragraph">Now, re-add the repository, but this time we will explicitly tell <code>apt</code> to use the keyring we just created to verify this specific URL:</p>



<p class="wp-block-paragraph">Bash</p>



<pre class="wp-block-code"><code>echo "deb &#91;arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/24.04/prod noble main" | sudo tee /etc/apt/sources.list.d/mssql-release.list
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">4. Update and Install</h3>



<p class="wp-block-paragraph">Now the signature check should pass:</p>



<p class="wp-block-paragraph">Bash</p>



<pre class="wp-block-code"><code>sudo apt-get update
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
</code></pre>



<h3 class="wp-block-heading">What went wrong?</h3>



<p class="wp-block-paragraph">In modern Linux distributions, &#8220;un-signed&#8221; repositories are blocked by default to prevent &#8220;man-in-the-middle&#8221; attacks where someone could inject malicious software into your update stream. By using the <code>signed-by</code> flag in the <code>.list</code> file, you are creating a direct trust link between the Microsoft URL and the Microsoft GPG key you just downloaded.</p>



<p class="wp-block-paragraph"><strong>One quick check:</strong> Are you definitely on Ubuntu 24.04? You can verify with <code>lsb_release -a</code>. If you are actually on 22.04, just swap the &#8220;24.04&#8221; and &#8220;noble&#8221; parts of the commands above with &#8220;22.04&#8221; and &#8220;jammy&#8221;.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"></p>



<p class="wp-block-paragraph"><strong>（選填）安裝 SQL Server 命令列工具 (sqlcmd/bcp)</strong></p>



<pre class="wp-block-code"><code>sudo ACCEPT_EULA=Y apt-get install -y mssql-tools18<br>echo 'export PATH="$PATH:/opt/mssql-tools18/bin"' &gt;&gt; ~/.bashrc<br>source ~/.bashrc</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第二步：安裝 Python 套件</h3>



<p class="wp-block-paragraph">在你的 Python 虛擬環境中安裝 <code>pyodbc</code>：</p>



<pre class="wp-block-code"><code>pip install pyodbc
</code></pre>



<p class="wp-block-paragraph">錯誤訊息:</p>



<p class="wp-block-paragraph">azureuser@vmptprodjpe001:~$ pip install pyodbc<br>error: externally-managed-environment</p>



<h3 class="wp-block-heading">解決方法：建立虛擬環境</h3>



<p class="wp-block-paragraph">請按照以下步驟操作：</p>



<p class="wp-block-paragraph">先確保系統有安裝建立虛擬環境的工具： </p>



<pre class="wp-block-code"><code>sudo apt update 
sudo apt install python3-venv python3-full</code></pre>



<p class="wp-block-paragraph">在你的專案目錄下建立一個名為 venv 的虛擬環境： </p>



<pre class="wp-block-code"><code>python3 -m venv venv</code></pre>



<p class="wp-block-paragraph">啟動虛擬環境： </p>



<pre class="wp-block-code"><code>source venv/bin/activate</code></pre>



<p class="wp-block-paragraph">啟動後，你會發現指令列開頭多了 (venv) 的字樣，這時就可以放心安裝套件了： </p>



<pre class="wp-block-code"><code>pip install pyodbc</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">第三步：Python 程式碼範例與注意事項</h3>



<p class="wp-block-paragraph">由於 ODBC 18 預設強制加密，如果你的 Azure SQL 伺服器證書不是由受信任的授權單位簽發的（或者你在本地測試），你可能需要在連線字串中加入 <code>TrustServerCertificate=yes</code>。</p>



<pre class="wp-block-code"><code>import pyodbc

# 請替換成你的 Azure SQL 資訊
server = 'your_server.database.windows.net'
database = 'your_db'
username = 'your_username'
password = 'your_password'

# 注意 Driver 名稱必須精確匹配：{ODBC Driver 18 for SQL Server}
conn_str = (
    f"DRIVER={{ODBC Driver 18 for SQL Server}};"
    f"SERVER={server};"
    f"DATABASE={database};"
    f"UID={username};"
    f"PWD={password};"
    "Encrypt=yes;"
    "TrustServerCertificate=no;" # 若連線失敗可改為 yes 測試，但在正式環境建議保持 no
    "Connection Timeout=30;"
)

try:
    conn = pyodbc.connect(conn_str)
    cursor = conn.cursor()
    cursor.execute("SELECT @@version;")
    row = cursor.fetchone()
    print(f"Connected! SQL Server Version: {row&#91;0]}")
except Exception as e:
    print(f"Error: {e}")
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">常見問題排除</h3>



<ul class="wp-block-list">
<li><strong>找不到驅動程式</strong>：執行 <code>odbcinst -q -d</code>。你應該會看到 <code>[ODBC Driver 18 for SQL Server]</code>。如果沒有，請確認第一步安裝是否成功。</li>



<li><strong>SSL/TLS 錯誤</strong>：Azure SQL 要求 TLS 1.2 以上版本。Ubuntu 20.04+ 通常預設支援，但如果你在連線時遇到證書驗證錯誤，請檢查 Azure SQL 的防火牆設定，並確認 <code>TrustServerCertificate</code> 的設定。</li>



<li><strong>權限問題</strong>：在 Azure VM 上，確保網路安全性群組 (NSG) 已允許 <strong>1433</strong> 埠通訊，且 Azure SQL 的防火牆已將該 VM 的公用 IP 加入白名單。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">Error</p>



<pre class="wp-block-code"><code>W: OpenPGP signature verification failed: https://packages.microsoft.com/ubuntu/26.04/prod resolute InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY EE4D7792F748182B
E: The repository 'https://packages.microsoft.com/ubuntu/26.04/prod resolute InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.</code></pre>



<p class="wp-block-paragraph">看來在 Ubuntu 24.04 (Noble) 上，系統沒能正確識別或找到你剛才匯入的 Microsoft GPG 金鑰。這通常是因為金鑰存放路徑與來源清單（<code>.list</code>）中定義的路徑不一致導致的。</p>



<p class="wp-block-paragraph">從你的錯誤訊息看，<code>noble InRelease</code> 找不到金鑰 <code>EB3E94ADBE1229CF</code>。</p>



<h3 class="wp-block-heading">解決方案</h3>



<p class="wp-block-paragraph">請按照以下步驟重新匯入金鑰並修正路徑：</p>



<h4 class="wp-block-heading">1. 下載並轉換金鑰格式</h4>



<p class="wp-block-paragraph">Ubuntu 24.04 偏好使用 <code>.gpg</code> 格式存放在 <code>/usr/share/keyrings/</code>。</p>



<pre class="wp-block-code"><code># 下載並將金鑰轉換為 gpg 格式存放到指定位置
curl -sSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | sudo tee /usr/share/keyrings/microsoft-prod.gpg &gt; /dev/null</code></pre>



<h4 class="wp-block-heading">2. 更新來源清單 (Source List)</h4>



<p class="wp-block-paragraph">確保你的 <code>mssql-release.list</code> 指向的是正確的金鑰路徑（剛才建立的那個）：</p>



<pre class="wp-block-code"><code>echo "deb &#91;arch=amd64,arm64,armhf signed-by=/usr/share/keyrings/microsoft-prod.gpg] https://packages.microsoft.com/ubuntu/24.04/prod noble main" | sudo tee /etc/apt/sources.list.d/mssql-release.list
</code></pre>



<h4 class="wp-block-heading">3. 再次執行安裝</h4>



<p class="wp-block-paragraph">現在更新索引，這一次應該不會報 GPG 錯誤了：</p>



<pre class="wp-block-code"><code>sudo apt-get update
sudo ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev
</code></pre>



<h3 class="wp-block-heading">驗證安裝結果</h3>



<p class="wp-block-paragraph">安裝完成後，你可以執行這個指令確認驅動程式已經就緒：</p>



<pre class="wp-block-code"><code>odbcinst -q -d -n "ODBC Driver 18 for SQL Server"
</code></pre>



<p class="wp-block-paragraph">如果出現驅動程式的路徑資訊，就代表你成功了！接著就可以繼續用 <code>pip install pyodbc</code> 進行 Python 開發。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">要在 Ubuntu 終端機直接測試連線，我們通常會使用 <code>sqlcmd</code> (隨 mssql-tools18 安裝) 或者使用 <code>isql</code> (unixODBC 自帶工具)。</p>



<p class="wp-block-paragraph">由於你安裝的是 <strong>ODBC Driver 18</strong>，請特別注意連線字串中的<strong>加密選項</strong>。</p>



<h3 class="wp-block-heading">方法一：使用 <code>isql</code> 進行快速測試</h3>



<p class="wp-block-paragraph"><code>isql</code> 是 unixODBC 提供的工具，適合用來驗證驅動程式是否能正確與資料庫握手。</p>



<p class="wp-block-paragraph">請在終端機輸入以下指令（將參數直接寫在連線字串中）：</p>



<pre class="wp-block-code"><code># -v 代表 verbose (詳細輸出)，可以幫你判斷錯誤原因
# 注意：ODBC 18 預設強制加密，若伺服器憑證未經第三方認證，需加上 TrustServerCertificate=yes
isql -v -k "DRIVER={ODBC Driver 18 for SQL Server};SERVER=1.2.3.4,1433;DATABASE=YOUR_DB;UID=userid;PWD=userpwd;Encrypt=yes;TrustServerCertificate=yes"
</code></pre>



<ul class="wp-block-list">
<li>如果成功，你會看到 <code>Connected!</code> 提示。</li>



<li>輸入 <code>quit</code> 即可結束連線。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方法二：使用 <code>sqlcmd</code> (Microsoft 官方工具)</h3>



<p class="wp-block-paragraph">如果你之前有安裝 <code>mssql-tools18</code>，這是最推薦的測試方式：</p>



<pre class="wp-block-code"><code>sqlcmd -S 1.2.3.4,1433 -d YOUR_DB -U userid -P 'userpwd' -C
</code></pre>



<ul class="wp-block-list">
<li>參數 <code>-C</code> 代表 <code>TrustServerCertificate</code>（信任伺服器憑證）。</li>



<li>注意：如果密碼包含特殊字元，請用<strong>單引號 <code>' '</code></strong> 包起來。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">疑難排解：連線失敗的常見原因</h3>



<ol start="1" class="wp-block-list">
<li><strong>Network Timeout (連線逾時)</strong>：
<ul class="wp-block-list">
<li><strong>檢查 Azure NSG</strong>：確保你的 Azure VM 出站規則 (Outbound) 允許 1433 埠。</li>



<li><strong>檢查目標伺服器</strong>：1.2.x.x  的 IP，請確認該伺服器防火牆是否有將你的 Azure VM <strong>公用 IP</strong> 加入白名單。</li>
</ul>
</li>



<li><strong>SSL Provider 錯誤 (憑證問題)</strong>：
<ul class="wp-block-list">
<li>這是 ODBC 18 最常見的問題。如果沒加 <code>TrustServerCertificate=yes</code> (或 <code>isql</code> 中的參數)，連線會因為無法驗證憑證而中斷。</li>
</ul>
</li>



<li><strong>Login Failed</strong>：
<ul class="wp-block-list">
<li>請確認資料庫名稱 YOUR_DB 與帳密無誤。</li>
</ul>
</li>
</ol>



<h3 class="wp-block-heading">Python 程式碼快速驗證</h3>



<p class="wp-block-paragraph">如果你已經進入了虛擬環境並安裝了 <code>pyodbc</code>，可以用這段代碼直接測試：</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>import pyodbc

conn_str = (
    "DRIVER={ODBC Driver 18 for SQL Server};"
    "SERVER=1.2.3.4,1433;"
    "DATABASE=YOUR_DB;"
    "UID=userid;"
    "PWD=userpwd;"
    "Encrypt=yes;"
    "TrustServerCertificate=yes;"
)

try:
    with pyodbc.connect(conn_str, timeout=5) as conn:
        print("連線成功！")
        cursor = conn.cursor()
        cursor.execute("SELECT TOP 1 name FROM sys.databases")
        print(f"資料庫清單測試: {cursor.fetchone()}")
except Exception as e:
    print(f"連線失敗原因: {e}")
</code></pre>



<p class="wp-block-paragraph">你的連線測試結果出現什麼錯誤訊息嗎？</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/04/azure-ubuntu-vm-odbc/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Go MSSQL 分頁實作比較與建議</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/go-sql-paging/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/go-sql-paging/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 25 Mar 2026 05:40:34 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8130</guid>

					<description><![CDATA[在 Go 語言開發後端並對接 MSSQL 時，分...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="572" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/go-sql-paging-1024x572.jpg?v=1774417037" alt="" class="wp-image-8131" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/go-sql-paging-1024x572.jpg?v=1774417037 1024w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/go-sql-paging-600x335.jpg?v=1774417037 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/go-sql-paging-768x429.jpg?v=1774417037 768w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/go-sql-paging.jpg?v=1774417037 1376w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p class="wp-block-paragraph">在 Go 語言開發後端並對接 MSSQL 時，分頁（Pagination）是一個非常經典的效能優化課題。對於 <strong>Cursor-based（指標式）</strong> 與 <strong>Offset-based（偏移量式，即你提到的下兩次 SQL）</strong> 的選擇，取決於你的業務場景（如：後台管理系統 vs. 無限捲動的 App）。</p>



<p class="wp-block-paragraph">以下是詳細的比較、實作建議與常見坑點。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">一、 分頁方案深度對比</h3>



<h4 class="wp-block-heading">1. Offset-based Paging (兩次 SQL 請求)</h4>



<p class="wp-block-paragraph">這是最常見的作法：先 <code>SELECT COUNT(*)</code> 算總數，再用 <code>OFFSET / FETCH</code> 取資料。</p>



<ul class="wp-block-list">
<li><strong>實作方式 (MSSQL 2012+)：</strong>SQL<code>-- 第 1 次：拿總筆數 SELECT COUNT(*) FROM Orders WHERE Status = 1; -- 第 2 次：拿分頁資料 SELECT * FROM Orders WHERE Status = 1 ORDER BY OrderID OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY;</code></li>



<li><strong>優點：</strong>
<ul class="wp-block-list">
<li><strong>支援跳頁：</strong> 使用者可以直接點擊「第 5 頁」。</li>



<li><strong>前端友善：</strong> 容易計算總頁數、顯示剩餘頁數。</li>
</ul>
</li>



<li><strong>缺點：</strong>
<ul class="wp-block-list">
<li><strong>效能問題：</strong> 當 <code>OFFSET</code> 很大時（例如萬級跳頁），資料庫必須掃描並丟棄前面的資料，效能劇降。</li>



<li><strong>資料飄移：</strong> 在分頁過程中若有新資料插入，使用者可能會看到重複的資料。</li>
</ul>
</li>
</ul>



<h4 class="wp-block-heading">2. Cursor-based Paging (Seek Method)</h4>



<p class="wp-block-paragraph">不使用偏移量，而是根據上一頁最後一筆資料的唯一標識（如 ID 或時間戳）來找下一頁。</p>



<ul class="wp-block-list">
<li><strong>實作方式：</strong>SQL<code>-- 假設上一頁最後一個 ID 是 500 SELECT TOP 10 * FROM Orders WHERE Status = 1 AND OrderID > 500 ORDER BY OrderID ASC;</code></li>



<li><strong>優點：</strong>
<ul class="wp-block-list">
<li><strong>高效能：</strong> 無論翻到多深，效能幾乎一致，因為它利用索引直接定位。</li>



<li><strong>穩定性：</strong> 不會因為資料插入/刪除導致看到重複或遺漏資料。</li>
</ul>
</li>



<li><strong>缺點：</strong>
<ul class="wp-block-list">
<li><strong>無法跳頁：</strong> 只能「下一頁」或「上一頁」，適合「載入更多」或無限捲動。</li>



<li><strong>排序限制：</strong> 排序欄位必須具有唯一性（或搭配主鍵排序）。</li>
</ul>
</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">二、 比較總覽表</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>特性</strong></td><td><strong>Offset-based (2 SQLs)</strong></td><td><strong>Cursor-based (Seek)</strong></td></tr></thead><tbody><tr><td><strong>隨機跳頁</strong></td><td>支援</td><td><strong>不支援</strong></td></tr><tr><td><strong>大數據效能</strong></td><td>差 (隨著頁數增加變慢)</td><td><strong>優 (效能穩定)</strong></td></tr><tr><td><strong>實作難度</strong></td><td>簡單</td><td>中等 (需記錄最後一個 ID)</td></tr><tr><td><strong>適用場景</strong></td><td>管理後台、報表、小規模資料</td><td>App 瀑布流、大型日誌、百萬級資料</td></tr><tr><td><strong>MSSQL 特效</strong></td><td><code>OFFSET...FETCH</code> 語法</td><td>依賴索引 B-Tree 搜尋</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">三、 其他實作解決辦法與第三方套件</h3>



<p class="wp-block-paragraph">在 Go 的生態系中，你不需要從零開始寫 SQL，可以利用以下套件處理：</p>



<h4 class="wp-block-heading">1. GORM (最流行的 ORM)</h4>



<p class="wp-block-paragraph">GORM 內建了分頁的封裝，但通常是基於 Offset 的。</p>



<ul class="wp-block-list">
<li><strong>Offset 實作：</strong> <code>db.Limit(10).Offset(20).Find(&amp;users)</code></li>



<li><strong>問題：</strong> 它不會自動幫你做 <code>COUNT(*)</code>，你通常需要自己寫一個 Wrapper 函數跑兩次查詢。</li>
</ul>



<h4 class="wp-block-heading">2. Ent (Facebook 出品的強型別 ORM)</h4>



<p class="wp-block-paragraph">Ent 對於分頁的 API 設計非常優雅，支援較複雜的查詢組合。</p>



<ul class="wp-block-list">
<li><strong>優點：</strong> 程式碼生成技術讓你的分頁查詢在編譯期就受檢查。</li>
</ul>



<h4 class="wp-block-heading">3. SQLX</h4>



<p class="wp-block-paragraph">如果你喜歡原生 SQL，<code>sqlx</code> 是首選。它沒有自動分頁功能，但能輕鬆將結果 Map 到 Struct。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">四、 實作中遇到的常見問題與坑</h3>



<h4 class="wp-block-heading">1. MSSQL 的 <code>ORDER BY</code> 限制</h4>



<p class="wp-block-paragraph">在 MSSQL 中，使用 <code>OFFSET / FETCH</code> <strong>必須</strong>搭配 <code>ORDER BY</code>。如果你沒寫 <code>ORDER BY</code>，SQL 會報錯。這在 Go 寫動態查詢時常被忽略。</p>



<h4 class="wp-block-heading">2. <code>COUNT(*)</code> 的效能殺手</h4>



<p class="wp-block-paragraph">當資料量達到千萬級，<code>SELECT COUNT(*)</code> 會變得非常慢。</p>



<ul class="wp-block-list">
<li><strong>解決：</strong> 如果不要求精確總數，可以從 MSSQL 的系統表（<code>sys.partitions</code>）讀取近似值，或者快取總數（Cache）。</li>
</ul>



<h4 class="wp-block-heading">3. Deep Paging (深分頁) 攻擊</h4>



<p class="wp-block-paragraph">惡意使用者或爬蟲可能會請求 <code>OFFSET 1000000</code>，這會瞬間拖垮資料庫 IO。</p>



<ul class="wp-block-list">
<li><strong>解決：</strong> 強制限制最大跳頁數，或者超過一定頁數後強制切換為 Cursor 模式。</li>
</ul>



<h4 class="wp-block-heading">4. N+1 問題</h4>



<p class="wp-block-paragraph">在 Go 實作分頁時，如果拿到分頁後又在 Loop 裡查詢關聯資料，會導致效能崩潰。</p>



<ul class="wp-block-list">
<li><strong>解決：</strong> 使用 <code>JOIN</code> 或 <code>Preload</code> (Eager Loading)。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">五、 最終建議</h3>



<ol start="1" class="wp-block-list">
<li><strong>一般管理後台：</strong> 使用 <strong>Offset-based</strong>。雖然它有兩次 SQL，但在資料量小於 10 萬筆時，使用者體驗最佳（因為能跳頁）。你可以寫一個 Go <code>Pagination</code> Helper 函式來統一回傳 <code>{total, page, data}</code>。</li>



<li><strong>高流量 / 大數據 App：</strong> 務必使用 <strong>Cursor-based</strong>。</li>



<li><strong>效能折衷方案：</strong>
<ul class="wp-block-list">
<li>先查出 ID 清單（Covering Index），再用 ID 去 <code>IN</code> 查資料。</li>



<li><code>SELECT COUNT(*)</code> 加上快取（例如 Redis 存 5 分鐘），不需要每次翻頁都重新算總數。</li>
</ul>
</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">從 MSSQL 切換到 MySQL 或 PostgreSQL，對於 Go 後端開發來說，最直接的影響在於 <strong>SQL 語法（Dialect）</strong>、<strong>參數佔位符</strong>以及<strong>底層驅動的行為</strong>。</p>



<p class="wp-block-paragraph">針對你關心的「分頁實作」，以下是切換後的詳細影響分析：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">一、 語法差異：分頁關鍵字</h3>



<p class="wp-block-paragraph">這是最明顯的改變。雖然邏輯不變，但 SQL 字串必須重寫。</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>資料庫</strong></td><td><strong>分頁語法 (Offset-based)</strong></td><td><strong>特性</strong></td></tr></thead><tbody><tr><td><strong>MSSQL</strong></td><td><code>OFFSET 10 ROWS FETCH NEXT 5 ROWS ONLY</code></td><td>必須搭配 <code>ORDER BY</code> 才能運作。</td></tr><tr><td><strong>MySQL</strong></td><td><code>LIMIT 5 OFFSET 10</code> (或 <code>LIMIT 10, 5</code>)</td><td>語法最簡潔，不強制要求 <code>ORDER BY</code>（但實務上強烈建議）。</td></tr><tr><td><strong>PostgreSQL</strong></td><td><code>LIMIT 5 OFFSET 10</code></td><td>語法與 MySQL 相同。</td></tr></tbody></table></figure>



<p class="wp-block-paragraph"><strong>Cursor-based 的影響：</strong></p>



<p class="wp-block-paragraph">三者在 Cursor 模式下的 <code>WHERE ID &gt; ? LIMIT 10</code> 語法幾乎完全一致。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">二、 Go 程式碼層面的影響</h3>



<h4 class="wp-block-heading">1. SQL 參數佔位符 (Placeholders)</h4>



<p class="wp-block-paragraph">這是 Go 開發者最容易踩坑的地方，不同的驅動對參數的標示方式不同：</p>



<ul class="wp-block-list">
<li><strong>MySQL:</strong> 使用問號 <code>?</code>（例如：<code>WHERE id = ?</code>）</li>



<li><strong>PostgreSQL:</strong> 使用序號 <code>$1</code>, <code>$2</code>（例如：<code>WHERE id = $1</code>）</li>



<li><strong>MSSQL:</strong> 通常使用 <code>@p1</code> 或 <code>?</code>（取決於驅動）</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>影響：</strong> 如果你寫的是原生 SQL，切換資料庫時，你必須修改所有的查詢字串。若使用 <strong>GORM</strong> 或 <strong>Ent</strong>，ORM 會自動幫你處理掉這個差異。</p>
</blockquote>



<h4 class="wp-block-heading">2. 驅動程式 (Drivers)</h4>



<ul class="wp-block-list">
<li><strong>MySQL:</strong> 常用 <code>github.com/go-sql-driver/mysql</code>。</li>



<li><strong>PostgreSQL:</strong> 推薦使用 <code>github.com/jackc/pgx</code>，效能與對功能（如 JSONB）的支援比舊的 <code>lib/pq</code> 好很多。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">三、 效能與實作細節的影響</h3>



<h4 class="wp-block-heading">1. PostgreSQL 的 <code>COUNT(*)</code> 痛點</h4>



<p class="wp-block-paragraph">在分頁需要回傳「總筆數」時，<strong>PostgreSQL 的 <code>COUNT(*)</code> 通常比 MySQL 和 MSSQL 慢</strong>。</p>



<ul class="wp-block-list">
<li><strong>原因：</strong> 由於 PostgreSQL 的多版本並發控制 (MVCC) 機制，它必須掃描資料來確定哪些列對當前事務是「可見的」，無法像某些引擎一樣直接從元數據讀取。</li>



<li><strong>解決：</strong> 在 PG 中，大數據量的分頁總數通常會改用 <code>reliquary</code>（估算值）或額外維護一個計數表。</li>
</ul>



<h4 class="wp-block-heading">2. MySQL 的大 Offset 效能</h4>



<p class="wp-block-paragraph">MySQL 在處理 <code>LIMIT 100000, 10</code> 時，效能損耗非常明顯。雖然這在所有 DB 都有發生，但 MySQL 的優化器在某些情況下處理「延遲關聯」（Late Row Lookups）的手法與 MSSQL 不同，需要手動優化。</p>



<h4 class="wp-block-heading">3. 交易隔離級別 (Isolation Levels)</h4>



<ul class="wp-block-list">
<li><strong>MSSQL:</strong> 預設有時會發生讀寫鎖定衝突。</li>



<li><strong>PostgreSQL:</strong> 預設是 <code>Read Committed</code>，在處理高併發分頁時表現非常穩定。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">四、 第三方套件的處理方案</h3>



<p class="wp-block-paragraph">如果你想讓系統在切換資料庫時更輕鬆，建議引入以下套件：</p>



<ol start="1" class="wp-block-list">
<li><strong>GORM (Object Relational Mapper)</strong>
<ul class="wp-block-list">
<li><strong>優點：</strong> 你只需更改初始化時的 <code>gorm.Open(mysql.Open(dsn))</code>，分頁程式碼 <code>db.Offset(10).Limit(5)</code> 完全不用動。</li>



<li><strong>缺點：</strong> 隱藏了底層細節，有時難以優化特定 DB 的特殊索引行為。</li>
</ul>
</li>



<li><strong>Masterminds/squirrel (SQL Builder)</strong>
<ul class="wp-block-list">
<li>如果你不想用 ORM 但想解決佔位符問題，squirrel 可以幫你動態構建 SQL。</li>



<li>它可以根據設定生成 <code>?</code> 或 <code>$1</code>，讓你換 DB 時不用手改字串。</li>
</ul>
</li>



<li><strong>ozzo-dbx / sqlx</strong>
<ul class="wp-block-list">
<li>輕量級封裝，雖然仍要寫 SQL，但簡化了將分頁結果 Map 到 Go Struct 的過程。</li>
</ul>
</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">五、 總結建議</h3>



<ol start="1" class="wp-block-list">
<li><strong>如果你追求開發速度與靈活性：</strong> 切換到 <strong>PostgreSQL</strong>。它對進階資料型別（JSONB, Array）的支援極佳，且分頁效能穩定。Go 社群對 PG 的支持度（如 <code>pgx</code> 驅動）目前是最高的。</li>



<li><strong>如果你擔心遷移成本：</strong>
<ul class="wp-block-list">
<li>若使用 <strong>ORM (GORM)</strong>：切換成本極低，只需調整配置。</li>



<li>若使用 <strong>原生 SQL</strong>：建議先建立一個 <code>QueryBuilder</code> 層，將 <code>LIMIT/OFFSET</code> 的生成抽象化，否則手改數百個 <code>?</code> 到 <code>$1</code> 會非常痛苦。</li>
</ul>
</li>



<li><strong>分頁策略調整：</strong>
<ul class="wp-block-list">
<li>無論換到哪種 DB，若資料量大，<strong>Cursor-based</strong> 永遠是首選。</li>



<li>若必須用 <strong>Offset-based</strong>，請務必在切換後針對 <code>COUNT(*)</code> 進行壓力測試，尤其是 PostgreSQL。</li>
</ul>
</li>
</ol>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/go-sql-paging/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>在 Windows 建立 Docker Compose 雲端開發環境</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/windows-docker-compose-to-cloud/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/windows-docker-compose-to-cloud/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Thu, 19 Mar 2026 09:04:56 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[mssql]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8033</guid>

					<description><![CDATA[要在 Windows 上開發一個結合 FastA...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">要在 Windows 上開發一個結合 <strong>FastAPI (Python)</strong>、<strong>React (Node.js)</strong> 與 <strong>Database</strong> 的容器化應用，最好的做法是利用 <strong>Docker Compose</strong>。這能讓你一鍵啟動所有服務，並模擬雲端（Azure）的多容器運行環境。</p>



<p class="wp-block-paragraph">既然你已經安裝了 Docker Desktop 和 Node.js，接下來可以按照以下步驟建立開發環境：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">1. 專案目錄結構建議</h2>



<p class="wp-block-paragraph">首先，建議建立一個清晰的資料夾結構，將前端、後端與 Docker 設定分開：</p>



<p class="wp-block-paragraph">Plaintext</p>



<pre class="wp-block-code"><code>my-app/
├── backend/           # FastAPI 程式碼
│   ├── main.py
│   ├── requirements.txt
│   └── Dockerfile
├── frontend/          # React 程式碼
│   ├── src/
│   ├── package.json
│   └── Dockerfile
└── docker-compose.yml # 核心：同時啟動前後端與資料庫
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">2. 後端環境準備 (FastAPI)</h2>



<p class="wp-block-paragraph">在 <code>backend/</code> 資料夾中，建立以下兩個核心檔案：</p>



<p class="wp-block-paragraph"><strong>requirements.txt</strong>:</p>



<pre class="wp-block-code"><code>fastapi
uvicorn
sqlalchemy
psycopg2-binary  # 如果使用 PostgreSQL</code></pre>



<p class="wp-block-paragraph"><strong>Dockerfile</strong>:</p>



<p class="wp-block-paragraph">Dockerfile</p>



<pre class="wp-block-code"><code>FROM python:3.11-slim<br>WORKDIR /app<br>COPY requirements.txt .<br>RUN pip install --no-cache-dir -r requirements.txt<br>COPY . .<br>CMD &#91;"uvicorn", "main.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]</code></pre>



<p class="wp-block-paragraph"><strong>Note:</strong> <code>--reload</code> 參數非常重要，它讓你修改代碼時，容器內的服務會自動重啟。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">3. 前端環境準備 (React)</h2>



<p class="wp-block-paragraph">在 <code>my-app/</code> 目錄下執行 </p>



<pre class="wp-block-code"><code><code>npx create-react-app frontend</code></code></pre>



<p class="wp-block-paragraph">然後在 <code>frontend/</code> 建立 <strong>Dockerfile</strong>:</p>



<p class="wp-block-paragraph">Dockerfile</p>



<pre class="wp-block-code"><code>FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD &#91;"npm", "start"]
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">4. 關鍵：配置 Docker Compose</h2>



<p class="wp-block-paragraph">這是將所有組件串聯起來的核心。在專案根目錄建立 <code>docker-compose.yml</code>：</p>



<p class="wp-block-paragraph">YAML</p>



<pre class="wp-block-code"><code>services:
  db:
    image: postgres:15 # 以 PostgreSQL 為例
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: password
      POSTGRES_DB: mydatabase
    ports:
      - "5432:5432"

  backend:
    build: ./backend
    volumes:
      - ./backend:/app # 掛載程式碼，實現即時更新
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgresql://user:password@db:5432/mydatabase
    depends_on:
      - db

  frontend:
    build: ./frontend
    volumes:
      - ./frontend:/app
      - /app/node_modules # 避免本地 node_modules 覆蓋容器內的
    ports:
      - "3000:3000"
    stdin_open: true # 讓 React 偵測到開發終端
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">5. 開發工作流程</h2>



<p class="wp-block-paragraph"><strong>啟動環境</strong>：在根目錄開啟終端機（PowerShell 或 CMD），輸入：</p>



<pre class="wp-block-code"><code><code>docker-compose up --build</code></code></pre>



<p class="wp-block-paragraph"><strong>存取服務</strong>：</p>



<ul class="wp-block-list">
<li>前端：<code><a href="http://localhost:3000">http://localhost:3000</a></code></li>



<li>後端 API：<code><a href="http://localhost:8000">http://localhost:8000</a></code></li>



<li>API 文件 (Swagger)：<code><a href="http://localhost:8000/docs">http://localhost:8000/docs</a></code></li>
</ul>



<p class="wp-block-paragraph"><strong>連接資料庫</strong>：在 FastAPI 中，資料庫的連線主機名稱（Hostname）直接填寫 <code>db</code>（即 Compose 中的服務名稱），而非 <code>localhost</code>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">6. Azure 準備 (下一步建議)</h2>



<p class="wp-block-paragraph">由於你最終要上傳到 Azure，建議你現在可以先安裝 <strong>Azure CLI</strong>：</p>



<p class="wp-block-paragraph"><strong>指令</strong>：</p>



<pre class="wp-block-code"><code><code>winget install -e --id Microsoft.AzureCLI</code></code></pre>



<p class="wp-block-paragraph"><strong>目的</strong>：這讓你能在本地端直接將 Docker Image 推送到 <strong>Azure Container Registry (ACR)</strong>，並部署到 <strong>Azure Container Apps</strong> 或 <strong>App Service</strong>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>溫馨提示</strong>：在 Windows 上開發時，建議將專案放在 <strong>WSL 2</strong> (Ubuntu) 的檔案系統內（路徑通常是 <code>\\wsl$\...</code>），Docker 的讀寫效能會比在 Windows 原生路徑下快非常多。</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">移除 sqlalchemy, database 使用 microsoft sql server, 在 azure 上會換成azure sql</p>



<p class="wp-block-paragraph">既然你要將資料庫從 PostgreSQL 換成 <strong>Microsoft SQL Server</strong> (本地開發) 與 <strong>Azure SQL</strong> (雲端部署)，並移除 SQLAlchemy 改用更輕量或原生的方式，我們的開發環境需要做一些關鍵調整。</p>



<p class="wp-block-paragraph">在 Windows 上連接 SQL Server，通常建議使用 <code>pyodbc</code> 驅動程式。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">1. 調整後的專案結構</h2>



<p class="wp-block-paragraph">結構保持不變，但 <code>backend</code> 的配置會有所不同：</p>



<pre class="wp-block-code"><code>my-app/
├── backend/
│   ├── main.py
│   ├── requirements.txt  # 加入 pyodbc
│   └── Dockerfile        # 需要安裝 SQL Server 驅動
├── frontend/             # React (Node.js)
└── docker-compose.yml    # 伺服器改為 MSSQL Edge 或 SQL Server
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">2. 後端環境 (FastAPI + MS SQL)</h2>



<h3 class="wp-block-heading"><strong>requirements.txt</strong></h3>



<pre class="wp-block-code"><code>fastapi
uvicorn
pyodbc
</code></pre>



<h3 class="wp-block-heading"><strong>Dockerfile (關鍵修改)</strong></h3>



<p class="wp-block-paragraph">連接 SQL Server 需要在 Linux 容器內安裝 Microsoft ODBC Driver。這部分的 Dockerfile 較為特殊：</p>



<p class="wp-block-paragraph">Dockerfile</p>



<pre class="wp-block-code"><code>FROM python:3.11-slim

# 1. 安裝必要工具
RUN apt-get update &amp;&amp; apt-get install -y \
    curl \
    gnupg2 \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

# 2. 下載並安裝 Microsoft ODBC Driver 18 (適用於 Debian 12/11)
RUN curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor &gt; /usr/share/keyrings/microsoft-prod.gpg \
    &amp;&amp; curl https://packages.microsoft.com/config/debian/12/prod.list &gt; /etc/apt/sources.list.d/mssql-release.list \
    &amp;&amp; apt-get update \
    &amp;&amp; ACCEPT_EULA=Y apt-get install -y msodbcsql18 unixodbc-dev \
    &amp;&amp; rm -rf /var/lib/apt/lists/*

# 3. 設定工作目錄
WORKDIR /app

# 4. 安裝 Python 套件
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 5. 複製程式碼並啟動
COPY . .
CMD &#91;"uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">3. Docker Compose 配置</h2>



<p class="wp-block-paragraph">我們在本地使用 <code>mcr.microsoft.com/azure-sql-edge</code>，它是專為輕量開發設計的 SQL Server 版本（支援 ARM 與 x64，且在 Docker 上跑得更快）。</p>



<p class="wp-block-paragraph"><strong>docker-compose.yml</strong>:</p>



<p class="wp-block-paragraph">YAML</p>



<pre class="wp-block-code"><code>services:
  db:
    image: mcr.microsoft.com/azure-sql-edge
    container_name: mssql_db
    environment:
      ACCEPT_EULA: "Y"
      MSSQL_SA_PASSWORD: "YourStrongPassword123!"
    ports:
      - "1433:1433"

  backend:
    build: ./backend
    volumes:
      - ./backend:/app
    ports:
      - "8000:8000"
    environment:
      # 注意：連接字串需包含 TrustServerCertificate=yes
      DB_CONNECTION_STRING: "DRIVER={ODBC Driver 18 for SQL Server};SERVER=db;DATABASE=master;UID=sa;PWD=YourStrongPassword123!;Encrypt=yes;TrustServerCertificate=yes"
    depends_on:
      - db

  frontend:
    build: ./frontend
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    stdin_open: true
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">4. FastAPI 存取範例 (main.py)</h2>



<p class="wp-block-paragraph">不使用 SQLAlchemy，我們直接使用 <code>pyodbc</code> 進行查詢：</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>from fastapi import FastAPI
import pyodbc
import os

app = FastAPI()

# 從環境變數讀取連線資訊
conn_str = os.getenv("DB_CONNECTION_STRING")

@app.get("/")
def read_root():
    try:
        conn = pyodbc.connect(conn_str)
        cursor = conn.cursor()
        cursor.execute("SELECT @@VERSION")
        row = cursor.fetchone()
        return {"database_version": row&#91;0]}
    except Exception as e:
        return {"error": str(e)}
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">5. 開發建議與注意事項</h2>



<ul class="wp-block-list">
<li><strong>Azure SQL 的差異</strong>：當你部署到 Azure 時，只需修改 <code>DB_CONNECTION_STRING</code> 環境變數。Azure SQL 強制要求加密 (<code>Encrypt=yes</code>)，這也是為什麼我們在 Dockerfile 中安裝了最新版的 <strong>ODBC Driver 18</strong>。</li>



<li><strong>防火牆</strong>：如果你在本地使用 SQL Server Management Studio (SSMS) 連接 Docker 裡的資料庫，伺服器名稱請填 <code>127.0.0.1,1433</code>。</li>



<li><strong>前端 Proxy</strong>：在 React 的 <code>package.json</code> 中加入 <code>"proxy": "http://backend:8000"</code>，可以簡化前端呼叫 API 的程式碼，避免 CORS 問題。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">要在 Windows 本地測試並推送到 Azure，流程主要分為 <strong>「本地聯動測試」</strong> 與 <strong>「雲端推送部署」</strong> 兩個階段。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">1. 本地 Docker Desktop 測試</h2>



<p class="wp-block-paragraph">在本地測試時，最重要的是確保 <strong>跨容器通訊</strong> 正常（例如 React 如何抓到 FastAPI 的資料）。</p>



<h3 class="wp-block-heading"><strong>執行測試指令</strong></h3>



<p class="wp-block-paragraph">開啟 PowerShell 或終端機，進入專案根目錄（包含 <code>docker-compose.yml</code> 的地方）：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 1. 啟動所有服務 (包含 MSSQL, Backend, Frontend)
docker-compose up --build

# 2. 如果要以後台模式執行
docker-compose up -d
</code></pre>



<h3 class="wp-block-heading"><strong>如何驗證測試成功？</strong></h3>



<ul class="wp-block-list">
<li><strong>API 測試</strong>：瀏覽 <code>http://localhost:8000/docs</code>，點擊 <code>Execute</code> 測試 API 是否能成功從 MSSQL 讀取資料。</li>



<li><strong>前端測試</strong>：瀏覽 <code>http://localhost:3000</code>。</li>



<li><strong>查看日誌</strong>：如果連線失敗，執行 <code>docker-compose logs backend</code> 查看 Python 的 <code>pyodbc</code> 報錯訊息。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">2. 使用 Azure CLI 推送到 Azure Container Registry (ACR)</h2>



<p class="wp-block-paragraph">當本地測試沒問題後，你需要將 Image 推送到雲端倉庫。Azure 提供 <strong>ACR (Azure Container Registry)</strong> 來存放這些映像檔。</p>



<h3 class="wp-block-heading"><strong>步驟 A：登入 Azure 並建立資源</strong></h3>



<p class="wp-block-paragraph">首先，確保你已安裝 <a target="_blank" rel="noreferrer noopener" href="https://learn.microsoft.com/zh-tw/cli/azure/install-azure-cli-windows">Azure CLI</a>。</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 1. 登入 Azure
az login

# 2. 建立資源群組 (如果還沒有)
az group create --name MyResourceGroup --location eastasia

# 3. 建立 ACR (名稱必須是唯一且小寫)
az acr create --resource-group MyResourceGroup --name myuniqueacr --sku Basic

# 4. 登入 ACR 
az acr login --name myuniqueacr
</code></pre>



<h3 class="wp-block-heading"><strong>步驟 B：標記並推送 Image</strong></h3>



<p class="wp-block-paragraph">你需要將本地的 Image 給予一個符合 ACR 規範的「標籤 (Tag)」。</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 1. 取得 ACR 的登入伺服器名稱 (通常是 myuniqueacr.azurecr.io)
$ACR_SERVER = "myuniqueacr.azurecr.io"

# 2. 標記本地 Image (假設你在 compose 產生的 image 叫 my-app_backend)
docker tag my-app_backend $ACR_SERVER/fastapi-backend:v1
docker tag my-app_frontend $ACR_SERVER/react-frontend:v1

# 3. 推送到雲端
docker push $ACR_SERVER/fastapi-backend:v1
docker push $ACR_SERVER/react-frontend:v1
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">3. Azure 端的資料庫銜接</h2>



<p class="wp-block-paragraph">當你推送到 Azure 後，你的 FastAPI 需要改連 <strong>Azure SQL Database</strong>。</p>



<ol start="1" class="wp-block-list">
<li><strong>建立 Azure SQL</strong>：在 Azure Portal 建立一個 SQL Database。</li>



<li><strong>防火牆設定</strong>：在 Azure SQL 的防火牆設定中，勾選 <strong>「允許 Azure 服務和資源存取此伺服器」</strong>。</li>



<li><strong>環境變數</strong>：在部署 Container（如使用 Azure Container Apps）時，將 <code>DB_CONNECTION_STRING</code> 改為 Azure SQL 的連線字串。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">4. 懶人部署法：Azure Container Apps (推薦)</h2>



<p class="wp-block-paragraph">如果你不想管複雜的 Kubernetes，可以使用 <strong>Azure Container Apps</strong>，它支援多容器應用。</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 建立 Container App 環境並直接從 ACR 部署 (簡化指令範例)
az containerapp up --name my-fastapi-app `
  --resource-group MyResourceGroup `
  --image $ACR_SERVER/fastapi-backend:v1 `
  --environment 'MyEnv'
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><strong>常見踩雷提醒：</strong></p>



<ul class="wp-block-list">
<li><strong>晶片架構</strong>：如果你使用的是 Mac M1/M2 開發但推送到 Azure (x86_64)，打包時要加上 <code>--platform linux/amd64</code>。<strong>在 Windows 上開發則通常沒這個問題。</strong></li>



<li><strong>MSSQL 密碼</strong>：Azure SQL 的密碼要求較嚴格，本地測試時建議就先用複雜密碼。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">錯誤訊息 <code>failed to read dockerfile: open Dockerfile: no such file or directory</code> 非常直觀：<strong>Docker 在你指定的路徑下找不到 <code>Dockerfile</code></strong>。</p>



<p class="wp-block-paragraph">這通常是因為 <code>docker-compose.yml</code> 裡面的 <code>build</code> 路徑設定，與你實際資料夾結構不一致造成的。</p>



<h3 class="wp-block-heading">1. 檢查你的目錄結構</h3>



<p class="wp-block-paragraph">請確認你的資料夾長得像這樣（注意大小寫，Linux 容器對大小寫很敏感）：</p>



<p class="wp-block-paragraph">Plaintext</p>



<pre class="wp-block-code"><code>C:\max\work\policy-recommends\my-fast-app\
├── docker-compose.yml
├── backend/
│   └── Dockerfile      &lt;-- 確認這個檔案存在，且副檔名沒有被隱藏 (不是 Dockerfile.txt)
└── frontend/
    └── Dockerfile      &lt;-- 確認這個檔案存在</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">重新執行的正確姿勢</h3>



<p class="wp-block-paragraph">修正路徑或檔案後，建議先清理一下快取再執行：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 強制重新檢查 build context 並建立
docker-compose build --no-cache

# 啟動
docker-compose up
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">如何推送到 Azure ACR (完整指令流)</h3>



<p class="wp-block-paragraph">等你本地 <code>docker-compose up</code> 成功執行後，按照以下順序推送到 Azure：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 1. 登入 Azure (會跳出瀏覽器)
az login

# 2. 登入你的 ACR (假設名字叫 myacr123)
az acr login --name myacr123

# 3. 取得本地剛剛 build 好的 Image ID 或名稱
# 執行 docker images 查看剛剛產生的 image 名稱
docker images

# 4. 幫 Image 打上標籤 (Tag)
# 格式：docker tag &#91;本地名稱] &#91;ACR名稱].azurecr.io/&#91;映像檔名稱]:&#91;版本]
docker tag my-fast-app-backend myacr123.azurecr.io/fastapi-api:v1

# 5. 推送
docker push myacr123.azurecr.io/fastapi-api:v1</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">針對 <strong>React (Node.js)</strong> 前端，在開發環境與生產環境（Azure）的需求不同。為了讓你現在能在 Windows Local 成功執行 <code>docker-compose up</code>，我們採用<strong>開發模式</strong>的 Dockerfile。</p>



<p class="wp-block-paragraph">請在 <code>frontend/</code> 資料夾下建立一個名為 <code>Dockerfile</code> 的檔案（不含副檔名），內容如下：</p>



<p class="wp-block-paragraph">Dockerfile</p>



<pre class="wp-block-code"><code># 1. 使用 Node.js 官方映像檔 (建議版本 18 或 20)
FROM node:20

# 2. 設定容器內的工作目錄
WORKDIR /app

# 3. 先複製 package.json 與 package-lock.json (利用 Docker 快取機制)
COPY package*.json ./

# 4. 安裝相依套件
RUN npm install

# 5. 複製其餘的所有程式碼到容器內
COPY . .

# 6. React 預設開發埠號為 3000
EXPOSE 3000

# 7. 啟動 React 開發伺服器
# 使用 --host 0.0.0.0 是為了讓容器外的瀏覽器能存取
CMD &#91;"npm", "start"]
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 兩個關鍵注意事項 (Windows 平台)</h3>



<h4 class="wp-block-heading">1. <code>.dockerignore</code> 檔案</h4>



<p class="wp-block-paragraph">在 <code>frontend/</code> 資料夾內建立一個 <code>.dockerignore</code> 檔案。這能防止將本地巨大的 <code>node_modules</code> 複製進容器，導致 Build 速度極慢或產生錯誤。</p>



<p class="wp-block-paragraph"><strong>內容如下：</strong></p>



<pre class="wp-block-code"><code>node_modules
build
.git
*.md
</code></pre>



<h4 class="wp-block-heading">2. 解決 Docker 抓不到檔案變更 (Hot Reload)</h4>



<p class="wp-block-paragraph">在 Windows 上使用 Docker Desktop 時，有時 React 的熱更新（存檔後網頁自動重新整理）會失效。如果發生這種情況，請在 <code>frontend/</code> 資料夾建立一個 <code>.env</code> 檔案，加入以下內容：</p>



<pre class="wp-block-code"><code>CHOKIDAR_USEPOLLING=true
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">接下來的操作步驟：</h3>



<p class="wp-block-paragraph"><strong>檢查 <code>package.json</code></strong>：確保你的 <code>frontend/</code> 資料夾內確實有 <code>package.json</code><br>（通常是執行 <code>npx create-react-app .</code> 產生的）。</p>



<p class="wp-block-paragraph"><strong>執行 Build</strong>：</p>



<pre class="wp-block-code"><code><code>docker-compose build frontend</code></code></pre>



<p class="wp-block-paragraph"><strong>啟動服務</strong>：</p>



<p class="wp-block-paragraph"><code>docker-compose up frontend</code></p>



<p class="wp-block-paragraph"><strong>如果你的 React 是用 Vite 建立的（現在比較流行），<code>CMD</code> 可能需要改成 <code>npm run dev -- --host</code>，你需要我確認你的 <code>package.json</code> 腳本內容嗎？</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">既然你的資料夾目前是空白的，我們來快速建立一個**「全端連通」**的範例。這個範例會讓 React 前端呼叫 FastAPI 後端，而後端會從 MSSQL 資料庫讀取版本資訊。</p>



<p class="wp-block-paragraph">請依照以下檔案內容填入你的資料夾：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1. 後端 (backend/main.py)</h3>



<p class="wp-block-paragraph">這是一個簡單的 API，它會嘗試連線資料庫並回傳 SQL Server 的版本。</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
import pyodbc
import os

app = FastAPI()

# 允許前端跨域存取 (CORS)
app.add_middleware(
    CORSMiddleware,
    allow_origins=&#91;"*"],
    allow_methods=&#91;"*"],
    allow_headers=&#91;"*"],
)

@app.get("/api/status")
def get_status():
    conn_str = os.getenv("DB_CONNECTION_STRING")
    try:
        # 建立連線
        conn = pyodbc.connect(conn_str, timeout=5)
        cursor = conn.cursor()
        cursor.execute("SELECT @@VERSION")
        db_version = cursor.fetchone()&#91;0]
        conn.close()
        return {
            "status": "Connected",
            "database": "Microsoft SQL Server",
            "version": db_version
        }
    except Exception as e:
        return {
            "status": "Error",
            "message": str(e)
        }

@app.get("/")
def read_root():
    return {"message": "FastAPI is running!"}
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. 前端 (frontend/src/App.js)</h3>



<p class="wp-block-paragraph">我們簡單用 <code>fetch</code> 來抓取後端的 API 資料。</p>



<p class="wp-block-paragraph">JavaScript</p>



<pre class="wp-block-code"><code>import React, { useEffect, useState } from 'react';

function App() {
  const &#91;data, setData] = useState({ status: "Loading...", version: "" });

  useEffect(() =&gt; {
    // 指向後端容器的對外 Port
    fetch('http://localhost:8000/api/status')
      .then(response =&gt; response.json())
      .then(json =&gt; setData(json))
      .catch(err =&gt; setData({ status: "Error", message: "Cannot connect to API" }));
  }, &#91;]);

  return (
    &lt;div style={{ textAlign: 'center', marginTop: '50px', fontFamily: 'sans-serif' }}&gt;
      &lt;h1&gt;React + FastAPI + MSSQL&lt;/h1&gt;
      &lt;div style={{ border: '1px solid #ccc', padding: '20px', display: 'inline-block' }}&gt;
        &lt;p&gt;&lt;strong&gt;DB Status:&lt;/strong&gt; {data.status}&lt;/p&gt;
        {data.version &amp;&amp; &lt;p&gt;&lt;strong&gt;DB Version:&lt;/strong&gt; {data.version}&lt;/p&gt;}
        {data.message &amp;&amp; &lt;p style={{ color: 'red' }}&gt;{data.message}&lt;/p&gt;}
      &lt;/div&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 前端必要設定 (frontend/package.json)</h3>



<p class="wp-block-paragraph">如果你還沒建立 React 專案，最簡單的方式是在 <code>frontend/</code> 資料夾下建立這個檔案：</p>



<p class="wp-block-paragraph">JSON</p>



<pre class="wp-block-code"><code>{
  "name": "frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build"
  },
  "eslintConfig": {
    "extends": &#91;
      "react-app"
    ]
  },
  "browserslist": {
    "production": &#91;"&gt;0.2%", "not dead", "not op_mini all"],
    "development": &#91;"last 1 chrome version", "last 1 firefox version", "last 1 safari version"]
  }
}
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">4. 啟動與驗證</h3>



<p class="wp-block-paragraph">現在再次執行：</p>



<pre class="wp-block-code"><code>docker-compose up --build
</code></pre>



<p class="wp-block-paragraph"><strong>驗證方式：</strong></p>



<ol start="1" class="wp-block-list">
<li><strong>等待 MSSQL 啟動</strong>：SQL Server 啟動較慢（約 20-30 秒），剛開始 API 可能會報錯，請重新整理頁面。</li>



<li><strong>開啟瀏覽器</strong>：輸入 <code>http://localhost:3000</code>。</li>



<li><strong>預期結果</strong>：你應該會看到網頁顯示 <strong>&#8220;DB Status: Connected&#8221;</strong> 以及 <strong>&#8220;Microsoft SQL Azure&#8230;&#8221;</strong> 的版本文字。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><strong>小提示：</strong></p>



<p class="wp-block-paragraph">由於我們是在 Docker 外部的瀏覽器看網頁，前端代碼 <code>fetch('http://localhost:8000/api/status')</code> 使用 <code>localhost</code> 是正確的。</p>



<p class="wp-block-paragraph">但如果是後端要找資料庫，<code>docker-compose.yml</code> 裡的連線字串必須寫 <code>SERVER=db</code>。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">這個錯誤 <code>"/requirements.txt": not found</code> 代表 Docker 在執行 <code>COPY requirements.txt .</code> 這行指令時，在你的 <strong><code>backend/</code> 資料夾</strong> 裡找不到這個檔案。</p>



<p class="wp-block-paragraph">在 Docker 的世界裡，<code>COPY</code> 指令尋找檔案的起點是 <code>docker-compose.yml</code> 中定義的 <code>context</code> 路徑。</p>



<h3 class="wp-block-heading">1. 解決方法：建立必要的相依檔案</h3>



<p class="wp-block-paragraph">請確保你的目錄結構與檔案名稱完全一致（注意大小寫）：</p>



<p class="wp-block-paragraph">在 <code>\my-fast-app\backend\</code> 資料夾下，建立一個名為 <strong><code>requirements.txt</code></strong> 的檔案，內容如下：</p>



<pre class="wp-block-code"><code>fastapi
uvicorn
pyodbc
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. 完整檢查清單</h3>



<p class="wp-block-paragraph">如果還是報錯，請檢查以下三點：</p>



<p class="wp-block-paragraph"><strong>檔案位置</strong>：<code>requirements.txt</code> 必須與 <code>backend/Dockerfile</code> 放在同一個資料夾內。</p>



<p class="wp-block-paragraph"><strong>副檔名陷阱</strong>：在 Windows 中，檔案可能會被命名為 <code>requirements.txt.txt</code>（如果你開啟了隱藏已知檔案類型的擴充名）。請確認檔名只有一個 <code>.txt</code>。</p>



<p class="wp-block-paragraph"><strong>Docker Compose 路徑</strong>：確認你的 <code>docker-compose.yml</code> 是這樣寫的：</p>



<pre class="wp-block-code"><code>backend:
 build: ./backend 
# 這代表 Docker 會去 backend 資料夾找 Dockerfile 和 requirements.txt</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 下一步：再次執行</h3>



<p class="wp-block-paragraph">修正檔案後，請執行以下指令（加上 <code>--no-cache</code> 確保它不會抓到舊的錯誤暫存）：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code>docker-compose build --no-cache
docker-compose up
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 給你的開發小秘訣</h3>



<p class="wp-block-paragraph">由於你之後要推送到 <strong>Azure</strong>，現在建立這些檔案時，可以順便檢查你的 <strong><code>frontend/</code></strong> 資料夾是否也已經準備好了：</p>



<ol start="1" class="wp-block-list">
<li><code>frontend/package.json</code> (前一個回答提供的內容)</li>



<li><code>frontend/src/App.js</code> (前一個回答提供的內容)</li>



<li><code>frontend/Dockerfile</code></li>
</ol>



<p class="wp-block-paragraph"><strong>如果 <code>backend</code> 編譯成功了，但換成 <code>frontend</code> 報錯 <code>package.json not found</code>，處理方式是一樣的：確保檔案真的躺在 <code>frontend/</code> 資料夾裡。</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">這是一個完整的測試指南，幫助你驗證 <strong>React (前端)</strong>、<strong>FastAPI (後端)</strong> 與 <strong>SQL Server (資料庫)</strong> 這三個組件是否已經成功串聯。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">1. 檢查容器運行狀態</h2>



<p class="wp-block-paragraph">首先，確保所有服務都已啟動。在 Windows PowerShell 執行：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code>docker ps
</code></pre>



<p class="wp-block-paragraph"><strong>檢查重點：</strong> 你應該會看到三個容器，且狀態（STATUS）顯示為 <code>Up</code>。如果看到 <code>Exited</code>，代表該服務啟動失敗，請執行 <code>docker-compose logs [服務名稱]</code> 查看報錯。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">2. 測試後端 API (FastAPI)</h2>



<p class="wp-block-paragraph">FastAPI 自帶強大的 <strong>Swagger UI</strong>，這是測試資料庫連線最快的方法。</p>



<ul class="wp-block-list">
<li><strong>網址：</strong> 打開瀏覽器輸入 <code>http://localhost:8000/docs</code></li>



<li><strong>操作：</strong> 1. 找到我們寫的 <code>GET /api/status</code> 端點。2. 點擊 <strong>&#8220;Try it out&#8221;</strong>，然後點擊 <strong>&#8220;Execute&#8221;</strong>。</li>



<li><strong>預期結果：</strong>
<ul class="wp-block-list">
<li><strong>Code 200 (Success)</strong>：你會看到包含 SQL Server 版本資訊的 JSON 字串。這代表 <code>FastAPI -&gt; MSSQL</code> 通訊正常。</li>



<li><strong>Error 500</strong>：通常是資料庫密碼錯誤或 Driver 設定問題。</li>



<li><strong>連線逾時</strong>：SQL Server 啟動較慢（通常需要 30 秒），請稍等片刻再試。</li>
</ul>
</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">3. 測試前端網頁 (React)</h2>



<p class="wp-block-paragraph">現在測試前端是否能成功抓到後端的資料。</p>



<ul class="wp-block-list">
<li><strong>網址：</strong> 打開瀏覽器輸入 <code>http://localhost:3000</code></li>



<li><strong>操作：</strong> 觀察網頁顯示的內容。</li>



<li><strong>預期結果：</strong>
<ul class="wp-block-list">
<li>畫面顯示 <strong>&#8220;DB Status: Connected&#8221;</strong>：恭喜！整個全端流程 <code>瀏覽器 -&gt; React -&gt; FastAPI -&gt; MSSQL</code> 全部打通。</li>



<li>畫面卡在 <strong>&#8220;Loading&#8230;&#8221;</strong> 或顯示 <strong>&#8220;Error&#8221;</strong>：
<ol start="1" class="wp-block-list">
<li>按下 <code>F12</code> 打開開發者工具。</li>



<li>切換到 <strong>Console (主控台)</strong>。如果是 <strong>CORS</strong> 報錯，代表 FastAPI 的中間件設定有誤；如果是 <strong>404</strong>，請確認 API 路徑是否正確。</li>
</ol>
</li>
</ul>
</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">4. 測試資料庫 (SQL Server)</h2>



<p class="wp-block-paragraph">如果你懷疑資料庫根本沒動，可以獨立測試它。</p>



<p class="wp-block-paragraph"><strong>查看日誌：</strong></p>



<pre class="wp-block-code"><code>docker logs mssql_db </code></pre>



<p class="wp-block-paragraph">尋找這一行：<code>SQL Server is now ready for client connections.</code>。</p>



<p class="wp-block-paragraph"><strong>外部連線：</strong>如果你電腦有安裝 <strong>SSMS (SQL Server Management Studio)</strong> 或 <strong>Azure Data Studio</strong>，可以直接連線：</p>



<p class="wp-block-paragraph"><strong>伺服器：</strong> <code>127.0.0.1,1433</code></p>



<p class="wp-block-paragraph"><strong>帳號：</strong> <code>sa</code></p>



<p class="wp-block-paragraph"><strong>密碼：</strong> <code>YourStrongPassword123!</code> (你在 compose 檔設定的那組)</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">5. 總結連線邏輯 (除錯關鍵)</h2>



<p class="wp-block-paragraph">在 Docker 環境下，連線位址會因為「誰發起請求」而不同：</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><td><strong>連線路徑</strong></td><td><strong>使用地址</strong></td><td><strong>原因</strong></td></tr></thead><tbody><tr><td><strong>你 -&gt; 前端</strong></td><td><code>localhost:3000</code></td><td>你在 Docker 網路外部。</td></tr><tr><td><strong>你 -&gt; 後端</strong></td><td><code>localhost:8000</code></td><td>你在 Docker 網路外部。</td></tr><tr><td><strong>前端 (瀏覽器) -&gt; 後端</strong></td><td><code>localhost:8000</code></td><td><strong>重要：</strong> React 代碼是在「你的瀏覽器」執行，所以對它來說後端就在 <code>localhost</code>。</td></tr><tr><td><strong>後端 -&gt; 資料庫</strong></td><td><code>db:1433</code></td><td><strong>重要：</strong> Python 代碼是在「容器內部」執行，必須使用 Compose 服務名稱 <code>db</code>。</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">常見問題排除</h3>



<ul class="wp-block-list">
<li><strong>修改代碼後沒反應？</strong> 確保 <code>docker-compose.yml</code> 有設定 <code>volumes</code> 掛載，這樣你在 Windows 修改程式碼時，容器內才會同步更新。</li>



<li><strong>無法連線資料庫？</strong> 請檢查 <code>main.py</code> 裡的連線字串是否包含 <code>TrustServerCertificate=yes</code>，因為自架的 Docker SQL Server 通常沒有正式憑證。</li>
</ul>



<p class="wp-block-paragraph"><strong>如果你現在測試成功了，下一步我們是否要來練習如何將這兩個 Image (Frontend/Backend) 推送到 Azure ACR 並準備部署？</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">port 3000 改 8080</p>



<p class="wp-block-paragraph">沒問題！在 Windows 上，<code>3000</code> 埠確實常被一些系統服務（如某些版本的 Windows 服務或開發工具）佔用。將前端改為 <code>8080</code> 是一個好選擇。</p>



<p class="wp-block-paragraph">請依照以下三個步驟修改設定：</p>



<h3 class="wp-block-heading">1. 修改 <code>docker-compose.yml</code></h3>



<p class="wp-block-paragraph">將 <code>frontend</code> 服務的 <code>ports</code> 對應改為 <code>8080:3000</code>。這代表：<strong>瀏覽器輸入 8080，會轉發到容器內的 3000</strong>。</p>



<p class="wp-block-paragraph">YAML</p>



<pre class="wp-block-code"><code>  frontend:
    build: ./frontend
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - "8080:3000"  # 修改這裡：外部 8080 -&gt; 內部 3000
    stdin_open: true
    environment:
      - WDS_SOCKET_PORT=8080 # 確保 React 的熱更新 (Hot Reload) 也能走 8080
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. 修改前端程式碼 (<code>frontend/src/App.js</code>)</h3>



<p class="wp-block-paragraph">如果你的 React 程式碼中原本有寫死後端網址，請確認它依然指向 <code>8000</code>（後端沒改）。</p>



<p class="wp-block-paragraph">但為了確保瀏覽器不會因為埠號變更而產生安全性阻攔，請檢查 <code>App.js</code> 中的 <code>fetch</code>：</p>



<p class="wp-block-paragraph">JavaScript</p>



<pre class="wp-block-code"><code>// 確保後端依然是 8000
fetch('http://localhost:8000/api/status')
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 重新啟動服務</h3>



<p class="wp-block-paragraph">由於你修改了 <code>docker-compose.yml</code> 的結構，建議先停止舊的容器再重新啟動：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 停止並移除舊的容器
docker-compose down

# 重新啟動
docker-compose up
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">如何驗證？</h3>



<ol start="1" class="wp-block-list">
<li><strong>前端網頁</strong>：打開瀏覽器訪問 <code>http://localhost:8080</code>。</li>



<li><strong>後端 API</strong>：訪問 <code>http://localhost:8000/docs</code>。</li>
</ol>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> 專業小撇步：如果連 8080 也被佔用？</h3>



<p class="wp-block-paragraph">如果在啟動時依然看到 <code>bind: An attempt was made...</code>，這代表 <code>8080</code> 也有人在用。你可以執行這個指令來徹底「殺掉」佔用者：</p>



<p class="wp-block-paragraph">PowerShell</p>



<pre class="wp-block-code"><code># 尋找誰在用 8080 (在管理員模式的 PowerShell)
$proc = Get-NetTCPConnection -LocalPort 8080 -ErrorAction SilentlyContinue
if($proc) { Stop-Process -Id $proc.OwningProcess -Force }
</code></pre>



<p class="wp-block-paragraph"><strong>現在你可以試試看 <code>http://localhost:8080</code> 能不能看到 React 畫面了。如果畫面出來了但顯示 &#8220;Connection Error&#8221;，我們再來調整 FastAPI 的 CORS 設定！</strong></p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/windows-docker-compose-to-cloud/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Azure SQL 資料庫以 DTU &#8211; Basic 在那些情況下, 會不夠使用</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/azure-sql-dtu-basic/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/azure-sql-dtu-basic/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 18 Mar 2026 04:17:25 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=8012</guid>

					<description><![CDATA[「DTU &#8211; Basic」是一個非常...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">「DTU &#8211; Basic」是一個非常親民的入門選擇，每月僅約 <strong>$5 USD</strong>，對於初期開發或流量極小的系統非常友善。但校務建言系統（尤其是若涉及全校性活動或高峰期）在使用 Basic 級別時，可能會遇到一些明顯的瓶頸。</p>



<p class="wp-block-paragraph">以下是 <strong>DTU &#8211; Basic</strong> 在開發校務建言系統時，可能「不夠用」的幾種情況：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1. 併發連線數（Max Concurrent Workers）限制</h3>



<p class="wp-block-paragraph">這是 Basic 最致命的短板。</p>



<ul class="wp-block-list">
<li><strong>限制</strong>：Basic 僅支援 <strong>30 個併發工作執行緒</strong>。</li>



<li><strong>情境</strong>：如果校方發出一封全校 Email 邀請大家來建言，瞬間有 50 位同學點開網頁，資料庫會立刻排隊甚至報錯（Timeouts），導致系統看起來像掛掉了。</li>



<li><strong>建議</strong>：若預期會有瞬間高流量（如開會期間、校園新聞發布），Basic 可能無法負荷。</li>
</ul>



<h3 class="wp-block-heading">2. IOPS 與資料吞吐量效能</h3>



<ul class="wp-block-list">
<li><strong>限制</strong>：Basic 的 I/O 效能非常基礎。</li>



<li><strong>情境</strong>：雖然你將附件存在 Blob Storage（這是非常正確的作法！），但資料庫仍需處理建言內容的 <strong>全文檢索</strong> 或 <strong>複雜查詢</strong>（例如：管理員要篩選「過去一年、所有關於總務處、且狀態為未處理」的建言）。當資料筆數累積到幾千筆以上時，這種分群彙整（Group By）的查詢速度會變得很慢。</li>
</ul>



<h3 class="wp-block-heading">3. 資料庫大小上限</h3>



<ul class="wp-block-list">
<li><strong>限制</strong>：Basic 的硬碟空間上限通常只有 <strong>2 GB</strong>。</li>



<li><strong>情境</strong>：雖然附件在 Blob，但如果建言系統包含大量文字回覆、系統 Log、或多個關聯資料表，2 GB 可能在 1-2 年內就會用滿。</li>
</ul>



<h3 class="wp-block-heading">4. 缺乏進階功能</h3>



<ul class="wp-block-list">
<li><strong>限制</strong>：Basic 不支援 <strong>Point-in-Time Restore (PITR)</strong> 較長的天數（僅 7 天），且不支援異地備份。</li>



<li><strong>情境</strong>：如果有人不小心刪錯資料，你只能回推到過去 7 天內。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">針對「校務建言系統」的架構建議</h3>



<p class="wp-block-paragraph">既然你已經規劃將附件存放在 <strong>Blob Storage</strong>，這大大減輕了資料庫的壓力。以下是針對你的專案建議的部署策略：</p>



<h4 class="wp-block-heading">策略 A：初期省錢法 (開發階段)</h4>



<ul class="wp-block-list">
<li><strong>使用 Basic (5 DTU)</strong>：在開發與內部測試（10 人以內）時完全足夠。</li>



<li><strong>優點</strong>：成本極低，適合慢慢磨功能。</li>
</ul>



<h4 class="wp-block-heading">策略 B：上線初期/正式運行 (彈性調整)</h4>



<ul class="wp-block-list">
<li><strong>升級至 Standard (S0 或 S1)</strong>：
<ul class="wp-block-list">
<li><strong>S0 (10 DTU)</strong>：約 $15 USD/月，支援 60 個併發連線，容量可達 250GB。</li>



<li><strong>S1 (20 DTU)</strong>：約 $30 USD/月，效能明顯提升，適合應對一般校園流量。</li>
</ul>
</li>
</ul>



<h4 class="wp-block-heading">策略 C：極致省錢法 (使用 Serverless)</h4>



<p class="wp-block-paragraph">如果你不希望固定每個月付 $15~$30 USD，可以考慮從 DTU 模式切換到 <strong>vCore &#8211; Serverless</strong>：</p>



<ul class="wp-block-list">
<li><strong>運作邏輯</strong>：建言系統通常「晚上沒人用」或「假日時段冷門」。Serverless 可以在沒人點擊網頁時<strong>自動暫停</strong>。</li>



<li><strong>費用</strong>：可能比 Standard 更便宜，卻能提供比 Basic 更強大的瞬間處理能力。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">總結建議</h3>



<p class="wp-block-paragraph">你的校務系統**「平時」<strong>用 Basic 是可以的，但</strong>「宣傳推廣期」**一定會卡頓。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p class="wp-block-paragraph"><strong>我的實務建議</strong>：</p>



<p class="wp-block-paragraph">先用 <strong>Basic</strong> 開發。等系統準備正式發布給學生使用的那一天，手動在 Azure Portal 將級別調整至 <strong>Standard S1</strong>（調整過程資料庫不會停機）。等宣傳熱潮過了，再降回 <strong>Basic</strong> 或 <strong>S0</strong> 節省開支。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>
</blockquote>



<p class="wp-block-paragraph"><strong>如何在程式碼中（例如 C# 或 Python）實作「上傳附件到 Blob 並將 URL 寫回 SQL」的最佳實踐</strong></p>



<p class="wp-block-paragraph">這是一個非常標準且高效的雲端開發模式：<strong>SQL 存放結構化資料（後設資料），Blob Storage 存放非結構化的大檔案（圖片、PDF、文件）</strong>。</p>



<p class="wp-block-paragraph">這種做法能讓資料庫保持輕量，避免 <code>DTU - Basic</code> 的空間迅速被填滿。以下是實作「校務建言系統」附件處理的最佳實踐與範例：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">1. 資料庫資料表設計 (SQL Side)</h3>



<p class="wp-block-paragraph">不要在 SQL 中存檔案本身（Blob 類型），而是存檔案的 <strong>URL</strong> 和 <strong>唯一識別碼 (GUID)</strong>。</p>



<p class="wp-block-paragraph">SQL</p>



<pre class="wp-block-code"><code>CREATE TABLE Suggestions (
    Id INT PRIMARY KEY IDENTITY(1,1),
    StudentId NVARCHAR(50),
    Content NVARCHAR(MAX),
    CreatedAt DATETIME DEFAULT GETDATE()
);

CREATE TABLE Attachments (
    Id INT PRIMARY KEY IDENTITY(1,1),
    SuggestionId INT, -- 關聯到建言
    FileName NVARCHAR(255),
    BlobUrl NVARCHAR(2048), -- 儲存 Blob 的完整路徑
    BlobName GUID, -- 建議用 GUID 命名檔案以防重複
    FOREIGN KEY (SuggestionId) REFERENCES Suggestions(Id)
);
</code></pre>



<h3 class="wp-block-heading">2. 檔案上傳邏輯 (Workflow)</h3>



<p class="wp-block-paragraph">在開發時，建議遵循以下流程以確保效能：</p>



<ol start="1" class="wp-block-list">
<li><strong>前端上傳</strong>：使用者送出建言與檔案。</li>



<li><strong>後端處理</strong>：
<ul class="wp-block-list">
<li>產生一個唯一的檔名（例如 <code>guid.jpg</code>）。</li>



<li>使用 Azure Storage SDK 將檔案上傳至 <strong>Blob Storage</strong>。</li>



<li>取得該檔案的 <strong>永久連結 (URL)</strong> 或路徑。</li>
</ul>
</li>



<li><strong>寫入資料庫</strong>：將建言文字與該 <strong>URL</strong> 同步寫入 SQL Database。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. C# 程式碼範例 (使用 Azure.Storage.Blobs)</h3>



<p class="wp-block-paragraph">這是最常見的實作方式，簡潔且安全：</p>



<p class="wp-block-paragraph">C#</p>



<pre class="wp-block-code"><code>// 1. 初始化 Blob 客戶端
BlobServiceClient blobServiceClient = new BlobServiceClient(connectionString);
BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient("school-attachments");

// 2. 上傳檔案
string blobName = Guid.NewGuid().ToString() + Path.GetExtension(file.FileName);
BlobClient blobClient = containerClient.GetBlobClient(blobName);

using (var stream = file.OpenReadStream())
{
    await blobClient.UploadAsync(stream, true);
}

// 3. 取得 URL 並存入 SQL
string fileUrl = blobClient.Uri.ToString();
// 接下來使用 ADO.NET 或 Entity Framework 將 fileUrl 存入 Attachments 表
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">4. 進階安全性建議：SAS Token</h3>



<p class="wp-block-paragraph">由於校務系統可能涉及隱私（例如建言者不想讓別人看到他的附件），你不應該把 Blob Container 設為「公開讀取」。</p>



<ul class="wp-block-list">
<li><strong>做法</strong>：將 Container 設為 <strong>Private (私用)</strong>。</li>



<li><strong>讀取方式</strong>：當使用者要查看附件時，後端產生一個 <strong>暫時性的簽署 URL (Shared Access Signature, SAS)</strong>。</li>



<li><strong>優點</strong>：這個連結會在 10 分鐘或 1 小時後失效，即使連結外流，別人也無法下載。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">5. 為什麼這樣做對 Basic 級別最有利？</h3>



<ul class="wp-block-list">
<li><strong>節省 DTU 消耗</strong>：資料庫不需要處理大數據流（I/O），只需處理簡短的文字字串。</li>



<li><strong>節省空間</strong>：SQL Basic 的 2GB 限制可以撐非常久，因為幾萬筆 URL 頂多才幾十 MB。</li>



<li><strong>成本優勢</strong>：Blob Storage 每 GB 的價格（約 $0.02 USD）遠低於 SQL Database 空間。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph">使用 Python 實作「上傳檔案至 Blob 並將 URL 寫入 SQL」是非常直覺的。我們通常會使用 <code>azure-storage-blob</code> 套件處理檔案，並使用 <code>pyodbc</code> 或 <code>SQLAlchemy</code> 處理資料庫。</p>



<p class="wp-block-paragraph">以下是一個完整的邏輯範例：</p>



<h3 class="wp-block-heading">1. 安裝必要套件</h3>



<p class="wp-block-paragraph">在你的開發環境中執行：</p>



<p class="wp-block-paragraph">Bash</p>



<pre class="wp-block-code"><code>pip install azure-storage-blob pyodbc
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. Python 實作程式碼</h3>



<p class="wp-block-paragraph">這個範例展示了如何接收一個檔案、上傳到 Azure，最後把資料存進 SQL Database。</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>import os
import uuid
from azure.storage.blob import BlobServiceClient
import pyodbc

# --- 1. 設定連線資訊 ---
# Azure Blob Storage 連線字串
AZURE_STORAGE_CONNECTION_STRING = "DefaultEndpointsProtocol=https;AccountName=...;AccountKey=...;"
CONTAINER_NAME = "school-attachments"

# Azure SQL 連線資訊 (注意：Basic 級別請確保防火牆已開放 IP)
SQL_CONNECTION_STRING = (
    "Driver={ODBC Driver 18 for SQL Server};"
    "Server=tcp:your-server.database.windows.net,1433;"
    "Database=your-db;"
    "Uid=your-username;Pwd=your-password;"
    "Encrypt=yes;TrustServerCertificate=no;Connection Timeout=30;"
)

def upload_suggestion_with_attachment(student_id, content, file_path):
    try:
        # --- 2. 上傳檔案到 Blob Storage ---
        blob_service_client = BlobServiceClient.from_connection_string(AZURE_STORAGE_CONNECTION_STRING)
        container_client = blob_service_client.get_container_client(CONTAINER_NAME)
        
        # 產生唯一檔名避免重複 (例如: 550e8400-e29b.pdf)
        file_extension = os.path.splitext(file_path)&#91;1]
        unique_blob_name = f"{uuid.uuid4()}{file_extension}"
        
        blob_client = container_client.get_blob_client(unique_blob_name)
        
        print(f"正在上傳檔案: {unique_blob_name}...")
        with open(file_path, "rb") as data:
            blob_client.upload_blob(data)
        
        # 取得上傳後的 URL
        blob_url = blob_client.url
        print(f"上傳成功！URL: {blob_url}")

        # --- 3. 將資料寫入 SQL Database ---
        conn = pyodbc.connect(SQL_CONNECTION_STRING)
        cursor = conn.cursor()

        # 插入建言並取得 ID (SQL Server 語法)
        cursor.execute(
            "INSERT INTO Suggestions (StudentId, Content) OUTPUT INSERTED.Id VALUES (?, ?)",
            (student_id, content)
        )
        suggestion_id = cursor.fetchone()&#91;0]

        # 插入附件資訊
        cursor.execute(
            "INSERT INTO Attachments (SuggestionId, FileName, BlobUrl, BlobName) VALUES (?, ?, ?, ?)",
            (suggestion_id, os.path.basename(file_path), blob_url, unique_blob_name)
        )

        conn.commit()
        print("資料庫寫入完成！")

    except Exception as ex:
        print(f"發生錯誤: {ex}")
    finally:
        if 'conn' in locals():
            conn.close()

# 測試執行
upload_suggestion_with_attachment("S123456", "飲水機過濾網該換了", "report.pdf")
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 進階架構優化（省錢與安全）</h3>



<p class="wp-block-paragraph">在實際開發校務系統時，還有兩點建議：</p>



<h4 class="wp-block-heading"><strong>A. 非同步上傳 (Async)</strong></h4>



<p class="wp-block-paragraph">如果你的系統是使用 <strong>FastAPI</strong> 或 <strong>Django</strong>，建議使用 <code>azure-storage-blob</code> 的非同步版本 (<code>BlobServiceClient</code> from <code>azure.storage.blob.aio</code>)。這樣當多個學生同時上傳大檔案時，不會卡住整個伺服器的執行緒。</p>



<h4 class="wp-block-heading"><strong>B. 使用受控識別 (Managed Identity)</strong> —— <strong>強烈建議</strong></h4>



<p class="wp-block-paragraph">與其在程式碼中寫死資料庫密碼（Uid/Pwd），你可以啟用 Web App 的 <strong>Managed Identity</strong>。</p>



<ul class="wp-block-list">
<li><strong>好處</strong>：不需要在設定檔存密碼。</li>



<li><strong>安全性</strong>：即使程式碼外流，別人也拿不到資料庫權限。</li>



<li><strong>適用</strong>：這在跨資源群組存取時非常方便且安全。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">4. 針對 DTU &#8211; Basic 的特別提醒</h3>



<p class="wp-block-paragraph">在使用 Python 連線時，請務必在 <code>pyodbc</code> 的連線字串中加入 <strong><code>Connection Timeout=30</code></strong>。</p>



<p class="wp-block-paragraph">因為 Basic 級別的資源較少，有時候冷啟動或排隊會稍微慢一點，適度增加超時時間可以減少連線失敗的機率。</p>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/azure-sql-dtu-basic/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Python MySQL to MSSQL 資料匯出</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/python-mysql-to-mssql-export/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/python-mysql-to-mssql-export/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 16 Mar 2026 01:58:19 +0000</pubDate>
				<category><![CDATA[Python筆記]]></category>
		<category><![CDATA[mssql]]></category>
		<category><![CDATA[mysql]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7960</guid>

					<description><![CDATA[mysql 使用 mysqldump 產生的 ....]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">mysql 使用 mysqldump 產生的 .sql 檔案, 要匯入到 mssql 之中, 由於匯出的資料量太大, 直接轉換 mysql sql command 為  mssql sql command, 如果使用 SSMS(SQL Server Management Studio) 會遇到記憶體不足的問題.</p>



<p class="wp-block-paragraph">透過 sqlcmd 匯入資料庫 (不經過 SSMS):</p>



<pre class="wp-block-code"><code>sqlcmd -S localhost -d %db_name% -E -i %output_file%</code></pre>



<p class="wp-block-paragraph">這個也會遇到很多奇奇怪怪的問題, 明明在 SSMS 執行正常的 table, 在 sqlcmd 會遇到重覆插入 PK 值.</p>



<p class="wp-block-paragraph">最佳解法是透過下列的python script 進行 export, 這是整合了「逐行讀取」、「自動切分 1000 筆」、「停用約束」以及修正後語法的完整版本：</p>



<pre class="wp-block-code"><code>import re
import pyodbc
import argparse
import sys

def parse_value(val_str):
    """Parse a single SQL value string and return a Python value."""
    val_str = val_str.strip()
    if val_str.upper() == 'NULL':
        return None
    if (val_str.startswith("'") and val_str.endswith("'")):
        # Remove surrounding quotes
        inner = val_str&#91;1:-1]
        # Unescape MySQL-style escape sequences
        result = &#91;]
        i = 0
        while i &lt; len(inner):
            if inner&#91;i] == '\\' and i + 1 &lt; len(inner):
                next_char = inner&#91;i + 1]
                if next_char == "'":
                    result.append("'")
                elif next_char == "\\":
                    result.append("\\")
                elif next_char == "n":
                    result.append("\n")
                elif next_char == "r":
                    result.append("\r")
                elif next_char == "t":
                    result.append("\t")
                elif next_char == "0":
                    result.append("\0")
                elif next_char == "%":
                    result.append("%")
                elif next_char == "_":
                    result.append("_")
                else:
                    result.append(next_char)
                i += 2
            elif inner&#91;i] == "'" and i + 1 &lt; len(inner) and inner&#91;i + 1] == "'":
                # SQL-style escaped quote
                result.append("'")
                i += 2
            else:
                result.append(inner&#91;i])
                i += 1
        return "".join(result)
    # Try numeric
    try:
        if '.' in val_str:
            return float(val_str)
        return int(val_str)
    except ValueError:
        return val_str

def split_row_values(row_str):
    """Split a single row '(val1, val2, ...)' into individual values."""
    # Remove outer parentheses
    inner = row_str.strip()
    if inner.startswith('('):
        inner = inner&#91;1:]
    if inner.endswith(')'):
        inner = inner&#91;:-1]
    
    values = &#91;]
    current = &#91;]
    in_string = False
    i = 0
    
    while i &lt; len(inner):
        char = inner&#91;i]
        
        if in_string:
            if char == '\\' and i + 1 &lt; len(inner):
                current.append(char)
                current.append(inner&#91;i + 1])
                i += 2
                continue
            elif char == "'":
                # Check for escaped quote ''
                if i + 1 &lt; len(inner) and inner&#91;i + 1] == "'":
                    current.append(char)
                    current.append(inner&#91;i + 1])
                    i += 2
                    continue
                else:
                    in_string = False
                    current.append(char)
            else:
                current.append(char)
        else:
            if char == "'":
                in_string = True
                current.append(char)
            elif char == ',':
                values.append("".join(current).strip())
                current = &#91;]
            else:
                current.append(char)
        i += 1
    
    if current:
        values.append("".join(current).strip())
    
    return values

def split_values_robust(values_str):
    """Split VALUES (...),(...),... into individual row strings."""
    rows = &#91;]
    buffer = &#91;]
    paren_depth = 0
    in_string = False
    i = 0
    length = len(values_str)
    
    while i &lt; length:
        char = values_str&#91;i]
        
        if in_string:
            if char == "\\" and i + 1 &lt; length:
                buffer.append(char)
                buffer.append(values_str&#91;i + 1])
                i += 2
                continue
            elif char == "'":
                # Check for '' escape
                if i + 1 &lt; length and values_str&#91;i + 1] == "'":
                    buffer.append(char)
                    buffer.append(values_str&#91;i + 1])
                    i += 2
                    continue
                in_string = False
                buffer.append(char)
            else:
                buffer.append(char)
        else:
            if char == "'":
                in_string = True
                buffer.append(char)
            elif char == "(":
                paren_depth += 1
                buffer.append(char)
            elif char == ")":
                paren_depth -= 1
                buffer.append(char)
                if paren_depth == 0:
                    rows.append("".join(buffer).strip())
                    buffer = &#91;]
            elif char == "," and paren_depth == 0:
                pass
            else:
                if paren_depth > 0:
                    buffer.append(char)
        i += 1
    return rows

def run_import(input_file):
    # 資料庫連線配置
    conn_config = {
        "DRIVER": "{ODBC Driver 18 for SQL Server}",
        "SERVER": "127.0.0.1",
        "DATABASE": "你的資料庫名稱",
        "UID": "帳號",
        "PWD": "密碼",
        "Encrypt": "yes",
        "TrustServerCertificate": "yes"
    }

    conn_str = ";".join(&#91;f"{k}={v}" for k, v in conn_config.items()])

    try:
        conn = pyodbc.connect(conn_str, autocommit=False)
        cursor = conn.cursor()
        print(f"檔案讀取中: {input_file}")
    except Exception as e:
        print(f"連線失敗: {e}")
        return

    try:
        with open(input_file, 'r', encoding='utf-8') as f:
            sql_content = f.read()
    except Exception as e:
        print(f"讀檔失敗: {e}")
        return

    # Handle INSERT INTO with parameterized queries
    insert_pattern = re.compile(
        r"INSERT INTO\s+`?(&#91;\w_]+)`?\s*(?:\((.*?)\))?\s*VALUES\s*(.+?);",
        re.S | re.I
    )

    truncated_tables = set()  # Track tables already truncated
    table_stats = {}  # Track cumulative success/error counts per table

    for match in insert_pattern.finditer(sql_content):
        table_name = match.group(1)
        cols_raw = match.group(2) if match.group(2) else ""
        raw_values = match.group(3)

        # Build column clause
        if cols_raw:
            col_names = &#91;c.strip().replace('`', '') for c in cols_raw.split(',')]
            columns_clause = "(&#91;" + "],&#91;".join(col_names) + "])"
        else:
            # No column list in SQL dump — fetch from MSSQL metadata
            # This is required when IDENTITY_INSERT is ON
            try:
                cursor.execute("""
                    SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
                    WHERE TABLE_NAME = ?
                    ORDER BY ORDINAL_POSITION
                """, table_name)
                db_cols = &#91;row&#91;0] for row in cursor.fetchall()]
                if db_cols:
                    columns_clause = "(&#91;" + "],&#91;".join(db_cols) + "])"
                else:
                    columns_clause = ""
            except:
                columns_clause = ""

        print(f"正在處理: {table_name}")

        # Truncate table before inserting (only once per table)
        if table_name not in truncated_tables:
            try:
                cursor.execute(f"TRUNCATE TABLE &#91;{table_name}]")
                conn.commit()
                print(f"  已清空資料表: {table_name}")
            except Exception as e:
                conn.rollback()
                # TRUNCATE fails if there are foreign key constraints, fallback to DELETE
                try:
                    cursor.execute(f"DELETE FROM &#91;{table_name}]")
                    conn.commit()
                    print(f"  已清空資料表 (DELETE): {table_name}")
                except Exception as e2:
                    conn.rollback()
                    print(f"  清空資料表失敗: {table_name}: {e2}")
            truncated_tables.add(table_name)

        all_rows = split_values_robust(raw_values)
        print(f"  共 {len(all_rows)} 筆資料")

        success_count = 0
        error_count = 0

        # Check if table has IDENTITY column, if so enable IDENTITY_INSERT
        has_identity = False
        try:
            cursor.execute(f"""
                SELECT COUNT(*) FROM sys.identity_columns 
                WHERE OBJECT_NAME(object_id) = ?
            """, table_name)
            row = cursor.fetchone()
            if row and row&#91;0] > 0:
                has_identity = True
        except:
            pass

        if has_identity:
            try:
                cursor.execute(f"SET IDENTITY_INSERT &#91;{table_name}] ON")
            except Exception as e:
                print(f"  無法開啟 IDENTITY_INSERT: {e}")

        for idx, row_str in enumerate(all_rows):
            raw_vals = split_row_values(row_str)
            parsed_vals = &#91;parse_value(v) for v in raw_vals]
            
            placeholders = ",".join(&#91;"?" for _ in parsed_vals])
            sql = f"INSERT INTO &#91;{table_name}] {columns_clause} VALUES ({placeholders})"
            
            try:
                cursor.execute(sql, parsed_vals)
                conn.commit()
                success_count += 1
            except Exception as e:
                conn.rollback()
                error_count += 1
                if error_count &lt;= 3:
                    print(f"  跳過錯誤列 {idx}: {e}")
                    # Print the values for debugging
                    preview_vals = &#91;str(v)&#91;:50] if v is not None else 'NULL' for v in parsed_vals]
                    print(f"  值預覽: {preview_vals}")
                elif error_count == 4:
                    print(f"  (後續錯誤將不再顯示...)")

        if has_identity:
            try:
                cursor.execute(f"SET IDENTITY_INSERT &#91;{table_name}] OFF")
            except:
                pass

        # Accumulate stats per table
        if table_name not in table_stats:
            table_stats&#91;table_name] = {"success": 0, "error": 0}
        table_stats&#91;table_name]&#91;"success"] += success_count
        table_stats&#91;table_name]&#91;"error"] += error_count
        print(f"  本批: 成功 {success_count} 筆, 失敗 {error_count} 筆")

    # Print summary for tables with multiple INSERT batches
    print("\n=== 匯入摘要 ===")
    for tbl, stats in table_stats.items():
        print(f"  {tbl}: 成功 {stats&#91;'success']} 筆, 失敗 {stats&#91;'error']} 筆")

    cursor.close()
    conn.close()
    print("全部完成。")

if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="MariaDB To MSSQL Migration Tool")
    parser.add_argument("input", help="Path to the .sql dump file")
    args = parser.parse_args()
    
    run_import(args.input)</code></pre>



<p class="wp-block-paragraph">如果執行過程中遇到記憶體不足的問題，通常是因為 <code>split_values</code> 處理了單一極其巨大的 <code>INSERT</code> 字串。若發生此情況，請告訴我，我們可以改用生成器（Generator）模式來進一步優化字串解析。</p>



<p class="wp-block-paragraph">上面這個版本針對一般情況是可以使用，針對特定的資料庫欄位類型（例如 Blob 或特殊的日期格式）則需要再進一步的轉換處理。</p>



<p class="wp-block-paragraph">執行結果:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="528" height="133" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/mstsc_2026-03-16-09-47-1e.jpg?v=1773626286" alt="" class="wp-image-7961"/></figure>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/python-mysql-to-mssql-export/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Python MSSQL 資料插入範例</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/python-mssql-insert/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/python-mssql-insert/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 16 Mar 2026 01:43:06 +0000</pubDate>
				<category><![CDATA[Python筆記]]></category>
		<category><![CDATA[mssql]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7957</guid>

					<description><![CDATA[插入成功的執行結果: 要在 Python 中將資...]]></description>
										<content:encoded><![CDATA[
<p class="wp-block-paragraph">插入成功的執行結果:</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="518" height="130" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/mstsc_2026-03-16-09-14-1d.jpg?v=1773623811" alt="" class="wp-image-7958"/></figure>



<p class="wp-block-paragraph">要在 Python 中將資料寫入 MSSQL，最常見且穩定的組合是使用 pyodbc 套件配合微軟官方提供的 ODBC Driver。安裝檔下載:</p>



<p class="wp-block-paragraph">下載適用於 SQL Server 的 ODBC 驅動程式<br><a href="https://learn.microsoft.com/zh-tw/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver17">https://learn.microsoft.com/zh-tw/sql/connect/odbc/download-odbc-driver-for-sql-server?view=sql-server-ver17</a></p>



<h3 class="wp-block-heading">需安裝的組件</h3>



<p class="wp-block-paragraph">系統層級需要安裝 Microsoft ODBC Driver for SQL Server。這是微軟提供的驅動程式，讓作業系統能夠與 SQL Server 通訊。建議安裝最新的版本（如 Driver 18 或 17）。</p>



<p class="wp-block-paragraph">Python 環境則需要安裝 pyodbc 函式庫。你可以透過 pip install pyodbc 指令完成安裝。如果你的專案較具規模，通常也會搭配 sqlalchemy 配合 pandas 來處理大數據量，安裝指令為 pip install sqlalchemy pandas。</p>



<h3 class="wp-block-heading">實作範例</h3>



<p class="wp-block-paragraph">以下是使用 pyodbc 進行單筆資料插入的標準寫法：</p>



<p class="wp-block-paragraph">Python</p>



<pre class="wp-block-code"><code>import pyodbc

# 設定連線資訊
conn_config = {
    "DRIVER": "{ODBC Driver 18 for SQL Server}",
    "SERVER": "127.0.0.1", # 如果是本機也可以用 localhost 或 (local)
    "DATABASE": "你的資料庫名稱",
    "UID": "帳號",
    "PWD": "密碼",
    "Encrypt": "yes",
    "TrustServerCertificate": "yes"
}

conn_str = ";".join(&#91;f"{k}={v}" for k, v in conn_config.items()])

def sync_database():
    conn = None
    try:
        # 建立連線
        conn = pyodbc.connect(conn_str)
        cursor = conn.cursor()

        table_name = "UserLogs"
        
        # 1. 檢查資料表是否存在
        check_sql = "SELECT 1 FROM sys.objects WHERE object_id = OBJECT_ID(?) AND type = 'U'"
        cursor.execute(check_sql, table_name)
        
        if not cursor.fetchone():
            print(f"資料表 {table_name} 不存在，正在建立...")
            # 2. 建立資料表
            create_sql = f"""
            CREATE TABLE {table_name} (
                ID INT IDENTITY(1,1) PRIMARY KEY,
                UserName NVARCHAR(50),
                ActionTime DATETIME DEFAULT GETDATE(),
                Status NVARCHAR(20)
            )
            """
            cursor.execute(create_sql)
            conn.commit()
            print("資料表建立成功")

        # 3. 執行資料插入
        insert_sql = f"INSERT INTO {table_name} (UserName, Status) VALUES (?, ?)"
        cursor.execute(insert_sql, ("Max", "Active"))
        conn.commit()
        print("資料插入成功")

        # 4. 刪除資料表 (Drop Table)
        print(f"正在刪除資料表 {table_name}...")
        drop_sql = f"DROP TABLE {table_name}"
        cursor.execute(drop_sql)
        conn.commit()
        print("資料表已刪除")

    except pyodbc.Error as e:
        print(f"資料庫操作失敗: {e}")
        if conn:
            conn.rollback()
    finally:
        if conn:
            cursor.close()
            conn.close()

if __name__ == "__main__":
    sync_database()</code></pre>



<h3 class="wp-block-heading">注意事項</h3>



<p class="wp-block-paragraph">在連線字串中，如果你是連線到本機測試環境且沒有設定 SSL 憑證，務必加上 TrustServerCertificate=yes 參數，否則常會因為加密安全性檢查而導致連線失敗。另外，對於大量資料的寫入，建議使用 cursor.fast_executemany = True 或者是 pandas 的 to_sql 方法，這能顯著提升寫入效能。</p>



<p class="wp-block-paragraph">在 conn_config 中明確加上了 Encrypt: yes。這是因為 ODBC Driver 18 預設要求加密連線。配合 TrustServerCertificate: yes，可以跳過 SSL 憑證的有效性檢查，通常能解決無法開啟具名管道或連線逾時的問題。</p>



<p class="wp-block-paragraph">如果你的 SQL Server 是 Express 版本，SERVER 欄位可能需要寫成 127.0.0.1\SQLEXPRESS 這種格式，請根據你的實例名稱進行調整。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/python-mssql-insert/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>連線至伺服器 SQL Server SSL Certificate Expired</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/sql-server-ssl-certificate-expired/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/sql-server-ssl-certificate-expired/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Fri, 13 Mar 2026 03:16:44 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7952</guid>

					<description><![CDATA[雖然您的電腦可以連線到 SQL Server，但...]]></description>
										<content:encoded><![CDATA[
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="964" height="548" src="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/mstsc_2026-03-13-11-01-15.jpg?v=1773371672" alt="" class="wp-image-7953" srcset="https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/mstsc_2026-03-13-11-01-15.jpg?v=1773371672 964w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/mstsc_2026-03-13-11-01-15-600x341.jpg?v=1773371672 600w, https://stackoverflow.max-everyday.com/wp-content/uploads/2026/03/mstsc_2026-03-13-11-01-15-768x437.jpg?v=1773371672 768w" sizes="auto, (max-width: 964px) 100vw, 964px" /></figure>



<p class="wp-block-paragraph">雖然您的電腦可以連線到 SQL Server，但伺服器使用的 <strong>SSL/TLS 憑證已經過期</strong>。</p>



<p class="wp-block-paragraph">因為加密憑證失效，為了安全性，系統自動阻斷了登入程序。您可以透過以下幾種方式解決：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方案一：快速跳過驗證（用戶端暫時解決方案）</h3>



<p class="wp-block-paragraph">如果您需要立即連線且處於安全的內部網路，可以調整 SQL Server Management Studio (SSMS) 的設定，忽略憑證過期問題。</p>



<ol start="1" class="wp-block-list">
<li>在「連線至伺服器」視窗（輸入帳號密碼的地方），點擊右下角的 <strong>「選項 (O) >>」</strong>。</li>



<li>切換到 <strong>「連線屬性」</strong> 頁籤。</li>



<li>勾選 <strong>「信任伺服器憑證」(Trust server certificate)</strong>。</li>



<li>再次嘗試 <strong>「連線」</strong>。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方案二：檢查系統時間</h3>



<p class="wp-block-paragraph">憑證效期是根據時間計算的。如果您的電腦或是伺服器的 <strong>系統日期/時間不正確</strong>（例如年份跳掉了），也會導致憑證被判定為過期。</p>



<ul class="wp-block-list">
<li><strong>操作：</strong> 請確認您的電腦與資料庫伺服器的時間都已經同步到目前的正確時間。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方案三：更換憑證（伺服器端永久解決方案）</h3>



<p class="wp-block-paragraph">如果您是該伺服器的管理員，應更新伺服器上的憑證，以確保所有使用者都能安全連線。</p>



<ol start="1" class="wp-block-list">
<li><strong>開啟工具：</strong> 開啟「SQL Server 組態管理員」(SQL Server Configuration Manager)。</li>



<li><strong>尋找協定：</strong> 導航至「SQL Server 網路組態」 > 「[您的實例名稱] 的協定」。</li>



<li><strong>檢查憑證：</strong> 右鍵點擊「內容」，在「憑證」頁籤中可以看到目前過期的憑證資訊。</li>



<li><strong>更換：</strong> 申請新的憑證（或產生新的自我簽署憑證）並在此處重新選取。</li>



<li><strong>重啟服務：</strong> 更改憑證後，必須<strong>重啟 SQL Server 服務</strong>才會生效。</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p class="wp-block-paragraph"><strong>為什麼以前沒問題，現在突然出現？</strong></p>



<p class="wp-block-paragraph">這通常有兩個原因：</p>



<ol start="1" class="wp-block-list">
<li>憑證真的剛好在最近到期了。</li>



<li>您最近更新了 <strong>SSMS 19.x 或更高版本</strong>。新版本預設將「加密」設為強制開啟，這會導致原本不嚴謹的連線環境（使用過期或自我簽署憑證）開始報錯。</li>
</ol>



<p class="wp-block-paragraph"></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/sql-server-ssl-certificate-expired/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
