web-dev-qa-db-ja.com

PL / pgSQL関数をリファクタリングして、さまざまなSELECTクエリの出力を返す

テキスト形式で整形式のPostgreSQL SELECTクエリを出力する関数を書きました。これで、テキストを出力したくなくなりましたが、実際には、生成されたSELECTステートメントをデータベースに対して実行し、クエリ自体と同じように結果を返します。

これまでのところ:

CREATE OR REPLACE FUNCTION data_of(integer)
  RETURNS text AS
$BODY$
DECLARE
   sensors varchar(100);   -- holds list of column names
   type    varchar(100);   -- holds name of table
   result  text;           -- holds SQL query
       -- declare more variables

BEGIN
      -- do some crazy stuff

      result := 'SELECT\r\nDatahora,' || sensors ||
      '\r\n\r\nFROM\r\n' || type ||
      '\r\n\r\nWHERE\r\id=' || $1 ||'\r\n\r\nORDER BY Datahora;';

      RETURN result;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION data_of(integer) OWNER TO postgres;

sensorsは、テーブルtypeの列名のリストを保持します。それらは関数の過程で宣言され、埋められます。最終的には、次のような値を保持します。

  • sensors'column1, column2, column3'
    Datahoratimestamp)を除いて、すべての列のタイプはdouble precision

  • type'myTable'
    4つのテーブルのいずれかの名前にすることができます。共通の列Datahoraを除いて、それぞれに異なる列があります。

基になるテーブルの定義

変数sensorsには、ここに表示されるtypeの対応するテーブルのall列が保持されます。例:typepcdmetの場合、sensors'datahora,dirvento,precipitacao,pressaoatm,radsolacum,tempar,umidrel,velvento'

変数は、SELECTに格納されるresultステートメントを作成するために使用されます。お気に入り:

SELECT Datahora, column1, column2, column3
FROM   myTable
WHERE  id=20
ORDER  BY Datahora;

現在、関数はこのステートメントをtextとして返します。コピーして貼り付け、pgAdminまたはpsqlで実行します。これを自動化し、クエリを自動的に実行して結果を返したいのですが。どうやってやるの?

33
waldyr.ar

動的SQLおよびRETURNタイプ

(最後に最後まで保存しました。読み続けてください!)
実行したい動的SQL。原則として、それはplpgsqlでは EXECUTE の助けを借りて簡単です。カーソルは必要ありません。実際、ほとんどの場合、明示的なカーソルがない方がよいでしょう。
SO検索あり の例を検索します。

遭遇する問題:まだ定義されていないタイプのレコードを返すが必要です。関数は、戻り値の型を RETURNS 句で(またはOUTまたはINOUTパラメーターで)宣言する必要があります。あなたの場合、numbernamestypesであるため、匿名レコードにフォールバックする必要があります返される列は異なります。お気に入り:

_CREATE FUNCTION data_of(integer)
  RETURNS SETOF record AS ...
_

ただし、これは特に有用ではありません。この方法では、関数のすべての呼び出しで列定義リストを提供する必要があります。お気に入り:

_SELECT * FROM data_of(17)
AS foo (colum_name1 integer
      , colum_name2 text
      , colum_name3 real);
_

しかし、事前に列がわからない場合はどうすればよいでしょうか。
jsonjsonbhstorexmlなどの構造化されていないドキュメントデータタイプを使用することができます。

しかし、この質問の目的のために、可能な限り個々の正しく型付けされた名前付きの列を返したいとしましょう。

固定戻り値型のシンプルなソリューション

datahoraは指定されているようです。データ型timestampを想定し、名前とデータ型が異なる2つ以上の列が常に存在すると仮定します。

Names戻り値の型では総称名を使用することをやめます。
Typesも放棄して、すべてをtextにキャストします。これは、everyデータ型がtext

_CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, col2 text, col3 text) AS
$func$
DECLARE
   _sensors text := 'col1::text, col2::text';  -- cast each col to text
   _type    text := 'foo';
BEGIN
   RETURN QUERY EXECUTE '
      SELECT datahora, ' || _sensors || '
      FROM   ' || quote_ident(_type) || '
      WHERE  id = $1
      ORDER  BY datahora'
   USING  _id;

END
$func$ LANGUAGE plpgsql;
_

これはどのように作動しますか?

  • 代わりに、変数__sensors_および__type_を入力パラメーターにすることができます。

  • _RETURNS TABLE_ 句に注意してください。

  • _RETURN QUERY EXECUTE_ の使用に注意してください。これは、動的クエリから行を返す最も洗練された方法の1つです。

  • _RETURN QUERY EXECUTE_のUSING句をわかりやすくするために、関数パラメーターの名前を使用しています。 SQL文字列内の_$1_は、関数パラメーターではなく、USING句で渡された値を参照します。 (この単純な例では、どちらもたまたまそれぞれのスコープで_$1_です。)

  • __sensors_の値の例に注意してください。各列はtext型にキャストされます。

  • この種のコードは SQLインジェクション に対して非常に脆弱です。私は quote_ident() を使用してそれから保護しています。変数__sensors_にいくつかの列名をまとめると、quote_ident()を使用できなくなります(通常、これは悪い考えです!)。たとえば、代わりにquote_ident()を使用して列名を個別に実行するなど、他の方法で問題が発生しないことを確認してください。 VARIADICパラメータが思い浮かびます...

PostgreSQL 9.1以降でよりシンプルに

バージョン9.1以降では format() を使用してさらに簡略化できます。

_RETURN QUERY EXECUTE format('
   SELECT datahora, %s  -- identifier passed as unescaped string
   FROM   %I            -- assuming the name is provided by user
   WHERE  id = $1
   ORDER  BY datahora'
  ,_sensors, _type)
USING  _id;
_

繰り返しますが、個々の列名は適切にエスケープでき、クリーンな方法になります。

同じ型を共有する可変数の列

質問が更新されると、戻り値の型が

  • 列の変数number
  • ただし、同じtype_double precision_のすべての列(エイリアス_float8_)

関数のRETURN型を定義する必要があるので、この場合、可変数の値を保持できるARRAY型に頼ります。さらに、列名を含む配列を返すので、結果から名前を解析することもできます。

_CREATE OR REPLACE FUNCTION data_of(_id integer)
  RETURNS TABLE (datahora timestamp, names text[], values float8[] ) AS
$func$
DECLARE
   _sensors text := 'col1, col2, col3';  -- plain list of column names
   _type    text := 'foo';
BEGIN
   RETURN QUERY EXECUTE format('
      SELECT datahora
           , string_to_array($1)  -- AS names
           , ARRAY[%s]            -- AS values
      FROM   %s
      WHERE  id = $2
      ORDER  BY datahora'
    , _sensors, _type)
   USING  _sensors, _id;
END
$func$  LANGUAGE plpgsql;
_


さまざまな完全なテーブルタイプ

実際にテーブルのすべての列を返そうとしている場合(たとえば、リンクされたページの テーブルの1つ の場合)、このシンプルで非常に強力なソリューションを-で使用します 多相型

_CREATE OR REPLACE FUNCTION data_of(_tbl_type anyelement, _id int)
  RETURNS SETOF anyelement AS
$func$
BEGIN
   RETURN QUERY EXECUTE format('
      SELECT *
      FROM   %s  -- pg_typeof returns regtype, quoted automatically
      WHERE  id = $1
      ORDER  BY datahora'
    , pg_typeof(_tbl_type))
   USING  _id;
END
$func$ LANGUAGE plpgsql;
_

コール:

_SELECT * FROM data_of(NULL::pcdmet, 17);
_

呼び出しのpcdmetを他のテーブル名に置き換えます。

これはどのように作動しますか?

  • anyelementは、疑似データ型、ポリモーフィック型、非配列データ型のプレースホルダーです。関数内のすべてのanyelementは、実行時に提供される同じ型に評価されます。定義された型の値を関数の引数として指定することで、戻り値の型を暗黙的に定義します。

  • PostgreSQLは、作成されたすべてのテーブルの行タイプ(複合データタイプ)を自動的に定義するため、すべてのテーブルに明確に定義されたタイプがあります。これには、一時的なテーブルが含まれており、臨時の使用に便利です。

  • どの型もNULLにすることができます。したがって、NULL値を渡して、テーブルタイプにキャストします。

  • これで、関数は明確に定義された行タイプを返し、SELECT * FROM data_of(...)を使用して行を分解し、個々の列を取得できます。

  • pg_typeof(_tbl_type) テーブルの名前を object identifier type regtype として返します。自動的にtextに変換される場合、識別子は必要に応じて自動的に二重引用符で囲まれ、スキーマ修飾されますになります。したがって、SQLインジェクションは不可能です。これは、スキーマ修飾されたテーブル名 quote_ident()が失敗する場合 にも対応できます。

70

おそらく cursor を返したいでしょう。このようなものを試してください(私は試していません):

CREATE OR REPLACE FUNCTION data_of(integer)
  RETURNS refcursor AS
$BODY$
DECLARE
      --Declaring variables
      ref refcursor;
BEGIN
      -- make sure `sensors`, `type`, $1 variable has valid value
      OPEN ref FOR 'SELECT Datahora,' || sensors ||
      ' FROM ' || type ||
      ' WHERE nomepcd=' || $1 ||' ORDER BY Datahora;';
      RETURN ref;
END;
$BODY$
LANGUAGE 'plpgsql' VOLATILE;
ALTER FUNCTION data_of(integer) OWNER TO postgres;
3
bpgergo

申し訳ありませんが、質問は非常に不明確です。ただし、以下に、カーソル変数を返す関数を作成して使用する方法の自己完結型の例を示します。それが役に立てば幸い !

begin;

create table test (id serial, data1 text, data2 text);

insert into test(data1, data2) values('one', 'un');
insert into test(data1, data2) values('two', 'deux');
insert into test(data1, data2) values('three', 'trois');

create function generate_query(query_name refcursor, columns text[])
returns refcursor 
as $$
begin
  open query_name for execute 
    'select id, ' || array_to_string(columns, ',') || ' from test order by id';
  return query_name;
end;
$$ language plpgsql;

select generate_query('english', array['data1']);
fetch all in english;

select generate_query('french', array['data2']);
fetch all in french;
move absolute 0 from french; -- do it again !
fetch all in french;

select generate_query('all_langs', array['data1','data2']);
fetch all in all_langs;

-- this will raise in runtime as there is no data3 column in the test table
select generate_query('broken', array['data3']);

rollback;
1
user272735
# copy paste me into bash Shell directly
clear; IFS='' read -r -d '' sql_code << 'EOF_SQL_CODE'
CREATE OR REPLACE FUNCTION func_get_all_users_roles()
  -- define the return type of the result set as table
  -- those datatypes must match the ones in the src
  RETURNS TABLE (
                 id           bigint
               , email        varchar(200)
               , password     varchar(200)
               , roles        varchar(100)) AS
$func$
BEGIN
   RETURN QUERY 
   -- start the select clause
   SELECT users.id, users.email, users.password, roles.name as roles
   FROM user_roles
   LEFT JOIN roles ON (roles.guid = user_roles.roles_guid)
   LEFT JOIN users ON (users.guid = user_roles.users_guid)
   -- stop the select clause
;
END
$func$  LANGUAGE plpgsql;
EOF_SQL_CODE
# create the function
psql -d db_name -c "$sql_code"; 

# call the function 
psql -d db_name -c "select * from func_get_all_users_roles() "
0
Yordan Georgiev