Carian, penapis dan halaman dalam senarai produk
Tambah carian nama dan SKU dengan LIKE yang selamat, penapis stok, dan pembahagian halaman menggunakan LIMIT dan OFFSET dalam MySQL 8.4 pada aplikasi CFML.

Kandungan
- Carian, penapis dan halaman dalam senarai produk
- Mengapa carian menggunakan GET
- Tiga nilai daripada URL
- Menapis dengan LIKE
- Perangkap tanda peratus dalam LIKE
- Penapis status stok
- Kira jumlah dahulu, kemudian ambil satu halaman
- Pautan halaman
- Kod penuh index.cfm
- Ujian yang saya jalankan
- Bila OFFSET menjadi perlahan
- Ralat lazim
- Apa yang anda sudah ada
- Soalan Lazim
- Sumber
Carian, penapis dan halaman dalam senarai produk
Dengan tiga produk, senarai panjang masih mudah dibaca. Dengan tiga ratus, ia menjadi tidak berguna: pemilik kedai perlu menatal berpuluh kali untuk mencari satu barang.
Bab ini menambah tiga perkara pada halaman senarai: carian, penapis dan pembahagian halaman. Ia menyambung terus daripada senarai yang anda bina dalam bab header dan jadual.
Mengapa carian menggunakan GET
Borang carian menghantar data dengan method="get", bukan post.
Sebabnya mudah: hasil carian sepatutnya boleh dikongsi dan disimpan sebagai penanda buku. Apabila anda menghantar pautan index.cfm?cari=kopi kepada rakan, dia melihat hasil yang sama.
Borang yang mengubah data menggunakan POST, seperti yang anda lihat dalam bab borang. Borang yang hanya membaca menggunakan GET.
Tiga nilai daripada URL
Halaman senarai kini menerima tiga nilai. Setiap satu diberi nilai lalai, supaya halaman tetap berfungsi walaupun URL kosong.
| Nilai | Lalai | Maksud |
|---|---|---|
cari | kosong | Teks carian untuk nama atau SKU |
status | semua | semua, ada atau habis |
ms | 1 | Nombor halaman |
<cfparam name="url.cari" default="">
<cfparam name="url.status" default="semua">
<cfparam name="url.ms" default="1">
cfparam menetapkan nilai hanya jika nilai itu belum ada. Ia mengelakkan ralat "variable doesn't exist" apabila pengguna membuka index.cfm tanpa apa-apa parameter.
Menapis dengan LIKE
Carian menggunakan LIKE, yang membolehkan padanan sebahagian. Kita membina corak dengan tanda peratus di kedua-dua hujung, supaya "kopi" sepadan dengan "Kopi O 250g" dan "Kopi 500g".
<cfquery name="senaraiProduk" datasource="inventori">
SELECT id, sku, nama, harga, stok
FROM produk
WHERE is_active = 1
<cfif len(carian)>
AND (nama LIKE <cfqueryparam value="%#carian#%" cfsqltype="cf_sql_varchar">
OR sku LIKE <cfqueryparam value="%#carian#%" cfsqltype="cf_sql_varchar">)
</cfif>
ORDER BY nama
</cfquery>
Perhatikan bahawa tanda % berada di dalam nilai cfqueryparam, bukan dicantum ke dalam SQL. Ini masih selamat, kerana keseluruhan nilai, termasuk tanda peratus, dihantar sebagai data.
Yang tidak selamat ialah menulis WHERE nama LIKE '%#carian#%' tanpa cfqueryparam. Jangan lakukan itu.
Perangkap tanda peratus dalam LIKE
Tanda % dalam LIKE bermaksud "apa-apa sahaja". Jadi kalau pengguna menaip % dalam kotak carian, query menjadi LIKE '%%%', dan ia memadankan setiap baris.
Saya mengujinya pada rig: menaip % memulangkan 13 produk, bukan sifar. Ia bukan keselamatan, tetapi ia mengelirukan.
Penyelesaiannya ialah melarikan tanda khas sebelum membina corak:
<cfset carianSelamat = replace(replace(replace(carian, "\", "\\"), "%", "\%"), "_", "\_")>
Selepas perubahan itu, ujian yang sama memulangkan sifar, kerana tiada nama produk yang mengandungi tanda peratus. Carian biasa seperti "kopi" masih memulangkan dua produk, jadi carian harian tidak terjejas.
Tanda _ juga liar: ia bermaksud "satu aksara". Kedua-duanya perlu dilarikan. Ini perkara kecil, tetapi ia membezakan carian yang boleh dipercayai daripada carian yang kadang-kadang memulangkan seluruh jadual.
Penapis status stok
Penapis menggunakan <cfif> di dalam query, supaya SQL hanya mengandungi syarat yang diperlukan:
<cfif status EQ "ada">AND stok > 0</cfif>
<cfif status EQ "habis">AND stok = 0</cfif>
Perhatikan bahawa tanda > ditulis terus, bukan sebagai >.
Ini bertentangan dengan nasihat yang biasa didengar, jadi saya mengujinya pada rig. Entiti > tidak diterjemah di dalam <cfquery>: teks itu dihantar kepada MySQL seadanya, dan MySQL membalas dengan ralat sintaks. Menulis > terus berfungsi. Tiga kes diuji, dan hanya bentuk > yang lulus.
Kira jumlah dahulu, kemudian ambil satu halaman
Pembahagian halaman memerlukan dua query. Yang pertama mengira jumlah baris, yang kedua mengambil hanya baris untuk halaman semasa.
<cfquery name="kira" datasource="inventori">
SELECT COUNT(*) AS jumlah
FROM produk
WHERE is_active = 1
<cfif len(carian)>
AND (nama LIKE <cfqueryparam value="%#carianSelamat#%" cfsqltype="cf_sql_varchar">
OR sku LIKE <cfqueryparam value="%#carianSelamat#%" cfsqltype="cf_sql_varchar">)
</cfif>
</cfquery>
<cfset jumlahMuka = max(1, ceiling(kira.jumlah / had))>
Kemudian query kedua menambah LIMIT dan OFFSET:
LIMIT <cfqueryparam value="#had#" cfsqltype="cf_sql_integer">
OFFSET <cfqueryparam value="#mula#" cfsqltype="cf_sql_integer">
had ialah bilangan baris setiap halaman, dan mula dikira daripada nombor halaman:
<cfset muka = max(1, val(url.ms))>
<cfset mula = (muka - 1) * had>
max(1, ...) menghalang halaman sifar atau negatif, yang akan menghasilkan OFFSET -10 dan ralat.
Pautan halaman
Pautan mesti membawa carian dan penapis semasa. Tanpanya, menekan "Seterusnya" akan membuang carian pengguna.
<a class="butang" href="index.cfm?cari=#encodeForURL(carian)#&status=#status#&ms=#muka+1#">Seterusnya »</a>
encodeForURL memastikan teks carian yang mengandungi ruang atau aksara khas tidak memecahkan pautan.
Kod penuh index.cfm
Selepas semua bahagian di atas dicantumkan, index.cfm kelihatan seperti ini. Anda boleh menyalinnya terus, dengan syarat jadual produk anda mempunyai lajur is_active daripada bab ubah dan arkib.
<cfinclude template="includes/atas.cfm">
<cfparam name="url.cari" default="">
<cfparam name="url.status" default="semua">
<cfparam name="url.ms" default="1">
<cfset carian = trim(url.cari)>
<cfset carianSelamat = replace(replace(replace(carian, "\", "\\"), "%", "\%"), "_", "\_")>
<cfset status = url.status>
<cfset had = 10>
<cfset muka = max(1, val(url.ms))>
<cfset mula = (muka - 1) * had>
<cfquery name="kira" datasource="inventori">
SELECT COUNT(*) AS jumlah
FROM produk
WHERE is_active = 1
<cfif len(carian)>
AND (nama LIKE <cfqueryparam value="%#carianSelamat#%" cfsqltype="cf_sql_varchar">
OR sku LIKE <cfqueryparam value="%#carianSelamat#%" cfsqltype="cf_sql_varchar">)
</cfif>
<cfif status EQ "ada">AND stok > 0</cfif>
<cfif status EQ "habis">AND stok = 0</cfif>
</cfquery>
<cfset jumlahMuka = max(1, ceiling(kira.jumlah / had))>
<cfquery name="senaraiProduk" datasource="inventori">
SELECT id, sku, nama, harga, stok
FROM produk
WHERE is_active = 1
<cfif len(carian)>
AND (nama LIKE <cfqueryparam value="%#carianSelamat#%" cfsqltype="cf_sql_varchar">
OR sku LIKE <cfqueryparam value="%#carianSelamat#%" cfsqltype="cf_sql_varchar">)
</cfif>
<cfif status EQ "ada">AND stok > 0</cfif>
<cfif status EQ "habis">AND stok = 0</cfif>
ORDER BY nama
LIMIT <cfqueryparam value="#had#" cfsqltype="cf_sql_integer">
OFFSET <cfqueryparam value="#mula#" cfsqltype="cf_sql_integer">
</cfquery>
<p class="tindakan"><a class="butang" href="produk-tambah.cfm">+ Tambah produk</a></p>
<cfoutput>
<form method="get" action="index.cfm" class="carian">
<input type="text" name="cari" value="#encodeForHTMLAttribute(carian)#" placeholder="Cari nama atau SKU">
<select name="status">
<option value="semua" #(status EQ "semua" ? "selected" : "")#>Semua</option>
<option value="ada" #(status EQ "ada" ? "selected" : "")#>Ada stok</option>
<option value="habis" #(status EQ "habis" ? "selected" : "")#>Habis</option>
</select>
<button type="submit">Cari</button>
</form>
<p class="butiran">#kira.jumlah# produk dijumpai. Halaman #muka# daripada #jumlahMuka#.</p>
</cfoutput>
<table>
<thead>
<tr>
<th scope="col">SKU</th>
<th scope="col">Nama</th>
<th scope="col">Harga</th>
<th scope="col">Stok</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
<cfoutput>
<cfloop query="senaraiProduk">
<tr>
<td>#sku#</td>
<td>#nama#</td>
<td class="nombor">RM#numberFormat(harga, "0.00")#</td>
<td class="nombor">#stok#</td>
<td><a href="produk-ubah.cfm?id=#id#">Ubah</a></td>
</tr>
</cfloop>
</cfoutput>
</tbody>
</table>
<cfoutput>
<cfif jumlahMuka GT 1>
<p class="muka">
<cfif muka GT 1>
<a class="butang" href="index.cfm?cari=#encodeForURL(carian)#&status=#status#&ms=#muka-1#">« Sebelum</a>
</cfif>
<cfif muka LT jumlahMuka>
<a class="butang" href="index.cfm?cari=#encodeForURL(carian)#&status=#status#&ms=#muka+1#">Seterusnya »</a>
</cfif>
</p>
</cfif>
</cfoutput>
<cfinclude template="includes/bawah.cfm">
Halaman ini menggunakan pengawal log masuk? Tidak, kerana ia hanya membaca data. Tetapi pautan ke produk-tambah.cfm akan menghantar pengguna ke halaman log masuk jika mereka belum masuk, dan pengawal itu sendiri sudah diuji dalam bab log masuk.

Hasil sebenar pada rig ujian kami: carian, penapis dan kiraan halaman berfungsi bersama.
Ujian yang saya jalankan
Dengan 13 produk aktif dan 10 baris setiap halaman:
index.cfmmemaparkan "13 produk dijumpai. Halaman 1 daripada 2."index.cfm?cari=kopimemaparkan 2 produk.index.cfm?status=habismemaparkan 3 produk.index.cfm?ms=2memaparkan halaman 2 daripada 2.index.cfm?cari=%25pada mulanya memaparkan 13 produk, kemudian sifar selepas pelarian ditambah.
Itu lima kes, dan setiap satu dijawab oleh halaman sebenar, bukan andaian.
Bila OFFSET menjadi perlahan
OFFSET memberitahu MySQL supaya melangkau baris. Untuk melangkau 10,000 baris, ia masih perlu membaca 10,000 baris itu dahulu.
Untuk kedai dengan beberapa ribu produk, ini tidak menjadi masalah. Kalau senarai anda sampai ratusan ribu baris, carian perlu beralih kepada indeks teks penuh atau pagination berasaskan id terakhir. Jangan bimbang tentang itu sekarang, tetapi ingat bahawa hadnya wujud.
Ralat lazim
Carian tidak memadankan apa-apa walaupun perkataan itu ada. Semak sama ada % berada di dalam nilai cfqueryparam, bukan di luar.
Ralat sintaks SQL pada penapis stok. Anda menulis > atau LT di dalam <cfquery>. Tulis operator SQL biasa: <, >, =. Operator CFML seperti LT dan GT hanya sah dalam <cfif>.
Pautan "Seterusnya" membuang carian. Pautan tidak membawa cari dan status.
Halaman terakhir kosong. jumlahMuka dikira tanpa max(1, ...), atau ms melebihi jumlah halaman. Hadkan ms kepada jumlahMuka.
Hasil carian berbeza antara huruf besar dan kecil. Collation lalai MySQL tidak sensitif huruf, jadi ia sepatutnya berfungsi. Kalau tidak, semak collation lajur nama.
Apa yang anda sudah ada
Senarai anda kini boleh dicari, ditapis dan ditatal. Itu tiga keperluan asas setiap aplikasi yang menyimpan lebih daripada beberapa dozen rekod.
Bab seterusnya menambah pergerakan stok: barang masuk dan keluar, dengan rekod setiap perubahan supaya baki boleh diaudit.
Soalan Lazim
Boleh saya mencari dalam beberapa lajur sekali?
Boleh, dan itulah yang kita buat: nama atau SKU. Untuk setiap lajur tambahan, tambah satu lagi syarat OR dengan cfqueryparam sendiri.
Kenapa tidak guna carian penuh MySQL?
Ia pilihan yang baik apabila data membesar, tetapi ia memerlukan indeks khusus dan cara menulis query yang berbeza. Untuk peringkat ini, LIKE cukup dan mudah difahami.
Berapa baris setiap halaman yang sesuai? Sepuluh hingga dua puluh untuk skrin biasa. Yang penting, ia tetap dan tidak berubah antara halaman.
Boleh saya menyusun mengikut harga?
Boleh. Tambah parameter susun dan gunakan <cfif> untuk memilih ORDER BY harga. Jangan masukkan teks pengguna terus ke dalam ORDER BY.
Adakah carian ini selamat daripada suntikan SQL?
Ya, kerana setiap nilai melalui cfqueryparam. Tetapi ingat, cfqueryparam melindungi nilai, bukan struktur. Jangan sekali-kali membenarkan pengguna memilih nama lajur atau arahan SQL.
Sumber
LIKEdalam MySQL: dev.mysql.com/doc/refman/8.4/en/string-comparison-functions.htmlLIMITdanOFFSET: dev.mysql.com/doc/refman/8.4/en/select.html- Rujukan
cfparam: docs.lucee.org/reference/tags/param.html - Rujukan
encodeForURL: docs.lucee.org/reference/functions/encodeforurl.html