Peringatan :gaya ini dengan SQL dinamis di SECURITY DEFINER
fungsi bisa elegan dan nyaman. Tapi jangan berlebihan menggunakannya. Jangan menumpuk beberapa level fungsi dengan cara ini:
- Gayanya jauh lebih rawan kesalahan daripada SQL biasa.
- Pengalih konteks dengan
SECURITY DEFINER
memiliki label harga. - SQL Dinamis dengan
EXECUTE
tidak dapat menyimpan dan menggunakan kembali paket kueri. - Tidak ada "fungsi inlining".
- Dan saya lebih suka tidak menggunakannya untuk kueri besar di tabel besar sama sekali. Kecanggihan tambahan dapat menjadi penghalang kinerja. Seperti:paralelisme dinonaktifkan untuk rencana kueri dengan cara ini.
Yang mengatakan, fungsi Anda terlihat bagus, saya tidak melihat cara untuk injeksi SQL. format() terbukti baik untuk menggabungkan dan mengutip nilai dan pengidentifikasi untuk SQL dinamis. Sebaliknya, Anda mungkin menghapus beberapa redundansi untuk membuatnya lebih murah.
Parameter fungsi offset__i
dan limit__i
adalah integer
. Injeksi SQL tidak mungkin dilakukan melalui bilangan bulat, sebenarnya tidak perlu mengutipnya (walaupun SQL mengizinkan konstanta string yang dikutip untuk LIMIT
dan OFFSET
). Jadi cukup:
format(' OFFSET %s LIMIT %s', offset__i, limit__i)
Juga, setelah memverifikasi bahwa setiap key__v
ada di antara nama kolom resmi Anda - dan meskipun itu semua adalah nama kolom resmi yang tidak dikutip - tidak perlu menjalankannya melalui %I
. Bisa saja %s
Saya lebih suka menggunakan text
bukannya varchar
. Bukan masalah besar, tapi text
adalah tipe string "pilihan".
Terkait:
- Penentu format untuk variabel integer dalam format() untuk EXECUTE?
- Fungsi untuk mengembalikan kumpulan kolom dinamis untuk tabel yang diberikan
COST 1
tampaknya terlalu rendah. Manual:
Kecuali Anda tahu lebih baik, tinggalkan COST
di default 100
.
Operasi berbasis set tunggal alih-alih semua perulangan
Seluruh perulangan dapat diganti dengan satu SELECT
penyataan. Harus terasa lebih cepat. Tugas relatif mahal di PL/pgSQL. Seperti ini:
CREATE OR REPLACE FUNCTION goods__list_json (_options json, _limit int = NULL, _offset int = NULL, OUT _result jsonb)
RETURNS jsonb
LANGUAGE plpgsql SECURITY DEFINER AS
$func$
DECLARE
_tbl CONSTANT text := 'public.goods_full';
_cols CONSTANT text[] := '{id, id__category, category, name, barcode, price, stock, sale, purchase}';
_oper CONSTANT text[] := '{<, >, <=, >=, =, <>, LIKE, "NOT LIKE", ILIKE, "NOT ILIKE", BETWEEN, "NOT BETWEEN"}';
_sql text;
BEGIN
SELECT concat('SELECT jsonb_agg(t) FROM ('
, 'SELECT ' || string_agg(t.col, ', ' ORDER BY ord) FILTER (WHERE t.arr->>0 = 'true')
-- ORDER BY to preserve order of objects in input
, ' FROM ' || _tbl
, ' WHERE ' || string_agg (
CASE WHEN (t.arr->>1)::int BETWEEN 1 AND 10 THEN
format('%s %s %L' , t.col, _oper[(arr->>1)::int], t.arr->>2)
WHEN (t.arr->>1)::int BETWEEN 11 AND 12 THEN
format('%s %s %L AND %L', t.col, _oper[(arr->>1)::int], t.arr->>2, t.arr->>3)
-- ELSE NULL -- = default - or raise exception for illegal operator index?
END
, ' AND ' ORDER BY ord) -- ORDER BY only cosmetic
, ' OFFSET ' || _offset -- SQLi-safe, no quotes required
, ' LIMIT ' || _limit -- SQLi-safe, no quotes required
, ') t'
)
FROM json_each(_options) WITH ORDINALITY t(col, arr, ord)
WHERE t.col = ANY(_cols) -- only allowed column names - or raise exception for illegal column?
INTO _sql;
IF _sql IS NULL THEN
RAISE EXCEPTION 'Invalid input resulted in empty SQL string! Input: %', _options;
END IF;
RAISE NOTICE 'SQL: %', _sql;
EXECUTE _sql INTO _result;
END
$func$;
db<>fiddle di sini
Lebih pendek, lebih cepat, dan tetap aman terhadap SQLi.
Kutipan hanya ditambahkan jika diperlukan untuk sintaks atau untuk mempertahankan injeksi SQL. Membakar ke nilai filter saja. Nama kolom dan operator diverifikasi berdasarkan daftar bawaan opsi yang diizinkan.
Masukannya adalah json
bukannya jsonb
. Urutan objek dipertahankan dalam json
, sehingga Anda dapat menentukan urutan kolom di SELECT
list (yang bermakna) dan WHERE
kondisi (yang murni kosmetik). Fungsi mengamati keduanya sekarang.
Keluaran _result
masih jsonb
. Menggunakan OUT
parameter, bukan variabel. Itu benar-benar opsional, hanya untuk kenyamanan. (Tidak ada RETURN
explicit eksplisit pernyataan yang diperlukan.)
Perhatikan penggunaan strategis concat()
untuk secara diam-diam mengabaikan NULL dan operator gabungan ||
sehingga NULL membuat string yang digabungkan menjadi NULL. Dengan cara ini, FROM
, WHERE
, LIMIT
, dan OFFSET
hanya dimasukkan jika diperlukan. Sebuah SELECT
pernyataan berfungsi tanpa salah satu dari itu. SELECT
empty kosong list (juga legal, tapi saya kira tidak diinginkan) menghasilkan kesalahan sintaks. Semua dimaksudkan.
Menggunakan format()
hanya untuk WHERE
filter, untuk kenyamanan dan untuk mengutip nilai. Lihat:
Fungsinya bukan STRICT
lagi. _limit
dan _offset
memiliki nilai default NULL
, jadi hanya parameter pertama _options
Dibutuhkan. _limit
dan _offset
dapat NULL atau dihilangkan, maka masing-masing dilucuti dari pernyataan.
Menggunakan text
bukannya varchar
.
Membuat variabel konstan sebenarnya CONSTANT
(kebanyakan untuk dokumentasi).
Selain itu, fungsinya melakukan apa yang asli Anda lakukan.