I am new to Postgresql, so please indulge if my question makes no sense.
I am trying to find a way to migrate my DB structure to Postgresql, in particular, I find functions quite convenient and would like to make my numerous triggers easier to write.
In my DB, I use the standard last_modified
and last_modified_by
fields to keep track of changes. I also use a standard primary key with incremental sequence.
There is built-in syntax to link sequences to primary key id's, but as I have to write triggers for the last_modified
fields anyway, I was wondering if it was possible to have a generic function to update all at once.
Example:
Table ANIMAL
has fields AMIMAL_ID
(primary key, with sequence SEQ_ANIMAL
), fields LAST_MODIFIED
and LAST_MODIFIED_BY
.
Similarly, I have a table PLANT
with fields PLANT_ID
(primary key, with sequence SEQ_PLANT
), fields LAST_MODIFIED
and LAST_MODIFIED_BY
again.
I would like to create a generic function to be called in the 4 triggers I need to create. I was hoping to get something like this:
Before insert function:
CREATE FUNCTION TRIGGER_BI(p_pkField text, p_Sequence text) RETURNS TRIGGER AS $$
DECLARE
curtime timestamp := now();
BEGIN
NEW.LAST_UPDATED := curtime;
NEW.LAST_UPDATED_BY := current_user;
NEW.p_pkField := nextval(p_Sequence);
END;
$$
LANGUAGE 'plpgsql' IMMUTABLE
SECURITY DEFINER;
Before update function:
CREATE FUNCTION TRIGGER_BU() RETURNS TRIGGER AS $$
DECLARE
curtime timestamp := now();
BEGIN
NEW.LAST_UPDATED := curtime;
NEW.LAST_UPDATED_BY := current_user;
END;
$$
LANGUAGE 'plpgsql' IMMUTABLE
SECURITY DEFINER;
And now, the triggers of table ANIMAL
:
Before Insert:
CREATE TRIGGER ANIMAL
BEFORE INSERT
FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BI("ANIMAL_ID", "SEQ_ANIMAL");
Before Update:
CREATE TRIGGER ANIMAL
BEFORE UPDATE
FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BU();
Triggers of table PLANT
:
Before Insert:
CREATE TRIGGER PLANT
BEFORE INSERT
FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BI("PLANT_ID", "SEQ_PLANT");
Before Update:
CREATE TRIGGER PLANT
BEFORE UPDATE
FOR EACH ROW EXECUTE PROCEDURE TRIGGER_BU();
Is it possible, in any way, to get something generic this way?
YES! What is the correct syntax? BONUS: it is event possible to have a single function to do all the work, with default empty parameters that, if empty, would not update the sequence.
YES, BUT WAIT! what are the down-sides of such an approach? (performance, security, anything else to be considered)?
NO! So I really need on function for each trigger?
UPDATE: I explicitly create sequences because I may want to have shared sequences among several tables. The idea is to use the shared sequence as a unique parent table with several child tables having a foreign key on their primary key to the parent table. Don't hesitate to comment on this approach, but my basic understanding is that accessing a sequence's next value is much more efficient than having to manage foreign keys...
UPDATE 2:
I found some quite interesting stuff which nearly gets me there - only that my setValue
function isn't working...
Here the generic trigger:
CREATE OR REPLACE FUNCTION TRIGGER_FUNC() RETURNS TRIGGER AS $$
DECLARE
p_pkField text;
p_Sequence text;
pkValue int;
BEGIN
EXECUTE format('SELECT ($1).%I::int', TG_ARGV[0]) USING NEW INTO pkValue;
p_Sequence := quote_ident(TG_ARGV[1]);
IF pkValue IS NULL THEN
SELECT setfieldValue(pg_typeof(NEW), TG_ARGV[0], nextval(p_Sequence));
END IF;
NEW.LAST_UPDATED := curtime;
NEW.LAST_UPDATED_BY := current_user;
RETURN NEW;
END;
$$
LANGUAGE 'plpgsql' IMMUTABLE
SECURITY DEFINER;
I found a hint to the solution of the setValue
function here and tried adapting it, but it doesn't work - am I simply using a wrong call? Or can I use some additional knowledge in the method to make it simpler? (I already used the fact that I am setting a bigint
value, but I might be able to do better?!)
Here the (non-working) code:
CREATE OR REPLACE FUNCTION public.setfieldValue(anyelement, text, bigint)
RETURNS anyelement
LANGUAGE plpgsql
AS $function$
DECLARE
_name text;
_values text[];
_value text;
_attnum int;
BEGIN
FOR _name, _attnum
IN SELECT a.attname, a.attnum
FROM pg_catalog.pg_attribute a
WHERE a.attrelid = (SELECT typrelid
FROM pg_type
WHERE oid = pg_typeof($1)::oid)
LOOP
IF _name = $2 THEN
_value := $3;
ELSE
EXECUTE 'SELECT (($1).' || quote_ident(_name) || ')::text' INTO _value USING $1;
END IF;
_values[_attnum] := COALESCE('"' || replace(replace(_value, '"', '""'), '''', '''''') || '"', '');
END LOOP;
EXECUTE 'SELECT (' || quote_ident(pg_typeof($1)::text) || ' ''(' || array_to_string(_values,',') || ')'').*' INTO $1;
RETURN $1;
END;
$function$;