错误更新带有触发器的非空约束的行。

huangapple go评论62阅读模式
英文:

Error updating row with trigger not-null constraint

问题

I've translated the provided text, excluding the code part:

我正在使用触发器来更新名为tb_sign_employees的表,其结构如下:

sign_id					INT NOT NULL,
employee_id				INT NOT NULL,
sign_term_in				CHARACTER(3) NOT NULL,
sign_data_in				TIMESTAMP NOT NULL,
sign_term_out				CHARACTER(3),
sign_data_out				TIMESTAMP,
	sign_time_minutes			INT,

主键是sign_id。
我需要使用一个名为tg_sign_inserted()的触发器,这样当我插入employee_id和sign_term_in时,它会自动插入一个新的sign_id,该sign_id是最大sign_id加1,sign_data_in是当前时间戳,其余字段为空。然后,如果我插入employee_id和sign_term_out,它应该更新sign_term_out为我输入的值,sign_data_out为当前时间戳,并将sign_time_minutes设置为sign_data_out减去sign_data_in的分钟数。
此外,如果您之前已经进行了签入但没有更新值,就不能再次进行签入。如果您已经签出,就不能再次签出,无法在插入时使用sign_tern_in和sign_term_out。
我已经尝试过以下代码:

(以下是代码部分)

当我使用以下代码时:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_in)
VALUES(1, 'Terminal1')

它正常工作,但如果之后我使用以下代码:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_out)
VALUES(1, 'Terminal1')

它不是更新之前命令生成的行,而是尝试创建一个新行,该行如下所示:

(null, 1, null, null, EP, null, null)

导致违反sign_id列的非空约束。它不仅没有更新列,而且尝试创建包含null而不是插入当前时间戳的sing_data_out列的新列。
我看不到错误。
为了给您一个应该如何行为的示例,假设最大的sign_id是1。
然后,使用2023-03-06 09:00:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_in)
VALUES(1, 'Terminal1')

应该创建以下行:

2, 1, Terminal1, 2023-03-06 09:14:00, null, null, null

如果我在同一天12:00执行以下操作:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_out)
VALUES(1, 'Terminal2')
2, 1, Terminal1, 2023-03-06 09:00:00, Terminal2, 2023-03-06 12:00:00, 180

其中180是12点和9点之间的分钟差异。
感谢您的提前回复。

英文:

I'm using a trigger to update a tabble named tb_sign_employees with the following structure:

sign_id					INT NOT NULL,
employee_id				INT NOT NULL,
sign_term_in				CHARACTER(3) NOT NULL,
sign_data_in				TIMESTAMP NOT NULL,
sign_term_out				CHARACTER(3),
sign_data_out				TIMESTAMP,
	sign_time_minutes			INT,

The primary key is sign_id.
I need to use a trigger tg_sign_inserted()so when I insert the employee_id and the sign_term_in it automaticaly inserts a new sign_id which is one plus the greatest sign_id and the sign_data_in which is the current timestamp, leaving the rest as null. Then whith the same function if I insert an employee_id and the sign_term_out it should update the sign_term_out with the one i entered, the sign_data_out with the current timestamp and the sign_time_minutes as the sign_data_out minus the sign_data_in in minutes.
Also you can't insert if you already had done it before but not updated the values, if you check in but don't check out is imposible to check in again. And if you have already checked out it's not possible to check out, you can't use sign_tern_in and sign_term_out while inserting.
I have tried with:

CREATE OR REPLACE FUNCTION erp.fn_check_sign_inserted_values(new_sign erp.tb_sign_employees)
RETURNS BOOLEAN AS $$
BEGIN
    IF new_sign.sign_term_in IS NULL AND new_sign.sign_term_out IS NULL THEN
        RAISE EXCEPTION 'Cal informar de sign_term_in o sign_term_out';
    END IF;

    IF new_sign.sign_term_in IS NOT NULL AND new_sign.sign_term_out IS NOT NULL THEN
        RAISE EXCEPTION 'Cal informar de sign_term_in o sign_term_out, no els dos';
    END IF;

    IF new_sign.sign_term_in IS NOT NULL THEN
        IF new_sign.employee_id IS NULL THEN
            RAISE EXCEPTION 'Cal informar del id del empleat';
        END IF;

        IF EXISTS (
            SELECT 1
            FROM erp.tb_sign_employees
            WHERE employee_id = new_sign.employee_id
            AND sign_term_out IS NULL
        ) THEN
            RAISE EXCEPTION E'Ja hi ha un registre d''entrada sense sortida';
        END IF;
    END IF;

    IF new_sign.sign_term_out IS NOT NULL THEN
        IF new_sign.employee_id IS NULL THEN
            RAISE EXCEPTION 'Cal informar del id del empleat';
        END IF;

        IF NOT EXISTS (
            SELECT 1
            FROM erp.tb_sign_employees
            WHERE employee_id = new_sign.employee_id
            AND sign_term_out IS NULL
        ) THEN
            RAISE EXCEPTION E'Ja hi ha un registre d''entrada sense sortida';
        END IF;
    END IF;

    RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION erp.tg_sign_inserted()
RETURNS TRIGGER AS $$
BEGIN
    IF NOT erp.fn_check_sign_inserted_values(NEW) THEN
        RETURN NULL;
    END IF;

    IF NEW.sign_term_in IS NOT NULL THEN
        INSERT INTO erp.tb_sign_employees (sign_id, employee_id, sign_term_in, sign_data_in)
        SELECT COALESCE(MAX(sign_id), 0) + 1, NEW.employee_id, NEW.sign_term_in, current_timestamp
        FROM erp.tb_sign_employees;
    ELSIF NEW.sign_term_out IS NOT NULL THEN
        UPDATE erp.tb_sign_employees
        SET sign_term_out = NEW.sign_term_out,
            sign_data_out = current_timestamp,
            sign_time_minutes = extract(epoch from current_timestamp - sign_data_in) / 60
        WHERE employee_id = NEW.employee_id
        AND sign_term_out IS NULL
        AND sign_term_in IS NOT NULL; -- Agregamos esta condición para actualizar solo las filas con sign_term_in ya definido.
    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER tg_insert_sign_employee
BEFORE INSERT ON erp.tb_sign_employees
FOR EACH ROW
EXECUTE FUNCTION erp.tg_sign_inserted()

And when I use:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_in)
VALUES(1, 'Terminal1')

It works just file but if then i use:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_out)
VALUES(1, 'Terminal1')

Instead of filling the row generated by the prior command it tries to create a new row that would look like this:
(null, 1, null, null, EP, null, null)
Resulting in a violation of the not-null constraint in the column sign_id. The fact that it doesn't update the column is an error but not the only one since the column that is trying to create contains null instead of inserting the current timestamp in sing_data_out.
I can't see the error.
To give you an example of how it should behave imagine that the greatest sign_id is one.
Then using the day 2023-03-06 at 9 o'clock:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_in)
VALUES(1, 'Terminal1')

Should create the following line:

2, 1, Terminal1, 2023-03-06 09:14:00, null, null, null

And if I executed the following the same day at 12 o'clock:

INSERT INTO erp.tb_sign_employees(employee_id, sign_term_out)
VALUES(1, 'Terminal2')
2, 1, Terminal1, 2023-03-06 09:00:00, Terminal2, 2023-03-06 12:00:00, 180

Where 180 is the difference in minutes between 12 and 9 o'clock.
Thanks in advance.

答案1

得分: 0

以下是翻译的内容:

你的触发器存在一些重大问题。如上所述,按照现在的编写方式,它将不会像声明的那样插入初始行。你的触发器在表 erp.tb_sign_employees 上的 insert 时触发。然后在触发器内部,它尝试执行相同的操作:

IF NEW.sign_term_in IS NOT NULL THEN
    INSERT INTO erp.tb_sign_employees (sign_id, employee_id, sign_term_in, sign_data_in)
    SELECT COALESCE(MAX(sign_id), 0) + 1, NEW.employee_id, NEW.sign_term_in, current_timestamp
    FROM erp.tb_sign_employees;

结果触发器再次触发,由于列 NEW.sign_term_in 仍然为 null,触发器会一次又一次地触发。这会创建一个无限循环,最终导致异常:

SQL 错误 [54001]: ERROR: stack depth limit exceeded ...;

在触发器中不应该执行与触发器本身触发的语句相同的 DML 语句。在这种情况下,这是不必要的。你可以直接赋值或者选择到该列,像这样:

select coalesce(max(sign_id), 0) + 1
  into new.sign_id
  from tb_sign_employees;

现在我们来解决第二个问题。上述做法在多用户环境中使用 MAX+1 赋值是极差的做法,几乎可以保证会出现重复。当两个用户尝试插入一行并且第二个用户在第一个用户提交之前执行时,它们会得到相同的 MAX 值,从而导致重复。<br/><br/>

现在让我们回到实际引发你问题的问题。你说 它不更新列的事实是一个错误...。这是错误的,它确实正确更新了。问题在于你实际上将一个 insert 更改为了 update。然而,Postgres 不会识别这种更改,所以当你之后执行 return new 时,它会继续处理插入。由于列 sign_in 没有被赋值且不是自动生成的,因此它为 NULL,因此你会得到 null 值异常,插入被中止。不幸的是,更新也被中止(回滚),所以看起来似乎没有被执行。正确处理此问题的方法是只发出一个 update 而不是第二个 insert。但有一个非常规的解决方法。在更新后不要发出 return new,而是发出:

return null;

返回 null 将终止进一步处理插入,但会保留已更新的行。(如果进行行计数,被终止的插入不会计为已处理的行。)考虑到所有这些,你的触发器函数变成了:

create or replace function tg_sign_inserted()
  returns trigger 
 language plpgsql
as $$ 
begin

    if not fn_check_sign_inserted_values(new) then
        return null;
    end if;

    if new.sign_term_in is not null then
        select coalesce(max(sign_id), 0) + 1
          into new.sign_id
          from tb_sign_employees ;
        return new;
    
    elsif new.sign_term_out is not null then

        update tb_sign_employees
        set sign_term_out = new.sign_term_out,
            sign_data_out = current_timestamp,
            sign_time_minutes = extract(epoch from current_timestamp - sign_data_in) / 60
        where employee_id = new.employee_id
        and sign_term_out is null
        and sign_term_in is not null; -- 添加此条件以仅更新具有已定义 sign_term_in 的行。
        
        return null; -- 告诉 Postgres 停止处理插入
    end if;

end;
$$;

请参见 此处的演示。注意:为了演示,我已经硬编码了你提到的时间戳,并修正了以下问题。<br/>

最后,即使没有提到的问题,你的初始插入也会失败,原因有两个附加的问题,可能只是拼写错误。1. 列 sign_term_insign_term_out 被定义为 CHARACTER(3),但你想插入值 Terminal1Terminal2。这些值将引发异常,因为它们不符合分配的字段限制。2. 列 sign_data_in 被定义为非 null,但没有默认值且没有在 insert 中引用。因此,它将引发 null 值异常。<br/><br/>

你说你的 要求是不修改结构。但要插入指定的值,你 必须 修改结构。当你与给你命令的人讨论时,让他们知道 MAX+1 对于分配 sign_id 有多糟糕,并让它们进行修改。对两者进行更改,很可能可以完全消除触发器。

英文:

Your trigger has a couple major problems. As written above it will not insert the initial row as claimed. Your trigger fires on an insert to table erp.tb_sign_employees. Then within the trigger it attempts to do the same:

IF NEW.sign_term_in IS NOT NULL THEN
        INSERT INTO erp.tb_sign_employees (sign_id, employee_id, sign_term_in, sign_data_in)
        SELECT COALESCE(MAX(sign_id), 0) + 1, NEW.employee_id, NEW.sign_term_in, current_timestamp
        FROM erp.tb_sign_employees;

As a result the trigger fires again and since the column NEW.sign_term_in is still null the trigger fires yet again and again. It creates an infinite loop which eventually results in the exception:

> SQL Error [54001]: ERROR: stack depth limit exceeded ...;

You should not issue a DML statement in a trigger that is the same as the statement that fires the trigger in the first place. In this case it is not necessary. You can make a direct assignment or select into to the column so:

select coalesce(max(sign_id), 0) + 1
  into new.sign_id
  from tb_sign_employees;

Now we arrive at the second issue. The above is extremely poor practice as MAX+1 assignment in a multiple user environment is a virtual guarantee to get a duplicate. It will occur when two users attempt to insert a row and the second user executes before the first user commits because each will get the same MAX value. <br/>
<br/>
Now to the issue that actually initiated your question. You state fact that it doesn't update the column is an error .... That is wrong, it does the update correctly. The problem is you effectively changed an insert into an update. However, Postgres does not recognize that change so when you when afterward return new it continues processing the insert. Since the column sign_in has not been assigned a value and is not auto generated it is NULL and therefore you get the null value exception and the insert is aborted. Unfortunately, the update is also aborted (rolled back) so it appears to have not been done. The correct way to handle this is to just issue an update instead of the second insert. However there is an unconventional work around. After the update do not issue return new instead issue

return null;

Returning null terminates further processing on the insert, but will leave the updated row. (Row counts, if taken will be off, a terminated insert does not count as processed row.) Taking all this into account your trigger function becomes:

create or replace function tg_sign_inserted()
  returns trigger 
 language plpgsql
as $$ 
begin

    if not fn_check_sign_inserted_values(new) then
        return null;
    end if;

    if new.sign_term_in is not null then
        select coalesce(max(sign_id), 0) + 1
          into new.sign_id
          from tb_sign_employees ;
        return new;
    
    elsif new.sign_term_out is not null then

        update tb_sign_employees
        set sign_term_out = new.sign_term_out,
            sign_data_out = current_timestamp,
            sign_time_minutes = extract(epoch from current_timestamp - sign_data_in) / 60
        where employee_id = new.employee_id
        and sign_term_out is null
        and sign_term_in is not null; -- agregamos esta condici&#243;n para actualizar solo las filas con sign_term_in ya definido.
        
        return null; -- tell Postgres to stop Processing the Insert
    end if;

end;
$$;

See demo here. NOTE: For demo I have hard coded the timestamps you mentioned and corrected for the following. <br/>
Finally, your initial insert would fail even without the mentioned issues for 2 additional reasons, perhaps just typos. 1. The columns sign_term_in and sign_term_out are defined as CHARACTER(3), but you want to insert the values Terminal1 and Terminal2. Those values will cause an exception as they will not fit in the allotted field limit. 2. The column sign_data_in is defined as not null, but no default given and not referenced on insert. Therefore it would raise a null value exception. <br/>
<br/>
You state your orders are to not modify the structure. Well to insert the values indicated you must modify the structure. When you discuss that with those giving your orders know let them how bad the MAX+1 is for assigning the sign_id and get it changed as well. Change both and you most likely can eliminate the trigger all together.

huangapple
  • 本文由 发表于 2023年5月30日 02:05:23
  • 转载请务必保留本文链接:https://go.coder-hub.com/76359478.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定