PostgreSQL işlev parametresi olarak tablo adı


87

Postgres işlevinde bir tablo adını parametre olarak geçirmek istiyorum. Bu kodu denedim:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
    BEGIN
    IF EXISTS (select * from quote_ident($1) where quote_ident($1).id=1) THEN
     return 1;
    END IF;
    return 0;
    END;
$$ LANGUAGE plpgsql;

select some_f('table_name');

Ve bunu anladım:

ERROR:  syntax error at or near "."
LINE 4: ...elect * from quote_ident($1) where quote_ident($1).id=1)...
                                                             ^

********** Error **********

ERROR: syntax error at or near "."

Ve işte buna değiştirildiğinde aldığım hata select * from quote_ident($1) tab where tab.id=1:

ERROR:  column tab.id does not exist
LINE 1: ...T EXISTS (select * from quote_ident($1) tab where tab.id...

Muhtemelen quote_ident($1)işe yarıyor, çünkü where quote_ident($1).id=1aldığım parça olmadan 1, bu da bir şeyin seçildiği anlamına geliyor. Neden ilki quote_ident($1)çalışıp ikincisi aynı anda çalışmayabilir? Ve bu nasıl çözülebilir?


Bu sorunun biraz eski olduğunu biliyorum ama başka bir sorunun cevabını ararken buldum. İşleviniz yalnızca bilgi_ şemasını sorgulayamaz mı? Demek istediğim, bu bir bakıma bunun için - veritabanında hangi nesnelerin var olduğunu sorgulamanıza ve görmenize izin vermek. Sadece bir fikir.
David S

@DavidS Yorum için teşekkürler, deneyeceğim.
John Doe

Yanıtlar:


126

Bu daha da basitleştirilebilir ve geliştirilebilir:

CREATE OR REPLACE FUNCTION some_f(_tbl regclass, OUT result integer)
    LANGUAGE plpgsql AS
$func$
BEGIN
   EXECUTE format('SELECT (EXISTS (SELECT FROM %s WHERE id = 1))::int', _tbl)
   INTO result;
END
$func$;

Şema nitelikli adla arayın (aşağıya bakın):

SELECT some_f('myschema.mytable');  -- would fail with quote_ident()

Veya:

SELECT some_f('"my very uncommon table name"');

Önemli noktalar

  • İşlevi basitleştirmek için bir OUTparametre kullanın . Dinamik SQL'in sonucunu doğrudan seçebilir ve işlem yapabilirsiniz. Ek değişkenlere ve koda gerek yok.

  • EXISTStam olarak ne istiyorsan onu yapar. trueSatır varsa veya falsebaşka türlü alırsınız . Bunu yapmanın çeşitli yolları vardır EXISTS, genellikle en verimlidir.

  • Görünüşe göre bir tamsayı geri istiyorsun , bu yüzden booleansonucu ' EXISTSa integerçeviriyorum, bu da tam olarak sahip olduğun şeyi verir. Bunun yerine boole döndürürdüm .

  • Nesne tanımlayıcı türünü regclassgirdi türü olarak kullanıyorum _tbl. Bu her şeyi yapar quote_ident(_tbl)veya format('%I', _tbl)yapar, ama daha iyi, çünkü:

  • .. SQL enjeksiyonunu da engeller .

  • .. tablo adı geçersizse / mevcut değilse / mevcut kullanıcı tarafından görünmüyorsa, hemen ve daha nazikçe başarısız olur. (Bir regclassparametre yalnızca mevcut tablolar için geçerlidir .)

  • .. şema nitelikli tablo adlarıyla çalışır, burada düz quote_ident(_tbl)veya format(%I)belirsizliği çözemedikleri için başarısız olur. Şema ve tablo adlarını ayrı ayrı geçirmeniz ve bunlardan kaçmanız gerekir.

  • Hala kullanıyorum format(), çünkü sözdizimini basitleştiriyor (ve nasıl kullanıldığını göstermek için), ancak %sbunun yerine %I. Tipik olarak, sorgular daha karmaşıktır, bu nedenle format()daha fazla yardımcı olur. Basit bir örnek olarak şunu da birleştirebiliriz:

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • Listede idyalnızca tek bir tablo varken , sütunu tablo olarak nitelendirmeye gerek yoktur FROM. Bu örnekte belirsizlik mümkün değil. (Dinamik), SQL içinde komutları EXECUTEbir olması ayrı kapsamı , fonksiyon değişken veya parametreler görünmeyen - işlev gövdesinde düz SQL komutları karşı.

Dinamik SQL için kullanıcı girdisinden her zaman doğru şekilde çıkmanızın nedeni :

db <> burada SQL enjeksiyonunu gösteren fiddle
Eski sqlfiddle


2
@suhprano: Elbette. Deneyin:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter

neden% s değil% L?
Lotus

3
@Lotus: Açıklama cevapta. regclassmetin olarak çıktı alınırken değerler otomatik olarak atlanır. bu durumda yanlış%L olur .
Erwin Brandstetter

CREATE OR REPLACE FUNCTION table_rows(_tbl regclass, OUT result integer) AS $func$ BEGIN EXECUTE 'SELECT (SELECT count(1) FROM ' || _tbl || ' )::int' INTO result; END $func$ LANGUAGE plpgsql; bir tablo satırı sayma işlevi oluşturunselect table_rows('nf_part1');
Ferris

tüm sütunları nasıl alabiliriz?
Ashish

13

Mümkünse bunu yapma.

Cevap bu - bu bir anti-model. Müşteri veriyi istediği tabloyu biliyorsa, o zaman SELECT FROM ThatTable. Bir veritabanı bunun gerekli olduğu şekilde tasarlanmışsa, alt-optimal olarak tasarlanmış gibi görünmektedir. Bir veri erişim katmanının bir tabloda bir değer olup olmadığını bilmesi gerekiyorsa, bu kodda SQL oluşturmak kolaydır ve bu kodu veritabanına aktarmak iyi değildir.

Bana göre bu, asansörün içine istenen katın numarasını yazabilecek bir cihaz kurmak gibi görünüyor. Git düğmesine basıldıktan sonra, mekanik bir kolu istenen kat için doğru düğmeye hareket ettirir ve basar. Bu, birçok olası sorunu ortaya çıkarır.

Lütfen dikkat: burada alay etme niyeti yok. Aptal asansör örneğim, bu teknikle ilgili sorunları kısa ve öz bir şekilde belirtmek için * hayal edebileceğim en iyi cihazdı *. İşe yaramaz bir yönlendirme katmanı ekler, tablo adı seçimini arayan alanından (sağlam ve iyi anlaşılmış bir DSL, SQL kullanarak) belirsiz / tuhaf sunucu tarafı SQL kodunu kullanarak hibrit bir alana taşır.

Sorgu oluşturma mantığının dinamik SQL'e taşınmasıyla bu tür sorumluluk bölme, kodun anlaşılmasını zorlaştırır. Hata potansiyeli taşıyan özel kod adına standart ve güvenilir bir kuralı (bir SQL sorgusunun neyi seçeceğini nasıl seçtiği) ihlal eder.

İşte bu yaklaşımla ilgili bazı olası sorunların ayrıntılı noktaları:

  • Dinamik SQL, ön uç kodunda veya yalnızca arka uç kodunda tanınması zor olan SQL enjeksiyonu olasılığını sunar (bunu görmek için bunları birlikte incelemeniz gerekir).

  • Saklanan prosedürler ve işlevler, SP / işlev sahibinin haklarına sahip olduğu ancak arayanın sahip olmadığı kaynaklara erişebilir. Anladığım kadarıyla, özel bir özen göstermeden, o zaman varsayılan olarak dinamik SQL üreten ve çalıştıran bir kod kullandığınızda, veritabanı dinamik SQL'i çağıranın hakları altında yürütür. Bu, ya ayrıcalıklı nesneleri hiç kullanamayacağınız ya da onları tüm istemcilere açmanız gerektiği anlamına gelir, böylece ayrıcalıklı verilere yönelik potansiyel saldırının yüzey alanını arttırırsınız. Oluşturma zamanında SP / işlevi her zaman belirli bir kullanıcı olarak (SQL Server'da EXECUTE AS) çalışacak şekilde ayarlamak bu sorunu çözebilir, ancak işleri daha karmaşık hale getirir. Bu, dinamik SQL'i çok cazip bir saldırı vektörü haline getirerek, önceki noktada bahsedilen SQL enjeksiyonu riskini arttırır.

  • Bir geliştiricinin, onu değiştirmek veya bir hatayı düzeltmek için uygulama kodunun ne yaptığını anlaması gerektiğinde, çalıştırılan tam SQL sorgusunu elde etmeyi çok zor bulacaktır. SQL profil oluşturucu kullanılabilir, ancak bu özel ayrıcalıklar gerektirir ve üretim sistemleri üzerinde olumsuz performans etkilerine neden olabilir. Yürütülen sorgu, SP tarafından günlüğe kaydedilebilir, ancak bu, şüpheli fayda için karmaşıklığı artırır (yeni tabloların yerleştirilmesi, eski verilerin temizlenmesi, vb.) Ve oldukça açık değildir. Aslında, bazı uygulamalar, geliştiricinin veritabanı kimlik bilgilerine sahip olmayacağı şekilde tasarlanmıştır, bu nedenle, gönderilen sorguyu gerçekten görmesi neredeyse imkansız hale gelir.

  • Var olmayan bir tabloyu seçmeye çalıştığınızda olduğu gibi bir hata oluştuğunda, veritabanından "geçersiz nesne adı" satırları boyunca bir mesaj alırsınız. Bu, SQL'i arka uçta veya veritabanında oluştursanız da tamamen aynı olacaktır, ancak fark şu ki, sistemi gidermeye çalışan bazı zayıf geliştiriciler, bir seviye daha derine, Sorun var, sorunun ne olduğunu anlamaya çalışmak için Her Şeyi Yapan harika prosedürü derinlemesine incelemek. Günlüklerde "GetWidget'te Hata" görünmeyecek, "OneProcedureToRuleThemAllRunner'da Hata" gösterilecektir. Bu soyutlama genellikle bir sistemi daha kötü hale getirecektir .

Bir parametreye dayalı olarak tablo adlarını değiştirmenin sözde C # örneğindeki bir örnek:

string sql = $"SELECT * FROM {EscapeSqlIdentifier(tableName)};"
results = connection.Execute(sql);

Bu, akla gelebilecek her olası sorunu ortadan kaldırmasa da, diğer teknikle ana hatlarıyla belirttiğim kusurlar bu örnekte yok.


4
Buna tamamen katılmıyorum. Diyelim ki, bu "Git" düğmesine basıyorsunuz ve sonra bazı mekanizmalar katın var olup olmadığını kontrol ediyor. Tetikleyicilerde işlevler kullanılabilir ve bu da bazı koşulları kontrol edebilir. Bu karar en güzel karar olmayabilir, ancak sistem zaten yeterince büyükse ve mantığında bazı düzeltmeler yapmanız gerekiyorsa, bu seçim sanırım o kadar dramatik değil.
John Doe

2
Ancak, var olmayan bir düğmeye basmaya çalışma eyleminin, onu nasıl ele alırsanız alın, bir istisna oluşturacağını düşünün. Gerçekte var olmayan bir düğmeye basamazsınız, bu nedenle, söz konusu katmanı oluşturmadan önce böyle bir numara girişi olmadığı için, düğmeye basmanın üstüne, var olmayan sayıları kontrol etmek için bir katman eklemenin bir faydası yoktur! Soyutlama, bence programlamadaki en güçlü araçtır. Ancak, mevcut bir soyutlamayı çok az kopyalayan bir katman eklemek yanlıştır . Veritabanının kendisi zaten isimleri veri kümelerine eşleyen bir soyutlama katmanıdır.
ErikE

3
Nokta üzerinde. SQL'in tüm amacı, ayıklanmasını istediğiniz veri kümesini ifade etmektir. Bu işlevin yaptığı tek şey, "korunmuş" bir SQL ifadesini kapsüllemektir. Tanımlayıcının da sabit kodlu olduğu gerçeği göz önüne alındığında, her şeyin kötü bir kokusu vardır.
Nick Hristov

2
@three Birisi bir becerinin ustalık aşamasında olana kadar ( Dreyfus beceri edinme modeline bakın ), "dinamik SQL'de kullanılacak bir prosedüre tablo adlarını GEÇMEYİN" gibi kurallara kesinlikle uymalıdır. Bunun her zaman kötü olmadığını ima etmek bile başlı başına kötü bir tavsiye . Bunu bilerek, yeni başlayanlar onu kullanmaya cazip gelecektir! Bu kötü. Sadece bir konunun ustaları kuralları çiğnemelidir, çünkü belirli bir durumda böyle bir kural ihlalinin gerçekten mantıklı olup olmadığını bilen deneyime sahip olan tek kişi onlardır.
ErikE

2
@ three-cup'ın neden kötü bir fikir olduğu konusunda çok daha fazla ayrıntıyla güncelleme yaptım.
ErikE

10

Plpgsql kodunun içinde, EXECUTE deyimi, tablo adlarının veya sütunların değişkenlerden geldiği sorgular için kullanılmalıdır. Ayrıca , dinamik olarak oluşturulduğunda IF EXISTS (<query>)yapıya izin verilmez query.

İşte her iki sorunun çözüldüğü işleviniz:

CREATE OR REPLACE FUNCTION some_f(param character varying) RETURNS integer 
AS $$
DECLARE
 v int;
BEGIN
      EXECUTE 'select 1 FROM ' || quote_ident(param) || ' WHERE '
            || quote_ident(param) || '.id = 1' INTO v;
      IF v THEN return 1; ELSE return 0; END IF;
END;
$$ LANGUAGE plpgsql;

Teşekkür ederim, birkaç dakika önce cevabınızı okurken aynısını yapıyordum. Tek fark, quote_ident()fazladan alıntılar eklediğinden kaldırmak zorunda kaldım , bu beni biraz şaşırttı, çünkü çoğu örnekte kullanıldı.
John Doe

Tablo adı [az] dışında karakterler içeriyorsa / olduğunda veya ayrılmış bir tanımlayıcıyla çakışırsa (örnek: tablo adı olarak "grup")
Daniel Vérité

Ve bu arada, lütfen bu IF EXISTS <query>yapının var olmadığını kanıtlayacak bir bağlantı sağlar mısınız? Çalışan bir kod örneği olarak böyle bir şey gördüğüme oldukça eminim.
John Doe

1
@JohnDoe: plpgsql'de IF EXISTS (<query>) THEN ...mükemmel bir şekilde geçerli bir yapıdır. Sadece dinamik SQL ile değil <query>. Onu çok kullanırım. Ayrıca bu fonksiyon oldukça geliştirilebilir. Bir cevap gönderdim.
Erwin Brandstetter

1
Üzgünüm, haklısınız if exists(<query>), genel durumda geçerlidir. Yanıtı buna göre kontrol edip değiştirdim.
Daniel Vérité

4

İlki aslında sizin demek istediğiniz anlamda "çalışmıyor", sadece bir hata oluşturmadığı sürece işe yarıyor.

Deneyin SELECT * FROM quote_ident('table_that_does_not_exist');, ve fonksiyonunuzun neden 1'i döndürdüğünü göreceksiniz: select, tek sütunlu (adlandırılmış quote_ident) tek satırlı (değişken $1veya bu özel durumda table_that_does_not_exist) bir tablo döndürüyor .

Yapmak istediğiniz şey dinamik SQL gerektirecektir, bu aslında quote_*işlevlerin kullanılması gereken yerdir .


Çok teşekkürler Matt, table_that_does_not_existaynı sonucu verdi, haklısın.
John Doe

2

Soru tablonun boş olup olmadığını test etmekse (id = 1), işte Erwin'in depolanan işleminin basitleştirilmiş bir sürümü:

CREATE OR REPLACE FUNCTION isEmpty(tableName text, OUT zeroIfEmpty integer) AS
$func$
BEGIN
EXECUTE format('SELECT COALESCE ((SELECT 1 FROM %s LIMIT 1),0)', tableName)
INTO zeroIfEmpty;
END
$func$ LANGUAGE plpgsql;

1

Bunun eski bir konu olduğunu biliyorum, ancak son zamanlarda aynı sorunu çözmeye çalışırken karşılaştım - benim durumumda, bazı oldukça karmaşık komut dosyaları için.

Tüm komut dosyasını dinamik SQL'e dönüştürmek ideal değildir. Bu yorucu ve hataya açık bir iştir ve parametrelendirme yeteneğini kaybedersiniz: parametreler, performans ve güvenlik açısından kötü sonuçlarla birlikte SQL'deki sabitlere eklenmelidir.

İşte sadece tablonuzdan seçim yapmanız gerekiyorsa SQL'i olduğu gibi tutmanıza izin veren basit bir püf noktası - geçici bir görünüm oluşturmak için dinamik SQL kullanın:

CREATE OR REPLACE FUNCTION some_f(_tbl varchar) returns integer
AS $$
BEGIN
    drop view if exists myview;
    execute format('create temporary view myview as select * from %s', _tbl);
    -- now you can reference myview in the SQL
    IF EXISTS (select * from myview where myview.id=1) THEN
     return 1;
    END IF;
    return 0;
END;
$$ language plpgsql;

0

Tablo adı, sütun adı ve değerinin parametre olarak işlev görmesi için dinamik olarak aktarılmasını istiyorsanız

bu kodu kullan

create or replace function total_rows(tbl_name text, column_name text, value int)
returns integer as $total$
declare
total integer;
begin
    EXECUTE format('select count(*) from %s WHERE %s = %s', tbl_name, column_name, value) INTO total;
    return total;
end;
$total$ language plpgsql;


postgres=# select total_rows('tbl_name','column_name',2); --2 is the value

-2

PostgreSQL'in 9.4 sürümüne sahibim ve her zaman şu kodu kullanıyorum:

CREATE FUNCTION add_new_table(text) RETURNS void AS
$BODY$
begin
    execute
        'CREATE TABLE ' || $1 || '(
        item_1      type,
        item_2      type
        )';
end;
$BODY$
LANGUAGE plpgsql

Ve sonra:

SELECT add_new_table('my_table_name');

Benim için iyi çalışıyor.

Dikkat! Yukarıdaki örnek, "Veritabanını sorgulama sırasında güvenliği korumak istiyorsak nasıl yapmayalım?"


1
newTablo oluşturmak , mevcut bir tablonun adıyla çalışmaktan farklıdır. Her iki durumda da, kod olarak çalıştırılan metin parametrelerinden çıkış yapmalısınız veya SQL enjeksiyonuna açıksınız.
Erwin Brandstetter

Oh, evet, benim hatam. Konu beni yanılttı ve ayrıca sonuna kadar okumadım. Normalde benim durumumda. : P Metin parametresi olan kod neden enjeksiyona maruz bırakılır?
dm3

Oops, bu gerçekten tehlikeli. Cevap için teşekkür ederim!
dm3
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.