PostgreSQL関数パラメーターとしてのテーブル名


85

Postgres関数のパラメーターとしてテーブル名を渡したい。私はこのコードを試しました:

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');

そして私はこれを手に入れました:

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 "."

そして、これに変更したときに私が得たエラーは次の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...

おそらく、私が得る部分がなければ、何かが選択されていることを意味するquote_ident($1)ので、うまくwhere quote_ident($1).id=1いきます1。最初のquote_ident($1)作業と2番目の作業が同時に行われないのはなぜですか?そして、これはどのように解決できますか?


この質問は古いものですが、別の問題の答えを探しているときに見つけました。あなたの関数は単にinformational_schemaをクエリすることができませんでしたか?つまり、ある意味では、データベースにどのオブジェクトが存在するかをクエリして確認できるようにするためです。ただのアイデア。
David S

@DavidSコメントありがとうございます、やってみます。
John Doe 2012

回答:


124

これはさらに簡素化および改善できます。

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$;

スキーマ修飾名で呼び出します(以下を参照):

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

または:

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

主なポイント

  • OUTパラメータを使用して関数を簡略化します。動的SQLの結果を直接選択して実行できます。追加の変数やコードは必要ありません。

  • EXISTSまさにあなたが望むことをします。true行が存在するかどうかを取得しfalseます。これを行うにはさまざまな方法があり、EXISTS通常は最も効率的です。

  • あなたはしたいように見える整数私はキャストので、背中をbooleanからの結果をEXISTSintegerあなたが持っていた正確に何を得ています、。代わりにブール値を返します。

  • regclass入力タイプとしてオブジェクト識別子タイプを使用します_tbl。それはすべてを行うか、quote_ident(_tbl)またはformat('%I', _tbl)行うでしょうが、より良い理由は次のとおりです。

  • ..SQLインジェクションも同様に防ぎます

  • ..テーブル名が無効であるか、存在しないか、現在のユーザーに表示されていない場合は、すぐに、より適切に失敗します。(regclassパラメーターは既存のテーブルにのみ適用できます。)

  • ..スキーマ修飾されたテーブル名で機能します。この場合、あいまいさを解決できないため、プレーンquote_ident(_tbl)またはformat(%I)失敗します。スキーマ名とテーブル名を別々に渡したりエスケープしたりする必要があります。

  • format()構文を単純化するため(およびその使用方法を示すため)、私はまだを使用していますが、の%s代わりにを使用してい%Iます。通常、クエリはより複雑なのでformat()、より役立ちます。簡単な例では、連結することもできます。

      EXECUTE 'SELECT (EXISTS (SELECT FROM ' || _tbl || ' WHERE id = 1))::int'
    
  • リストにidテーブルが1つしかない場合は、列をテーブル修飾する必要はありませんFROM。この例では、あいまいさはあり得ません。(動的)内部のSQLコマンドにEXECUTE個別のスコープがあり、関数本体のプレーンSQLコマンドとは対照的に、関数変数またはパラメーターは表示されません。

動的SQLのユーザー入力を常に適切にエスケープする理由は次のとおりです。

ここでdb <> fiddleは、SQLインジェクションを示しています
古いsqlfiddle


2
@suhprano:もちろんです。試してみてください:DO $$BEGIN EXECUTE 'ANALYZE mytbl'; END$$;
Erwin Brandstetter 2015

なぜ%Lではなく%sなのですか?
ロータス

3
@ロータス:説明は答えにあります。regclassテキストとして出力すると、値は自動的にエスケープされます。この場合は間違っている%Lでしょう。
Erwin Brandstetter 2017年

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; テーブル行カウント関数を作成しますselect table_rows('nf_part1');
lmingzhi19年

どうすればすべての列を取得できますか?
Ashish

12

可能であれば、これを行わないでください。

それが答えです—それはアンチパターンです。クライアントがデータを必要とするテーブルを知っている場合は、SELECT FROM ThatTable。これが必要とされる方法でデータベースが設計されている場合、データベースは最適に設計されていないようです。データアクセス層が値がテーブルに存在するかどうかを知る必要がある場合、そのコードでSQLを作成するのは簡単であり、このコードをデータベースにプッシュするのはよくありません。

私には、これは、希望する階数を入力できるエレベータ内にデバイスを設置するように思えます。Goボタンを押すと、機械式ハンドが目的のフロアの正しいボタンに移動して押されます。これにより、多くの潜在的な問題が発生します。

注意:ここでは、嘲笑の意図はありません。私のばかげたエレベーターの例は、このテクニックの問題を簡潔に指摘するための*私が想像できる最高のデバイス*でした。これは、間接参照の役に立たないレイヤーを追加し、テーブル名の選択を呼び出し元スペースから(堅牢でよく理解されているDSL、SQLを使用して)、あいまいで奇妙なサーバー側SQLコードを使用するハイブリッドに移動します。

クエリ構築ロジックを動的SQLに移行することで責任を分割すると、コードが理解しにくくなります。これは、エラーの可能性があるカスタムコードの名前で、標準の信頼できる規則(SQLクエリが選択するものを選択する方法)に違反しています。

このアプローチの潜在的な問題のいくつかに関する詳細なポイントは次のとおりです。

  • 動的SQLは、フロントエンドコードまたはバックエンドコードだけでは認識しにくいSQLインジェクションの可能性を提供します(これを確認するには、それらを一緒に検査する必要があります)。

  • ストアドプロシージャと関数は、SP /関数の所有者が権利を持っているが、呼び出し元は持っていないリソースにアクセスできます。私が理解している限り、特別な注意を払わずに、動的SQLを生成して実行するコードをデフォルトで使用すると、データベースは呼び出し元の権限で動的SQLを実行します。つまり、特権オブジェクトをまったく使用できないか、すべてのクライアントに公開する必要があり、特権データへの潜在的な攻撃の表面積が増加します。作成時にSP /関数を常に特定のユーザー(SQL ServerでEXECUTE AS)として実行するように設定すると、その問題は解決する場合がありますが、事態はさらに複雑になります。これは、動的SQLを非常に魅力的な攻撃ベクトルにすることにより、前のポイントで述べたSQLインジェクションのリスクを悪化させます。

  • 開発者がアプリケーションコードを変更したりバグを修正したりするためにアプリケーションコードが何をしているのかを理解する必要がある場合、実行されている正確なSQLクエリを取得するのは非常に困難です。SQLプロファイラーを使用できますが、これには特別な特権が必要であり、実稼働システムのパフォーマンスに悪影響を与える可能性があります。実行されたクエリはSPによってログに記録できますが、これは疑わしい利点(新しいテーブルの収容、古いデータのパージなど)のために複雑さを増し、まったく自明ではありません。実際、一部のアプリケーションは、開発者がデータベースの資格情報を持たないように設計されているため、送信されているクエリを実際に確認することはほとんど不可能になります。

  • 存在しないテーブルを選択しようとした場合など、エラーが発生すると、データベースから「無効なオブジェクト名」の行に沿ってメッセージが表示されます。これは、バックエンドでSQLを作成する場合でもデータベースで作成する場合でもまったく同じように発生しますが、違いは、システムのトラブルシューティングを試みている一部の貧しい開発者は、1つのレベルをさらに深く掘り下げて、問題が存在します。問題が何であるかを理解しようとするために、すべてを行うという不思議な手順を掘り下げます。ログには「GetWidgetのエラー」は表示されず、「OneProcedureToRuleThemAllRunnerのエラー」が表示されます。この抽象化は一般的にシステムを悪化させます。

パラメータに基づいてテーブル名を切り替える疑似C#の例:

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

これにより、考えられるすべての問題が解消されるわけではありませんが、他の手法で概説した欠陥は、この例にはありません。


4
私はそれに完全には同意しません。たとえば、この「移動」ボタンを押してから、フロアが存在するかどうかを確認するメカニズムがいくつかあります。関数はトリガーで使用でき、トリガーはいくつかの条件をチェックできます。この乾燥は最も美しいとは言えないかもしれませんが、システムがすでに十分に大きく、ロジックを修正する必要がある場合は、この選択はそれほど劇的ではないと思います。
John Doe 2012

1
ただし、存在しないボタンを押そうとするアクションは、どのように処理しても例外を生成するだけであると考えてください。存在しないボタンを実際に押すことはできないため、ボタンを押すだけでなく、存在しない番号をチェックするレイヤーを追加するメリットはありません。このような番号のエントリは、レイヤーを作成する前に存在していなかったためです。私の意見では、抽象化はプログラミングにおいて最も強力なツールです。ただし、既存の抽象化をほとんど複製しないレイヤーを追加するのは誤りです。データベース自体は、すでにデータセットに名前をマップする抽象化レイヤ。
ErikE 2013

3
スポット。SQLの要点は、抽出するデータのセットを表現することです。この関数が行う唯一のことは、「既定の」SQLステートメントをカプセル化することです。識別子もハードコーディングされているという事実を考えると、全体が悪臭を放っています。
Nick Hristov 2014年

1
@three誰かがスキル習得段階(スキル習得のDreyfusモデルを参照)になるまで、「動的SQLで使用されるプロシージャにテーブル名を渡さないでください」などのルールに完全に従う必要があります。それが必ずしも悪いとは限らないことをほのめかすことでさえ、それ自体が悪いアドバイスです。これを知っていると、初心者はそれを使いたくなるでしょう!それは良くないね。トピックのマスターだけがルールを破るべきです。なぜなら、そのようなルール違反が実際に意味があるかどうかを特定の場合に知る経験を持つのは彼らだけだからです。
ErikE 2015年

1
@ three-cupsなぜそれが悪い考えなのかについて、もっと詳しく更新しました。
ErikE 2016

10

plpgsqlコード内では、テーブル名または列が変数に由来するクエリには、EXECUTEステートメントを使用する必要があります。また、が動的に生成さIF EXISTS (<query>)れる場合、構成は許可されませんquery

両方の問題が修正された関数は次のとおりです。

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;

ありがとう、私はあなたの答えを読んだとき、ちょうど数分前に同じものを作っていました。唯一の違いはquote_ident()、引用符が追加されたために削除する必要があったことです。これは、ほとんどの例で使用されているため、少し驚いています。
John Doe

:テーブル名が外の文字が含まれている場合/場合は、これらの余分な引用符は、[AZ]、またはそれは予約済みの識別子(テーブル名として「グループ」の例)と衝突するとき/場合に必要となります
ダニエル・ベリテ

ちなみに、IF EXISTS <query>構成が存在しないことを証明するリンクを提供していただけませんか。私はそのようなものを実用的なコードサンプルとして見たと確信しています。
John Doe

1
@JohnDoe:plpgsqlでIF EXISTS (<query>) THEN ...完全に有効な構成です。の動的SQLではありません<query>。よく使います。また、この機能はかなり改善することができます。回答を投稿しました。
Erwin Brandstetter 2012年

1
申し訳ありませんが、あなたは正しいですif exists(<query>)、それは一般的な場合に有効です。それに応じて答えをチェックして修正しました。
ダニエルヴェライト2012年

4

最初のものは、あなたが意味する意味で実際には「機能」しません。エラーを生成しない限り、機能します。

を試してみるとSELECT * FROM quote_ident('table_that_does_not_exist');、関数が1を返す理由がわかります。selectは、1quote_identつの行(変数$1またはこの特定の場合table_that_does_not_exist)を持つ1つの列(named )を持つテーブルを返します。

あなたがしたいことは動的SQLを必要とします、それは実際にquote_*関数が使われることを意図されている場所です。


どうもありがとう、マット、table_that_does_not_exist同じ結果を出しました、あなたは正しいです。
John Doe 2012年

2

テーブルが空かどうか(id = 1)をテストすることが問題だった場合、Erwinのストアドプロシージャの簡略版を次に示します。

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

私はこれが古いスレッドであることを知っていますが、同じ問題を解決しようとしたときに最近遭遇しました-私の場合、いくつかのかなり複雑なスクリプトについて。

スクリプト全体を動的SQLに変換することは理想的ではありません。これは面倒でエラーが発生しやすい作業であり、パラメーター化する機能が失われます。パラメーターはSQLの定数に補間する必要があり、パフォーマンスとセキュリティに悪影響を及ぼします。

テーブルから選択するだけでよい場合にSQLをそのまま維持できる簡単なトリックを次に示します。動的SQLを使用して一時ビューを作成します。

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

テーブル名、列名、および値を動的に渡してパラメーターとして機能させる場合

このコードを使用する

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

私は9.4バージョンのPostgreSQLを使用しており、常に次のコードを使用しています。

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

その後:

SELECT add_new_table('my_table_name');

それは私にとってはうまくいきます。

注意!上記の例は、「データベースのクエリ中に安全性を維持したい場合はどうすればよいか」を示すものの1つです:P


1
newテーブルの作成は、既存のテーブルの名前での操作とは異なります。いずれにせよ、コードとして実行されるテキストパラメータをエスケープするか、SQLインジェクションを受け入れる必要があります。
Erwin Brandstetter 2015年

そうそう、私の間違い。そのトピックは私を誤解させ、さらに私はそれを最後まで読んでいませんでした。通常私の場合。:Pテキストパラメータを持つコードがインジェクションにさらされるのはなぜですか?
dm3 2015年

おっと、それは本当に危険です。答えてくれてありがとう!
dm3 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.