星期三, 4月 01, 2026

[GAS] Realtime Database

透過 Google Apps Script 存取 Firebase Realtime Database (NoSQL)

Firebase 上建立新專案

建立過程依序會詢問專案名稱、是否在 Firebase 上啟用 Gemini、是否啟用 Google Analytics

建立 Firebase RealTime Database

產品類別 => 資料庫和儲存空間 => NoSQL => Realtime Database
點選建立資料庫
選擇資料庫位置,選擇新加玻 (asia-southeadt1)
安全性規則,預設為鎖定模式
Realtime Database 建立完成,紅線為 Readtime Database URL,GAS 會透過它來存取

訂閱方案 - Spark Plan

使用雲端服務最重要的了解帳單費用,Spark Plan 訂閱是 free 的,但 Realtime Database 有使用量限制,萬一到達使用量限制服務就會停止,下圖為 Realtime Database 使用限制,關於 Spark Plan 其他服務使用限制,可以參考官方網站 - Pricing
Google Apps Script CRUD

從 GAS 連線至 Realtime Database 是透過 ScriptApp.getOAuthToken() 取的個人身份來進行驗證,該筆記只著重在 Realtime Database CRUD 操作是沒有問題,但萬一該存取 Realtime Database 會開放,改用 GAS 第三方套件 OAuth2 搭配 Firebase 服務帳號來使用會比較適合

設定 oauth 授權範圍,Google Apps Script Project settings => 啟用 [在編輯器中顯示「appsscript.json」資訊清單檔案]

在編輯器中出現「appsscript.json」資訊清單檔案並加入 oauth 授權範圍

appsscript.json 完整資訊,oauthScopes 為手動加入
{
  "timeZone": "Asia/Taipei",
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "oauthScopes": [
      "https://www.googleapis.com/auth/script.external_request",
      "https://www.googleapis.com/auth/userinfo.email",
      "https://www.googleapis.com/auth/firebase.database"
    ]  
}

在指令碼指令內建立 Realtime Database URL
CRUD Code
/**
 * 從 Script Properties 取得 Firebase 資料庫網址
 * @constant {string}
 */
const FIREBASE_URL = PropertiesService.getScriptProperties().getProperty("FIREBASE_URL");

/**
 * 寫入或覆寫資料 (PUT)
 * 執行此操作會將指定路徑下的資料完全替換為傳入的 data。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @param {Object} data - 欲寫入或覆寫的 JSON 資料物件
 * @returns {Object} Firebase 伺服器回傳的 JSON 解析物件
 */
function writeDataBuiltIn(path, data) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;

  const options = {
    method: "put", // 使用 put 進行資料寫入
    contentType: "application/json",
    headers: {
      Authorization: 'Bearer ' + token
    },
    payload: JSON.stringify(data),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return JSON.parse(response.getContentText());
}

/**
 * 讀取資料 (GET)
 * 從指定的資料庫路徑讀取目前的資料。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @returns {Object|null} Firebase 伺服器回傳的 JSON 解析物件(若該路徑無資料則為 null)
 */
function readDataBuiltIn(path) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;
  
  const options = {
    method: "get", // 使用 get 進行存取
    headers: {
      Authorization: 'Bearer ' + token
    },
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return JSON.parse(response.getContentText());
}

/**
 * 部分更新資料 (PATCH)
 * 只會你傳入的欄位,不會覆寫原本存在的其他欄位。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @param {Object} data - 要更新的 JSON 資料物件
 * @returns {Object} Firebase 伺服器回傳的 JSON 解析物件(包含已更新的欄位資料)
 */
function updateDataBuiltIn(path, data) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;

  const options = {
    method: "patch", // 使用 patch 進行部分更新
    contentType: "application/json",
    headers: {
      Authorization: 'Bearer ' + token
    },
    payload: JSON.stringify(data),
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return JSON.parse(response.getContentText());
}

/**
 * 刪除資料 (DELETE)
 * 會刪除該路徑下的所有資料與子節點。
 * * @param {string} path - Firebase 資料庫中的目標路徑(不含 .json 後綴)
 * @returns {string} Firebase 伺服器回傳的回應字串(刪除成功通常會回傳 "null" 字串)
 */
function deleteDataBuiltIn(path) {

  const token = ScriptApp.getOAuthToken();
  const url = `${FIREBASE_URL}${path}.json`;

  const options = {
    method: "delete", // 使用 delete 刪除節點
    headers: {
      Authorization: 'Bearer ' + token
    },
    muteHttpExceptions: true
  };

  const response = UrlFetchApp.fetch(url, options);
  console.log("寫入狀態: " + response.getResponseCode());
  return response.getContentText(); 
}
實際測試

寫入
function testWriteDataBuiltIn() {
  const userPath = "customers/user_123";
  const userData = {
    name: "陳大文",
    email: "david@example.com",
    membership: "VIP",
    joinDate: Utilities.formatDate(new Date() , Session.getScriptTimeZone() , 'yyyy-MM-dd HH:mm:ss')
  };
    
  writeDataBuiltIn(userPath, userData);
  console.log("寫入測試完成!請到 Firebase 後台查看結果。");
}
讀取

function testReadDataBuiltIn() {

  const testPath = "customers/user_123";   
  const data = readDataBuiltIn(testPath);
  
  if (data !== null) {
    console.log("讀取成功!資料內容如下:");
    console.log(JSON.stringify(data, null, 2));
  } else {
    console.log("讀取失敗,請檢查權限或路徑。");
  }
}
更新

function testUpdateDataBuiltIn() {

  const userPath = "customers/user_123";
  
  // 假設原本資料有 name, email, membership。
  // 我們現在只想升級他的 membership,並新增一個 points 欄位,保留原本的 name 與 email。
  const updatePayload = {
    membership: "VVIP", // 更新現有欄位
    points: 500,        // 新增原本沒有的欄位
    lastUpdated: Utilities.formatDate(new Date() , Session.getScriptTimeZone() , 'yyyy-MM-dd HH:mm:ss')
  };
  
  updateDataBuiltIn(userPath, updatePayload);
  console.log("更新完成!請去 Firebase 檢查 user_123 的資料,原本的名字應該還在。");
}
刪除
function testDeleteDataBuiltIn() {
  const userPath = "customers/user_123";
  deleteDataBuiltIn(userPath);
  console.log("刪除完成!該節點已經從 Firebase 移除了。");
}


星期六, 3月 21, 2026

[EFCore] Like

使用 EFCore 10 來整理 Linq to Entity 產生 TSQL like 語法應用

Linq to Entity 語法
using EFCoreLike.Models;
using Microsoft.EntityFrameworkCore;

namespace EFCoreLike
{
    internal class Program
    {
        static void Main(string[] args)
        {
            using var dbContext = new AdventureWorks2022DbContext();

            // StartWith
            var startWithResult = dbContext.Person
                .Where(p => p.LastName.StartsWith("Hen"))
                .ToList();

            // EndWith
            var endWithResult = dbContext.Person
                .Where(p => p.LastName.EndsWith("Hen"))
                .ToList();

            // Containes
            var containResult = dbContext.Person
                .Where(p => p.LastName.Contains("Hen"))
                .ToList();

            // EF.Functions.Like
            var functionsResult = dbContext.Person
                .Where(p => EF.Functions.Like(p.LastName, "H[a-c]n%"))
                .ToList();
        }
    }
}
產生 TSQL Like 語法觀察

StartWith 轉成 關鍵字%
SELECT [p].[BusinessEntityID], [p].[AdditionalContactInfo], [p].[Demographics], [p].[EmailPromotion], [p].[FirstName], [p].[LastName], [p].[MiddleName], [p].[ModifiedDate], [p].[NameStyle], [p].[PersonType], [p].[Suffix], [p].[Title], [p].[rowguid]
FROM [Person].[Person] AS [p]
WHERE [p].[LastName] LIKE N'Hen%'

EndWith 轉成 %關鍵字
SELECT [p].[BusinessEntityID], [p].[AdditionalContactInfo], [p].[Demographics], [p].[EmailPromotion], [p].[FirstName], [p].[LastName], [p].[MiddleName], [p].[ModifiedDate], [p].[NameStyle], [p].[PersonType], [p].[Suffix], [p].[Title], [p].[rowguid]
FROM [Person].[Person] AS [p]
WHERE [p].[LastName] LIKE N'%Hen'

Containes 轉成 %關鍵字%
SELECT [p].[BusinessEntityID], [p].[AdditionalContactInfo], [p].[Demographics], [p].[EmailPromotion], [p].[FirstName], [p].[LastName], [p].[MiddleName], [p].[ModifiedDate], [p].[NameStyle], [p].[PersonType], [p].[Suffix], [p].[Title], [p].[rowguid]
FROM [Person].[Person] AS [p]
WHERE [p].[LastName] LIKE N'%Hen%'

EFCore 2.0 開始支援 EF.Functions.Like 來自訂 TSQL Like 應用
SELECT [p].[BusinessEntityID], [p].[AdditionalContactInfo], [p].[Demographics], [p].[EmailPromotion], [p].[FirstName], [p].[LastName], [p].[MiddleName], [p].[ModifiedDate], [p].[NameStyle], [p].[PersonType], [p].[Suffix], [p].[Title], [p].[rowguid]
FROM [Person].[Person] AS [p]
WHERE [p].[LastName] LIKE N'H[a-c]n%'

EF 6.5.1 StartWith、EndWith 和 Contains 轉出的 TSQL 跟 EFCore 10 是相同的

星期二, 3月 17, 2026

[VS] 在方案總管的檔案下顯示符號

在 VS2026 更版資訊內看見 [在方案總管的檔案下顯示符號] 選項,覺得在 Windows Form 專案上還不錯用

在 .NET10 Window Form 內可以看見 Form1 底下還有很多資訊
VS 選項 => 專案和解決方案 => 一般 => 取消 [在方案總管的檔案下顯示符號],變更設定後需要重啟 VS2026
取消後 Form1 下資訊就不會顯示,但使用方案總管搜尋功能還是會顯示出來的喔
筆記當下 VS2026 版本為 V18.4.0,但之前版本就有才是

星期二, 3月 10, 2026

[C#] 自訂數值字串 - 零

平時都是用標準格式來格式化數值,突然需要應用上自訂格式情況

常用自訂格式
  • 0:零值預留位置
  • #:數字預留位置
  • .:小數點
  • ,:千分位
LinqPad 上執行

用 0 當成對象簡易紀錄一下
void Main()
{
	
	decimal 零 = 0m;

	零.ToString("#,#").Dump($"#,#:顯示空白");

	零.ToString("#,0").Dump("#,0:顯示一個 0");

	零.ToString("#,##0").Dump("#,##0:千分位寫法");
}
基本上用標準格式是最推薦的,能夠自行依據文化特性來進行轉換,並不是每個國家的千分位和小數點都是 , 和 . 的喔

星期四, 3月 05, 2026

[LINQ] TakeWhile、SkipWhile

實務上遇上 TakeWhile、SkipWhile 應用情境,字串資料為 [英文 + 數字] 組合,但英文和數字字元都是隨機長度,使用 TakeWhile 來取出隨機英語字串

使用 LINQPad 來記錄
void Main()
{
  string data = "ABCDE12345".Dump("測試資料");
  (new string(data.TakeWhile(char.IsLetter).ToArray())).Dump("TakeWhile:擷取字母字元,直至字元不是字母停止");
  (new string(data.SkipWhile(char.IsLetter).ToArray())).Dump("SkipWhile:跳過字母字元,直至字元不是字母開始擷取");
}

執行結果