2025年11月13日 星期四

laravel 12 Eloquent ORM-Query Builder

一、簡潔比較 Raw SQL、Query Builder 與 Eloquent(以 Laravel 生態為背景)。
每一項都列出主要優點與缺點,並在最後給出適用情境建議。

        (1) Raw SQL(原生 SQL)
優點:
性能最佳:可撰寫最直接、最優化的查詢(例如複雜 JOIN、窗口函數、子查詢、特定索引提示)。
完全控制:可以使用資料庫專屬語法、函數與最佳化技巧,無抽象限制。
可重現/可移植於非 ORM 環境:不依賴框架抽象,任何 DB client 都能執行。
缺點:
開發速度慢:查詢需手寫,維護成本高,重複性代碼易增加。
可讀性差:複雜查詢不易閱讀與理解。
安全風險:若未妥善綁定參數,易遭 SQL injection。
測試與重構不便:欄位或表名變動需手動追蹤修改。

        (2) Query Builder(查詢建構器,例:Laravel Query Builder)
優點:
平衡性能與便利性:比 Eloquent 更接近 SQL、執行效率高但仍提供連鎖式 API 增進可讀性。
防注入:內建參數綁定,安全性高。
易讀且可組合:動態建立查詢(條件分支、分頁、排序)容易。
更接近 SQL 行為:對複雜查詢比 ORM 更靈活。
缺點:
還是需要理解 SQL:對非常複雜的 DB 特性仍可能受限或變得冗長。
無模型層:返回通常是陣列/物件而非 rich model(未自帶關聯、事件、屬性存取器等)。
中等抽象:在大量業務邏輯與資料操作時,可能需要自行封裝重複邏輯。
適用情境(簡述)
要動態組合查詢、追求效能但想保留較好可讀性時使用。

        (3) Eloquent(ActiveRecord ORM,例:Laravel Eloquent)
優點
開發速度快:以模型表示資料表,能用物件式語法操作資料(create, update, relations)。
高可讀性與可維護性:關聯、訪問器、事件、scope、mutator 等功能使商業邏輯集中在 Model。
生態整合:與框架遷移、驗證、序列化、資源(Resource)等配合良好。
適合快速原型與中等複雜度應用:提高生產力。
缺點
性能開銷:大量關聯查詢或 N+1 問題若不注意會造成效能下降(需 eager loading、精細調整)。
複雜查詢受限:高度複雜或資料庫特有功能時,ORM 生成的 SQL 可能不理想或難以實作。
抽象過度:當資料層行為需要精細調整時,ORM 層可能成為障礙。
隱式行為:例如自動時間戳、大量事件/observer 可能在背景造成意外負擔。

        適用情境(簡述)
CRUD 為主、開發速度與可維護性重於最高性能的應用;團隊偏向領域驅動與物件導向設計。

        總結比較(重點):
1.可控性與效能:Raw SQL > Query Builder > Eloquent(通常)。
2.開發速度與抽象便利性:Eloquent > Query Builder > Raw SQL。
3.安全性(參數綁定預設):Query Builder 與 Eloquent 內建較安全;Raw SQL 需手動綁定。
4.可維護性:Eloquent(關聯與模型封裝)最佳;Raw SQL 最差(除非良好封裝)。

         建議做法(實務折衷)
1.預設使用 Eloquent 來快速開發與維護常見 CRUD 和關聯邏輯,配合 eager loading、scope 與適當的索引避免 N+1 與效能問題。
2.在對效能要求高或需複雜查詢時,改用 Query Builder 以兼顧可讀性與效能。
3.當 Query Builder 仍無法寫出最佳 SQL(或需使用專屬 DB 功能、大查詢最佳化)時,使用 Raw SQL(並務必使用參數綁定與測試)。
4.在專案層面可建立規範:大多數情況用 Eloquent,熱點(Hot path)或報表查詢用 Query Builder/Raw SQL,並把 Raw SQL 封裝至 Repository/Query Class 以利測試與維護。

二、利用Query Builder 在資料庫(DataBase) 插入資料(Data):
1.在Http\Controller\StudentController.php
輸入如下:
   public function addData(){
        DB::table('students')->insert([
        ['name' => 'tester01',
        'email' => 'tester01@gmail.com',
        'age' => 15,
        'date_of_birth' => '2010-01-01',
        'gender' => 'm',
        ],
        ['name' => 'tester02',
        'email' => 'tester02@gmail.com',
        'age' => 15,
        'date_of_birth' => '2010-01-02',
        'gender' => 'f',
        ]
        ]);
        return 'added successfully';
    }

在\routes\web.php 加入
Route::get('add-data',[StudentController::class,'addData']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/add-data
就會出現 
在資料庫,就會出現

三、利用Query Builder 在資料庫(DataBase) 查詢(fetch)資料(Data):
1.取得全部資料
在Http\Controller\StudentController.php
輸入如下:
    public function getData() {
        $item = DB::table('students')->get();
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data',[StudentController::class,'getData']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data
就會出現 
2.取得頭一筆
在Http\Controller\StudentController.php
輸入如下:
        public function getDataFirst() {
        $item = DB::table('students')->first();
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data-first',[StudentController::class,'getDataFirst']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-first
就會出現
3.取得前面兩筆
在Http\Controller\StudentController.php
輸入如下:
        public function getDataLimit() {
        $item = DB::table('students')
        ->limit(2)
        ->get();
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data-limit',[StudentController::class,'getDataLimit']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-limit
就會出現
4.取得指定ID範圍
在Http\Controller\StudentController.php
輸入如下:
    public function getDataWhere() {
        $item = DB::table('students')
        ->where('id','>=',50)
        ->where('id','<=',60)
        ->get();
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data-where',[StudentController::class,'getDataWhere']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-where
就會出現


5.取得指定ID的部分欄位
在Http\Controller\StudentController.php
輸入如下:
    public function getDataSelect() {
        $item = DB::table('students')
        ->select('id','name')
        ->where('id',60)
        ->get();
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data-select',[StudentController::class,'getDataSelect']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-select
就會出現

四、
利用Query Builder 在資料庫(DataBase) 更新(Update)資料(Data):
1.更新第一筆資料的名字
在Http\Controller\StudentController.php
輸入如下:
    public function updateData() {
        DB::table('students')->where('id',1)->update([
            'name' => 'Updated Name',
        ]);
        return 'Updated Successfully !';
    }
如下圖:
在\routes\web.php 加入
Route::get('update-data',[StudentController::class,'updateData']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/update-data
就會出現 
在網址輸入: http://127.0.0.1:8000/get-data-first
就會出現

2.更新第一筆資料的名字、年紀、電子郵件
在Http\Controller\StudentController.php
輸入如下:
    public function updateData01() {
        DB::table('students')->where('id',1)->update([
            'name' => 'Updated Name01',
            'age' => 20,
            'email' => 'Updated01@gmail.com',
        ]);
        return 'Updated Successfully !';
    }
如下圖:
在\routes\web.php 加入
Route::get('update-data-01',[StudentController::class,'updateData01']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/update-data
就會出現 
在網址輸入: http://127.0.0.1:8000/get-data-first
就會出現

五、
利用Query Builder 在資料庫(DataBase) 刪除(Delete)資料(Data):
1.刪除第一筆資料
在Http\Controller\StudentController.php
輸入如下:
    public function deleteData() {
        DB::table('students')->where('id',1)->delete();
        return 'Deleted Successfully !';
    }
如下圖:

在\routes\web.php 加入
Route::get('delete-data',[StudentController::class,'deleteData']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/delete-data
就會出現 
在網址輸入: http://127.0.0.1:8000/get-data-first
就會出現
1.刪除第90筆以後的資料
在Http\Controller\StudentController.php
輸入如下:
    public function deleteData90() {
        DB::table('students')->where('id','>',90)->delete();
        return 'Deleted Successfully !';
    }

如下圖:
在\routes\web.php 加入
Route::get('delete-data-90',[StudentController::class,'deleteData90']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/delete-data-90
就會出現 
在網址輸入: http://127.0.0.1:8000/get-data
就會出現

六、利用Query Builder 在資料庫(DataBase) 使用聚合函式(Aggregate Functions):
1.計算資料表 students 有幾筆資料
在Http\Controller\StudentController.php
輸入如下:
    public function getDataCount() {
        $item = DB::table('students')->count();
        return $item;
    }
如下圖:

在\routes\web.php 加入
Route::get('get-data-count',[StudentController::class,'getDataCount']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-count
就會出現 

2.計算資料表 students 的欄位age 的最大值
在Http\Controller\StudentController.php
輸入如下:
    public function getDataMax() {
        $item = DB::table('students')->max('age');
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data-max',[StudentController::class,'getDataMax']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-max
就會出現 

也可ˇ以在 http://127.0.0.1:8080/phpmyadmin 下SQL指令 
                SELECT * FROM `students` order by `age` desc;
就可以看到欄位age 的最大值

3.計算資料表 students 的欄位age 的最小值
在Http\Controller\StudentController.php
輸入如下:
    public function getDataMin() {
        $item = DB::table('students')->min('age');
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data-min',[StudentController::class,'getDataMin']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-min
就會出現 
也可ˇ以在 http://127.0.0.1:8080/phpmyadmin 下SQL指令 
                SELECT * FROM `students` order by `age`;
就可以看到欄位age 的最大值

4.計算資料表 students 的欄位age 的平均值
在Http\Controller\StudentController.php
輸入如下:
    public function getDataAvg() {
        $item = DB::table('students')->avg('age');
        return $item;
    }
如下圖:
在\routes\web.php 加入
Route::get('get-data-avg',[StudentController::class,'getDataAvg']);
如下圖
接下來,下指令 php artisan serve
在網址輸入: http://127.0.0.1:8000/get-data-avg
就會出現 

資料來源:


2025年10月12日 星期日

變更套件,重寫程式。只要點兩下,就能離開位子。讓電腦依照規劃自動下載YT影片

系列文章:
1.只要點兩下,就能離開位子。讓電腦依照規劃自動下載YT影片https://skjhcreator.blogspot.com/2022/11/youtube.html
2.只要點兩下,就能離開位子。讓電腦依照公開的PlayList自動下載YT多媒體https://skjhcreator.blogspot.com/2022/11/playlistyoutube.html
3.只要點兩下,就能離開位子。讓電腦依照規劃自動下載臉書的影片https://skjhcreator.blogspot.com/2022/11/facebook.html
4.只要點兩下,就能離開位子。讓電腦依照規劃自動下載YT多媒體https://skjhcreator.blogspot.com/2023/02/yt.html
5.變更套件,重寫程式。只要點兩下,就能離開位子。讓電腦依照規劃自動下載YT影片https://skjhcreator.blogspot.com/2025/10/yt.html

       沒想到再次寫程式來下載YT影片,理由是先前的程式在2023年後已無法正常下載YT影片。所以重新再寫程式。但關鍵是原先套件pytube無法正常執行,一直發生錯誤。更改套件便是目前解決問題的關鍵。

       下載檔案。解壓密碼:demo1234
使用步驟:

安裝套件yt-dlp:
pip install yt-dlp

檔案名稱:pyyt_dlp.py
檔案內容:

import yt_dlp
import os

# 取得目前的工作目錄
current_directory = os.getcwd()

# 檢查 YTDownloadList.txt 是否存在
if os.path.exists(current_directory + "\\YTDownloadList.txt"):
    # 如果存在,則讀取檔案內容
    with open("YTDownloadList.txt", "r", encoding="utf-8") as file:
        content = file.readlines()
else:
    # 如果不存在,則創建YTDownloadList.txt 並寫入內容
    with open("YTDownloadList.txt", "w", encoding="utf-8") as file:
        file.write('https://www.youtube.com/watch?v=uSqVw9rO7lg')
    print("YTDownloadList.txt 內有預設音樂影片,請再按一次")
    os.system("pause")
    os.system("exit")
       
ydl_opts = {
    'format': 'best',
}

with yt_dlp.YoutubeDL(ydl_opts) as ydl:
    for i in content:
        ydl.download([i])

資料來源:

2025年9月21日 星期日

只要點兩下兩次,就能依照設定拆分指定PDF頁數並合併成設定好的一個PDF

系列文章:
01.python 不管何時何地,只要點兩下,資料夾內的所有pdf都會合併成一個pdf
https://skjhcreator.blogspot.com/2022/06/pythonpdfpdf.html
02.python 只要點兩下,分別對各資料夾內的pdf合併,合併後的檔名要讓人知道來自哪個資料夾 
https://skjhcreator.blogspot.com/2022/06/python-pdf.html
03. 只要點兩下,就能將一堆的Doc與Docx 轉成 PDF
03.https://skjhcreator.blogspot.com/2023/05/docdocx-pdf.html
04.只要點兩下,就能將一堆的JPG轉成一個PDF,並以JPG所在的資料夾名稱為PDF的檔名
04.https://skjhcreator.blogspot.com/2023/06/jpgpdfjpgpdf.html
05.只要點兩下,就能將放進input的一堆PDF轉成各自的WORD
05.https://skjhcreator.blogspot.com/2023/10/inputpdfword.html
06.只要點兩下,就能將放進input的一堆PDF轉成在ouput資料夾內的各自的WORD 
06.https://skjhcreator.blogspot.com/2023/10/inputpdfouputword.html
07.只要點兩下,就能夠將InputAndOutput資料夾底下的子子孫孫資料夾內所有Word通通轉成PDF
07.https://skjhcreator.blogspot.com/2024/09/word-word-pdf.html
08.只要點兩下,就可以將資料夾input內的所有Word通通轉成一個PDF
08.https://skjhcreator.blogspot.com/2025/07/inputwordpdf.html
09.只要點兩下兩次,就能依照設定拆分指定PDF頁數並合併成設定好的一個PDF
09.https://skjhcreator.blogspot.com/2025/09/pdfpdf.html
10.只要點兩下,就能將input資料夾內所有PDF進行容量壓縮
10.https://skjhcreator.blogspot.com/2026/04/inputpdf.html

        最近遇到一個問題,想要對四個不同的PDF,分別擷取1~2頁、3~4頁、5~6頁、7~8頁,合併成一個總共8頁的PDF。那該怎麼寫程式來處理這件事呢?那如果是用線上拆分PDF網頁與線上合併PDF網頁,該怎麼做呢?則是需要將四個不同PDF分別上傳到拆分網頁後再一頁一頁的PDF下載下來,再將所需要頁數的PDF放在同一個資料夾,然後再上傳到合併網頁,最後下到自己的資料夾。不斷地上傳、下載。光是處理這些步驟,所花的時間根本就無法想像。能不能有個程式,會掃描input資料夾內檔案。匯出input資料夾內所有檔案名稱,我們只要設定起始頁面與最後頁面。再對程式點兩下,就會合併所有的頁面到一個PDF。換句話說,只要點兩下兩次,就能依照設定拆分指定PDF頁數並合併成設定好的一個PDF。

下載檔案。解壓密碼:demo1234
Here is the website where you can download the program and find instructions:
Download。Extraction Password: demo1234
使用說明(Instructions for use):
    1.下載後,解壓縮。解壓縮的方式要保持原有的資料夾架構,如下:
    意即解壓縮後,會看到內有一個pyPDFDivide.exe與兩個資料夾分別是      
     input 與 source 。如下圖:
    2.將所需的PDF 放入資料夾input 內
    3.滑鼠對pyPDFDivide.exe點兩下,就會產出一個OutputFileName.txt
    4.在OutputFileName.txt,填寫所需的頁數起始範圍後,儲存。
    5.滑鼠對pyPDFDivide.exe點兩下,就會產出output.pdf
    6.該output.pdf即為所求。
使用教學(Instructional videos):



以下是開發過程與原始碼 (Development process and code):
安裝python套件 pikepdf:
pip install pikepdf
程式名稱(Program name):pyPDFDivide.py
程式內容(Code):
import os
from pikepdf import Pdf

# 取得目前的工作目錄
current_directory = os.getcwd()
# 取得目前的工作目錄內input資料夾的路徑
current_directory_input = current_directory + "\\input\\"
# 取得工作目錄input資料夾內所有檔案名稱
current_directory_input_files = os.listdir(current_directory_input)

# 檢查 OutputFileName.txt 是否存在
if os.path.exists(current_directory + "\\OutputFileName.txt"):
    # 如果存在,則讀取檔案內容
    with open("OutputFileName.txt", "r", encoding="utf-8") as file:
        content = file.readlines()
else:
    # 如果不存在,則創建 OutputFileName.txt 並寫入內容
    with open("OutputFileName.txt", "w", encoding="utf-8") as file:
        for current_directory_input_file in current_directory_input_files:
            # 開啟每個 PDF 檔案
            pdf = Pdf.open(current_directory_input + current_directory_input_file)
            pages = pdf.pages  # 獲取 PDF 檔案的頁面數量
            # 寫入檔案名稱和頁面數量到 OutputFileName.txt
            file.write("FileName:" + str(current_directory_input_file) + "\nTotalPages:" + str(len(pages)) + "\n" + "StartPage:\nEndPage:\n")

num = 0  # 計數器
FileNameList = []  # 存放檔案名稱的列表
StartPageList = []  # 存放起始頁面的列表
EndPageList = []  # 存放結束頁面的列表

# 解析 OutputFileName.txt 的內容
for i in content:
    if num % 4 == 0:
        # 每四行的第一行是檔案名稱
        temp, tempfilename = i.split(':')
        FileNameList.append(tempfilename.replace("\n", ""))
    elif num % 4 == 2:
        # 每四行的第三行是起始頁面
        temp, tempstartpage = i.split(':')
        StartPageList.append(tempstartpage.replace("\n", ""))
    elif num % 4 == 3:
        # 每四行的第四行是結束頁面
        temp, tempendpage = i.split(':')
        EndPageList.append(tempendpage.replace("\n", ""))
    num = num + 1  # 增加計數器

# 將設定檔 OutputFileName.txt 的內容依序將指定 PDF 拆分合併到 output.pdf
output = Pdf.new()  # 創建一個新的 PDF 檔案
for i in range(len(FileNameList)):
    # 開啟每個指定的 PDF 檔案
    pdf = Pdf.open(current_directory_input + FileNameList[i])
    pages = pdf.pages  # 獲取 PDF 檔案的頁面
    a = int(StartPageList[i])  # 轉換起始頁面為整數
    b = int(EndPageList[i])  # 轉換結束頁面為整數
    # 從起始頁面到結束頁面將頁面添加到新的 PDF 檔案中
    for j in range(a, b + 1):
        output.pages.append(pages[j])
# 儲存合併後的 PDF 檔案
output.save("output.pdf")


2025年9月13日 星期六

只要點兩下就能利用彰化縣EIP系統學生帳號CSV產出指定學生名單的Google Classroom帳號總表


        彰化縣數位學習師生單一帳號系統(EIP=Enterprise Information Portal)於2025年7月7日正式啟用。學務處想利用Google Classroom 來建立全校視訊廣播。因此,給資訊組全年級各班班長的年班座號總表。而資訊組要能產出一個全年級各班班長的Classroom 帳號總表,方便學務處將各班班長加入Google Classroom。 而資訊組只要將這件事簡化,只要點兩下就能得到指定學生名單的Google Classroom帳號總表。然後教學務處如何使用這個程式即可。
        由於EIP系統學生帳號只有帳號,沒有後面的@chc.edu.tw。還要想辦法將@chc.edu.tw加入。但問題是用Excel開啟CSV,學生帳號只有數字,在Excel 格式就會跑掉。比方說學生帳號0432579,在Excel,就會變成432579。若用記事本開,學生帳號"0432579",完全沒法用尋找、取代等方式來簡單完成。就要複製@chc.edu.tw,然後一個個貼上。

1.EIP學生帳號下載方式:
下載的學生資料檔名為學校代碼_學生資料_日期時間.CSV,其格式如下:


下載檔案。解壓密碼:demo1234
Here is the website where you can download the program and find instructions:
Download。Extraction Password: demo1234
使用教學(Instructional videos):



以下是開發過程與原始碼 (Development process and code):
程式名稱(Program name):pyStudent2ClassRoomStudentAccount.py
程式內容(Code):
import csv
import os

# 取得目前的工作目錄
current_directory = os.getcwd()
# 取得目前的工作目錄內input
current_directory_input = current_directory+"\\input\\"
# 取得工作目錄input內所有檔案名稱
current_directory_input_files = os.listdir(current_directory_input)
# 以副檔名來判斷設定相對應的檔案
for i in current_directory_input_files:
    TempFileName,TempFileExtension = os.path.splitext(i)
    if TempFileExtension == ".csv":
        TempCSVFile = i
    elif TempFileExtension == ".xlsx":
        TempXLSXFile = i
    else:
        TempTXT = i
             
# 設定CSVList為一個 list
CSVList = []
# 開啟 CSV 檔案
with open(current_directory_input+TempCSVFile, mode='r', encoding='utf-8') as file:
    # 建立 CSV 讀取器
    reader = csv.reader(file)
    # 逐行讀取 CSV 檔案內容
    for row in reader:
        CSVList.append(row)

# CSVListTitle 取得 CSVList[0]
CSVListTitle = CSVList[0]
# 刪除CSVList[0]
CSVList.pop(0)
# 對CSVList 進行排序
CSVList.sort(key=lambda x: x[0])

ResultList = []
ResultList.append(CSVListTitle)

StudentList = []
with open(current_directory_input+TempTXT, "r") as Stufile:
    tempStudentList = Stufile.readlines()

for i in tempStudentList:
    temp = i.strip("\n")
    StudentList.append(temp)

TempStudentSet = set(StudentList)    
   
TempResultSet = set()
for i in StudentList:
    for j in CSVList:
        if str(i) == str(j[0]):
            #print(str(i),str(j[0]))
            ResultList.append([str(j[0]),str(j[1])+"@chc.edu.tw"])
            TempResultSet.add(str(i))
           
ExceptionSet = TempStudentSet - TempResultSet
ExceptionList = list(ExceptionSet)
           
with open(current_directory+"\\Student2ClassRoomStudentAccount.csv","w", newline="", encoding="utf-8") as file:
    writer = csv.writer(file)
    writer.writerows(ResultList)
print("Student2ClassRoomStudentAccount.csv 檔案已成功寫入!")    

with open(current_directory+"\\Student2ClassRoomStudentAccountException.txt", "w",encoding="utf-8") as file:
    file.write("Excel 找不到的學生帳號如下:\n")
    for i in ExceptionList:
        file.write(str(i)+"\n")
print("Student2ClassRoomStudentAccountException.csv 檔案已成功寫入!")



Ubuntu 26.04:Apache + Django + MySQL 正式部署

系列文章: 1.在 Ubuntu 26.04 建立 Django 開發環境 2.Ubuntu 26.04:Apache + Django + MySQL 正式部署 以 Apache 2.4、 mod_wsgi 、Python 虛擬環境及 MySQL 8.4 部署 D...