Tutorial Pasukan Lucee MY

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.

Carian, penapis dan halaman dalam senarai produk
0
Kandungan

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.

NilaiLalaiMaksud
carikosongTeks carian untuk nama atau SKU
statussemuasemua, ada atau habis
ms1Nombor 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 &raquo;</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#">&laquo; Sebelum</a>
        </cfif>
        <cfif muka LT jumlahMuka>
            <a class="butang" href="index.cfm?cari=#encodeForURL(carian)#&status=#status#&ms=#muka+1#">Seterusnya &raquo;</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 carian 'kopi' dengan penapis dan kiraan halaman

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.cfm memaparkan "13 produk dijumpai. Halaman 1 daripada 2."
  • index.cfm?cari=kopi memaparkan 2 produk.
  • index.cfm?status=habis memaparkan 3 produk.
  • index.cfm?ms=2 memaparkan halaman 2 daripada 2.
  • index.cfm?cari=%25 pada 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 &gt; 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

Log masuk untuk menyimpan kemajuan bacaan anda.

0 komen

0 komen

Log masuk untuk menjawab atau memberi komen.

Log masuk