

<?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>Wed, 25 Mar 2026 05:40:35 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</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>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 fetchpriority="high" 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="(max-width: 1024px) 100vw, 1024px" /></figure>



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



<p>以下是詳細的比較、實作建議與常見坑點。</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>這是最常見的作法：先 <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>不使用偏移量，而是根據上一頁最後一筆資料的唯一標識（如 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>在 Go 的生態系中，你不需要從零開始寫 SQL，可以利用以下套件處理：</p>



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



<p>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>Ent 對於分頁的 API 設計非常優雅，支援較複雜的查詢組合。</p>



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



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



<p>如果你喜歡原生 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>在 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>當資料量達到千萬級，<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>惡意使用者或爬蟲可能會請求 <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>在 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>從 MSSQL 切換到 MySQL 或 PostgreSQL，對於 Go 後端開發來說，最直接的影響在於 <strong>SQL 語法（Dialect）</strong>、<strong>參數佔位符</strong>以及<strong>底層驅動的行為</strong>。</p>



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



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



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



<p>這是最明顯的改變。雖然邏輯不變，但 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><strong>Cursor-based 的影響：</strong></p>



<p>三者在 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>這是 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><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>在分頁需要回傳「總筆數」時，<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>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>如果你想讓系統在切換資料庫時更輕鬆，建議引入以下套件：</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>要在 Windows 上開發一個結合 <strong>FastAPI (Python)</strong>、<strong>React (Node.js)</strong> 與 <strong>Database</strong> 的容器化應用，最好的做法是利用 <strong>Docker Compose</strong>。這能讓你一鍵啟動所有服務，並模擬雲端（Azure）的多容器運行環境。</p>



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



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



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



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



<p>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>在 <code>backend/</code> 資料夾中，建立以下兩個核心檔案：</p>



<p><strong>requirements.txt</strong>:</p>



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



<p><strong>Dockerfile</strong>:</p>



<p>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><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>在 <code>my-app/</code> 目錄下執行 </p>



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



<p>然後在 <code>frontend/</code> 建立 <strong>Dockerfile</strong>:</p>



<p>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>這是將所有組件串聯起來的核心。在專案根目錄建立 <code>docker-compose.yml</code>：</p>



<p>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><strong>啟動環境</strong>：在根目錄開啟終端機（PowerShell 或 CMD），輸入：</p>



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



<p><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><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>由於你最終要上傳到 Azure，建議你現在可以先安裝 <strong>Azure CLI</strong>：</p>



<p><strong>指令</strong>：</p>



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



<p><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><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>移除 sqlalchemy, database 使用 microsoft sql server, 在 azure 上會換成azure sql</p>



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



<p>在 Windows 上連接 SQL Server，通常建議使用 <code>pyodbc</code> 驅動程式。</p>



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



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



<p>結構保持不變，但 <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>連接 SQL Server 需要在 Linux 容器內安裝 Microsoft ODBC Driver。這部分的 Dockerfile 較為特殊：</p>



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



<p><strong>docker-compose.yml</strong>:</p>



<p>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>不使用 SQLAlchemy，我們直接使用 <code>pyodbc</code> 進行查詢：</p>



<p>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>要在 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>在本地測試時，最重要的是確保 <strong>跨容器通訊</strong> 正常（例如 React 如何抓到 FastAPI 的資料）。</p>



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



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



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



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



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



<p>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>你需要將本地的 Image 給予一個符合 ACR 規範的「標籤 (Tag)」。</p>



<p>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>當你推送到 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>如果你不想管複雜的 Kubernetes，可以使用 <strong>Azure Container Apps</strong>，它支援多容器應用。</p>



<p>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><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>錯誤訊息 <code>failed to read dockerfile: open Dockerfile: no such file or directory</code> 非常直觀：<strong>Docker 在你指定的路徑下找不到 <code>Dockerfile</code></strong>。</p>



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



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



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



<p>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>修正路徑或檔案後，建議先清理一下快取再執行：</p>



<p>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>等你本地 <code>docker-compose up</code> 成功執行後，按照以下順序推送到 Azure：</p>



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



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



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



<p><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>在 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><strong>檢查 <code>package.json</code></strong>：確保你的 <code>frontend/</code> 資料夾內確實有 <code>package.json</code><br>（通常是執行 <code>npx create-react-app .</code> 產生的）。</p>



<p><strong>執行 Build</strong>：</p>



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



<p><strong>啟動服務</strong>：</p>



<p><code>docker-compose up frontend</code></p>



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



<p>請依照以下檔案內容填入你的資料夾：</p>



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



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



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



<p>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>我們簡單用 <code>fetch</code> 來抓取後端的 API 資料。</p>



<p>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>如果你還沒建立 React 專案，最簡單的方式是在 <code>frontend/</code> 資料夾下建立這個檔案：</p>



<p>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>現在再次執行：</p>



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



<p><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><strong>小提示：</strong></p>



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



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



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



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



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



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



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



<p>在 <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>如果還是報錯，請檢查以下三點：</p>



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



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



<p><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>修正檔案後，請執行以下指令（加上 <code>--no-cache</code> 確保它不會抓到舊的錯誤暫存）：</p>



<p>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>由於你之後要推送到 <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><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>這是一個完整的測試指南，幫助你驗證 <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>首先，確保所有服務都已啟動。在 Windows PowerShell 執行：</p>



<p>PowerShell</p>



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



<p><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>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>現在測試前端是否能成功抓到後端的資料。</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>如果你懷疑資料庫根本沒動，可以獨立測試它。</p>



<p><strong>查看日誌：</strong></p>



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



<p>尋找這一行：<code>SQL Server is now ready for client connections.</code>。</p>



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



<p><strong>伺服器：</strong> <code>127.0.0.1,1433</code></p>



<p><strong>帳號：</strong> <code>sa</code></p>



<p><strong>密碼：</strong> <code>YourStrongPassword123!</code> (你在 compose 檔設定的那組)</p>



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



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



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



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



<p>port 3000 改 8080</p>



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



<p>請依照以下三個步驟修改設定：</p>



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



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



<p>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>如果你的 React 程式碼中原本有寫死後端網址，請確認它依然指向 <code>8000</code>（後端沒改）。</p>



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



<p>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>由於你修改了 <code>docker-compose.yml</code> 的結構，建議先停止舊的容器再重新啟動：</p>



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



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



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



<p>以下是 <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>這是 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>既然你已經規劃將附件存放在 <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>如果你不希望固定每個月付 $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>你的校務系統**「平時」<strong>用 Basic 是可以的，但</strong>「宣傳推廣期」**一定會卡頓。</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>我的實務建議</strong>：</p>



<p>先用 <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><strong>如何在程式碼中（例如 C# 或 Python）實作「上傳附件到 Blob 並將 URL 寫回 SQL」的最佳實踐</strong></p>



<p>這是一個非常標準且高效的雲端開發模式：<strong>SQL 存放結構化資料（後設資料），Blob Storage 存放非結構化的大檔案（圖片、PDF、文件）</strong>。</p>



<p>這種做法能讓資料庫保持輕量，避免 <code>DTU - Basic</code> 的空間迅速被填滿。以下是實作「校務建言系統」附件處理的最佳實踐與範例：</p>



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



<h3 class="wp-block-heading">1. 資料庫資料表設計 (SQL Side)</h3>



<p>不要在 SQL 中存檔案本身（Blob 類型），而是存檔案的 <strong>URL</strong> 和 <strong>唯一識別碼 (GUID)</strong>。</p>



<p>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>在開發時，建議遵循以下流程以確保效能：</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>這是最常見的實作方式，簡潔且安全：</p>



<p>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>由於校務系統可能涉及隱私（例如建言者不想讓別人看到他的附件），你不應該把 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>使用 Python 實作「上傳檔案至 Blob 並將 URL 寫入 SQL」是非常直覺的。我們通常會使用 <code>azure-storage-blob</code> 套件處理檔案，並使用 <code>pyodbc</code> 或 <code>SQLAlchemy</code> 處理資料庫。</p>



<p>以下是一個完整的邏輯範例：</p>



<h3 class="wp-block-heading">1. 安裝必要套件</h3>



<p>在你的開發環境中執行：</p>



<p>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>這個範例展示了如何接收一個檔案、上傳到 Azure，最後把資料存進 SQL Database。</p>



<p>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>在實際開發校務系統時，還有兩點建議：</p>



<h4 class="wp-block-heading"><strong>A. 非同步上傳 (Async)</strong></h4>



<p>如果你的系統是使用 <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>與其在程式碼中寫死資料庫密碼（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>在使用 Python 連線時，請務必在 <code>pyodbc</code> 的連線字串中加入 <strong><code>Connection Timeout=30</code></strong>。</p>



<p>因為 Basic 級別的資源較少，有時候冷啟動或排隊會稍微慢一點，適度增加超時時間可以減少連線失敗的機率。</p>



<p></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>mysql 使用 mysqldump 產生的 .sql 檔案, 要匯入到 mssql 之中, 由於匯出的資料量太大, 直接轉換 mysql sql command 為  mssql sql command, 如果使用 SSMS(SQL Server Management Studio) 會遇到記憶體不足的問題.</p>



<p>透過 sqlcmd 匯入資料庫 (不經過 SSMS):</p>



<pre class="wp-block-code"><code>sqlcmd -S localhost -d %db_name% -E -i %output_file%</code></pre>



<p>這個也會遇到很多奇奇怪怪的問題, 明明在 SSMS 執行正常的 table, 在 sqlcmd 會遇到重覆插入 PK 值.</p>



<p>最佳解法是透過下列的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>如果執行過程中遇到記憶體不足的問題，通常是因為 <code>split_values</code> 處理了單一極其巨大的 <code>INSERT</code> 字串。若發生此情況，請告訴我，我們可以改用生成器（Generator）模式來進一步優化字串解析。</p>



<p>上面這個版本針對一般情況是可以使用，針對特定的資料庫欄位類型（例如 Blob 或特殊的日期格式）則需要再進一步的轉換處理。</p>



<p>執行結果:</p>



<figure class="wp-block-image size-full"><img 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>插入成功的執行結果:</p>



<figure class="wp-block-image size-full"><img 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>要在 Python 中將資料寫入 MSSQL，最常見且穩定的組合是使用 pyodbc 套件配合微軟官方提供的 ODBC Driver。安裝檔下載:</p>



<p>下載適用於 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>系統層級需要安裝 Microsoft ODBC Driver for SQL Server。這是微軟提供的驅動程式，讓作業系統能夠與 SQL Server 通訊。建議安裝最新的版本（如 Driver 18 或 17）。</p>



<p>Python 環境則需要安裝 pyodbc 函式庫。你可以透過 pip install pyodbc 指令完成安裝。如果你的專案較具規模，通常也會搭配 sqlalchemy 配合 pandas 來處理大數據量，安裝指令為 pip install sqlalchemy pandas。</p>



<h3 class="wp-block-heading">實作範例</h3>



<p>以下是使用 pyodbc 進行單筆資料插入的標準寫法：</p>



<p>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>在連線字串中，如果你是連線到本機測試環境且沒有設定 SSL 憑證，務必加上 TrustServerCertificate=yes 參數，否則常會因為加密安全性檢查而導致連線失敗。另外，對於大量資料的寫入，建議使用 cursor.fast_executemany = True 或者是 pandas 的 to_sql 方法，這能顯著提升寫入效能。</p>



<p>在 conn_config 中明確加上了 Encrypt: yes。這是因為 ODBC Driver 18 預設要求加密連線。配合 TrustServerCertificate: yes，可以跳過 SSL 憑證的有效性檢查，通常能解決無法開啟具名管道或連線逾時的問題。</p>



<p>如果你的 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>雖然您的電腦可以連線到 SQL Server，但伺服器使用的 <strong>SSL/TLS 憑證已經過期</strong>。</p>



<p>因為加密憑證失效，為了安全性，系統自動阻斷了登入程序。您可以透過以下幾種方式解決：</p>



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



<h3 class="wp-block-heading">方案一：快速跳過驗證（用戶端暫時解決方案）</h3>



<p>如果您需要立即連線且處於安全的內部網路，可以調整 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>憑證效期是根據時間計算的。如果您的電腦或是伺服器的 <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>如果您是該伺服器的管理員，應更新伺服器上的憑證，以確保所有使用者都能安全連線。</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><strong>為什麼以前沒問題，現在突然出現？</strong></p>



<p>這通常有兩個原因：</p>



<ol start="1" class="wp-block-list">
<li>憑證真的剛好在最近到期了。</li>



<li>您最近更新了 <strong>SSMS 19.x 或更高版本</strong>。新版本預設將「加密」設為強制開啟，這會導致原本不嚴謹的連線環境（使用過期或自我簽署憑證）開始報錯。</li>
</ol>



<p></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>
		<item>
		<title>Sql server / MySQL 隨機產生 32bit token</title>
		<link>https://stackoverflow.max-everyday.com/2018/04/sql-server-mysql-random-32bit-token/</link>
					<comments>https://stackoverflow.max-everyday.com/2018/04/sql-server-mysql-random-32bit-token/#comments</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 11 Apr 2018 03:29:33 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<category><![CDATA[mssql]]></category>
		<category><![CDATA[mysql]]></category>
		<guid isPermaLink="false">http://stackoverflow.max-everyday.com/?p=2234</guid>

					<description><![CDATA[MS SQL: Create: token bi...]]></description>
										<content:encoded><![CDATA[<p>MS SQL:</p>
<p><strong>Create:</strong></p>
<blockquote><p>token binary(32)</p></blockquote>
<p><strong>Insert:</strong></p>
<blockquote>
<pre class="p1">HASHBYTES(<span class="s1">'md5'</span>, <span class="s2">CONVERT</span> (<span class="s3">varchar</span>, <span class="s3">RAND</span>(CHECKSUM(NEWID()))))<span class="Apple-converted-space">  </span>+ HASHBYTES(<span class="s1">'md5'</span>, <span class="s2">CONVERT</span> (<span class="s3">varchar</span>, SYSDATETIME()))</pre>
</blockquote>
<p><strong>Selec (Get String)</strong></p>
<blockquote>
<pre>convert(varchar(max),token,2)</pre>
</blockquote>
<p><strong>Select (Query String)</strong></p>
<blockquote>
<pre>token=convert(binary(32), 'token_hex_string', 2)</pre>
</blockquote>
<hr />
<p>MYSQL:</p>
<p><strong>Create:</strong></p>
<blockquote><p>token binary(32)</p></blockquote>
<p><strong>Insert:</strong></p>
<blockquote>
<pre class="p1">unhex(CONCAT(MD5(uuid()),MD5(now())))</pre>
</blockquote>
<p><strong>Selec (Get String)</strong></p>
<blockquote>
<pre>hex(token)</pre>
</blockquote>
<p><strong>Select (Query String)</strong></p>
<blockquote>
<pre>token=unhex('token_hex_string')</pre>
</blockquote>
<p>&nbsp;</p>
<h4>相關文章：</h4>
<p>SQL Server hex string to varbinary conversion<br />
<a href="https://stackoverflow.max-everyday.com/2018/04/sql-server-hex-string-to-varbinary-conversion/">https://stackoverflow.max-everyday.com/2018/04/sql-server-hex-string-to-varbinary-conversion/</a></p>
<p>&nbsp;</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2018/04/sql-server-mysql-random-32bit-token/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Server hex string to varbinary conversion</title>
		<link>https://stackoverflow.max-everyday.com/2018/04/sql-server-hex-string-to-varbinary-conversion/</link>
					<comments>https://stackoverflow.max-everyday.com/2018/04/sql-server-hex-string-to-varbinary-conversion/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Wed, 11 Apr 2018 02:50:35 +0000</pubDate>
				<category><![CDATA[電腦相關應用]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">http://stackoverflow.max-everyday.com/?p=2232</guid>

					<description><![CDATA[在 MS SQL Server 裡做 hex s...]]></description>
										<content:encoded><![CDATA[<p>在 MS SQL Server 裡做 hex string 與 binary 的轉換範例如下：</p>
<p>From <a href="http://blogs.msdn.com/b/sqltips/archive/2008/07/02/converting-from-hex-string-to-varbinary-and-vice-versa.aspx" rel="noreferrer">MSDN</a></p>
<blockquote><p>In SQL Server 2008, these conversions are even more easier since we added support directly in the CONVERT built-in function. The code samples below show how to perform the conversion(s):</p>
<pre class="lang-sql prettyprint prettyprinted"><code><span class="kwd">declare</span> <span class="pun">@</span><span class="pln">hexstring varchar</span><span class="pun">(</span><span class="pln">max</span><span class="pun">);</span>

<span class="kwd">set</span> <span class="pun">@</span><span class="pln">hexstring </span><span class="pun">=</span> <span class="str">'0xabcedf012439'</span><span class="pun">;</span>

<span class="kwd">select</span> <span class="kwd">CONVERT</span><span class="pun">(</span><span class="pln">varbinary</span><span class="pun">(</span><span class="pln">max</span><span class="pun">),</span> <span class="pun">@</span><span class="pln">hexstring</span><span class="pun">,</span> <span class="lit">1</span><span class="pun">);</span>

<span class="kwd">set</span> <span class="pun">@</span><span class="pln">hexstring </span><span class="pun">=</span> <span class="str">'abcedf012439'</span><span class="pun">;</span>

<span class="kwd">select</span> <span class="kwd">CONVERT</span><span class="pun">(</span><span class="pln">varbinary</span><span class="pun">(</span><span class="pln">max</span><span class="pun">),</span> <span class="pun">@</span><span class="pln">hexstring</span><span class="pun">,</span> <span class="lit">2</span><span class="pun">);</span><span class="pln">

go

</span><span class="kwd">declare</span> <span class="pun">@</span><span class="pln">hexbin varbinary</span><span class="pun">(</span><span class="pln">max</span><span class="pun">);</span>

<span class="kwd">set</span> <span class="pun">@</span><span class="pln">hexbin </span><span class="pun">=</span> <span class="lit">0xabcedf012439</span><span class="pun">;</span>

<span class="kwd">select</span>
   <span class="kwd">CONVERT</span><span class="pun">(</span><span class="pln">varchar</span><span class="pun">(</span><span class="pln">max</span><span class="pun">),</span> <span class="pun">@</span><span class="pln">hexbin</span><span class="pun">,</span> <span class="lit">1</span><span class="pun">),</span>
   <span class="kwd">CONVERT</span><span class="pun">(</span><span class="pln">varchar</span><span class="pun">(</span><span class="pln">max</span><span class="pun">),</span> <span class="pun">@</span><span class="pln">hexbin</span><span class="pun">,</span> <span class="lit">2</span><span class="pun">);</span><span class="pln">

go</span></code></pre>
</blockquote>
<hr />
<p>Ex:</p>
<pre class="p1"><span class="s1">select</span> <span class="s1">convert</span>(<span class="s2">varchar</span>(<span class="s2">max</span>), HASHBYTES(<span class="s3">'md5'</span>, <span class="s3">'P@ssw0rd'</span>), 1)<span class="s5">;</span></pre>
<blockquote>
<pre>0x5F4DCC3B5AA765D61D8327DEB882CF99</pre>
</blockquote>
<pre>select convert(varchar(max), HASHBYTES('md5', 'P@ssw0rd'), 2);</pre>
<blockquote>
<pre>5F4DCC3B5AA765D61D8327DEB882CF99</pre>
</blockquote>
<pre class="p1"><span class="s1">select</span> <span class="s1">convert</span><span class="s2">(</span><span class="s3">varbinary</span><span class="s2">(</span><span class="s3">max</span><span class="s2">), </span>'5F4DCC3B5AA765D61D8327DEB882CF99'<span class="s2">, </span><span class="s4">2</span><span class="s2">)</span><span class="s5">;</span></pre>
<blockquote>
<pre>_M³;ZØe© ü'¡Çý°æ</pre>
</blockquote>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2018/04/sql-server-hex-string-to-varbinary-conversion/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[java] 取得資料庫的欄位schema</title>
		<link>https://stackoverflow.max-everyday.com/2017/12/java-table-schema/</link>
					<comments>https://stackoverflow.max-everyday.com/2017/12/java-table-schema/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 05 Dec 2017 09:25:18 +0000</pubDate>
				<category><![CDATA[Java筆記]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[mssql]]></category>
		<category><![CDATA[mysql]]></category>
		<guid isPermaLink="false">http://stackoverflow.max-everyday.com/?p=1683</guid>

					<description><![CDATA[sample code: import java...]]></description>
										<content:encoded><![CDATA[<p>sample code:</p>
<pre>import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;

public class Main {
  private static final String DRIVER = "com.mysql.jdbc.Driver";

  private static final String URL = "jdbc:mysql://localhost/testdb";

  private static final String USERNAME = "root";

  private static final String PASSWORD = "";

  public static void main(String[] args) throws Exception {
    Class.forName(DRIVER);
    Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);

    DatabaseMetaData metadata = connection.getMetaData();
    ResultSet resultSet = metadata.getColumns(null, null, "users", null);
    while (resultSet.next()) {
      String name = resultSet.getString("COLUMN_NAME");
      String type = resultSet.getString("TYPE_NAME");
      int size = resultSet.getInt("COLUMN_SIZE");

      System.out.println("Column name: [" + name + "]; type: [" + type + "]; size: [" + size + "]");
    }
    connection.close();
  }
}</pre>
<hr />
<p>資料來源：</p>
<p><a href="http://www.java2s.com/Code/JavaAPI/java.sql/DatabaseMetaDatagetColumnsStringcatalogStringschemaPatternStringtableNamePatternStringcolumnNamePattern.htm">http://www.java2s.com/Code/JavaAPI/java.sql/DatabaseMetaDatagetColumnsStringcatalogStringschemaPatternStringtableNamePatternStringcolumnNamePattern.htm</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2017/12/java-table-schema/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>com.microsoft.sqlserver.jdbc.SQLServerException: Invalid object name &#8216;trace_xe_action_map&#8217;.  RSS feed</title>
		<link>https://stackoverflow.max-everyday.com/2017/11/com-microsoft-sqlserver-jdbc-sqlserverexception-invalid-object-name-trace_xe_action_map-rss-feed/</link>
					<comments>https://stackoverflow.max-everyday.com/2017/11/com-microsoft-sqlserver-jdbc-sqlserverexception-invalid-object-name-trace_xe_action_map-rss-feed/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Thu, 16 Nov 2017 06:29:25 +0000</pubDate>
				<category><![CDATA[Java筆記]]></category>
		<category><![CDATA[mssql]]></category>
		<guid isPermaLink="false">http://stackoverflow.max-everyday.com/?p=1605</guid>

					<description><![CDATA[使用 DatabaseMetaData.getT...]]></description>
										<content:encoded><![CDATA[<p>使用 DatabaseMetaData.getTables(null, &#8220;%&#8221;, &#8220;%&#8221;, new String[] {&#8220;TABLE&#8221;}); 在 MSSQL 的環境下會多出2個系統的 table.</p>
<p><code>trace_xe_action_map</code> and <code>trace_xe_event_map</code> it&#8217;s some system table in system database.</p>
<p>From MS docs:<br />
This table is stored in the master database, in the sys schema.</p>
<hr />
<div class="card">I am trying to get the names of all tables in a database in using Java. This is the method I am using. However, on running it, it also lists other tables, which are not actually part of my database, or maybe some system tables that I am not interested in. How can I get only the tables which I have created?</p>
<div class="codeDescription"><code class="language-java">/* connecting to database using supplied credentials, and printing out the SQL queries for inserting data to local db */<br />
    public void connectToAzure(String connectionString, String databaseName, String username, String password) throws ClassNotFoundException, SQLException<br />
    {<br />
        Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");  </p>
<p>        final Connection m_Connection = DriverManager.getConnection( connectionString+";DatabaseName="+databaseName, username, password );<br />
        final ArrayList&lt;String&gt; tables = getTableNames(m_Connection);<br />
    }</p>
<p>/* helper method to get table names */<br />
    public ArrayList&lt;String&gt; getTableNames(final Connection m_Connection)<br />
    {<br />
        final ArrayList&lt;String&gt; tables = new ArrayList&lt;&gt;();<br />
        try<br />
        {<br />
            DatabaseMetaData dbmd = m_Connection.getMetaData();<br />
            String[] types = {"TABLE"};<br />
            ResultSet rs = dbmd.getTables(null, null, "%", types);<br />
            while (rs.next())<br />
            {<br />
                String tableName = rs.getString("TABLE_NAME");<br />
                tables.add( tableName );<br />
            }<br />
        }<br />
        catch (SQLException e)<br />
        {<br />
            e.printStackTrace();<br />
        }</p>
<p>        return tables;<br />
    }<br />
</code></div>
<p>Output on running the above</p>
<div class="codeDescription"><code class="language-java">table1, table2, table3, table4, table5, table6, trace_xe_action_map, trace_xe_event_map<br />
</code></div>
<p>Of these,</p>
<div class="codeDescription"><code class="language-java">trace_xe_action_map, trace_xe_event_map<br />
</code></div>
<p>are not tables that I have created.</p></div>
<div class="card">
<h2>Answer</h2>
<div class="code-info"><a title="Java" href="https://reformatcode.com/search/language/Java/1"><span class="badge badge-success">Java</span></a> <span class="post-info">Posted November 9, 2017 by GurV</span></div>
<p>You might want to run a simple select query on the database like this:</p>
<pre class="language-java"><code class="language-java">select TABLE_NAME from INFORMATION_SCHEMA.TABLES;
</code></pre>
<p>You can further filter out the table_names using where clauses like these:</p>
<pre class="language-java"><code class="language-java">SELECT 
    TABLE_NAME
FROM
    INFORMATION_SCHEMA.TABLES
WHERE
    TABLE_CATALOG = ? AND TABLE_SCHEMA = ?;</code></pre>
</div>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2017/11/com-microsoft-sqlserver-jdbc-sqlserverexception-invalid-object-name-trace_xe_action_map-rss-feed/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
