PostgreSQL 9.2 - 删除主表中的记录

时间:2013-05-03 07:55:07

标签: postgresql

首先,抱歉我的英语不好。

我正在创建一个使用PostgreSQL 9.2的新应用程序。 我正在尝试使用Firebird中使用的相同“逻辑”,但显然在PostgreSQL上不起作用。

我有一张名为“Albaran”的桌子和其他名为“AlbaMov”的细节表。我已经定义了一些具有相应职责的触发器,当您在Detail表中修改记录时,Master表会更新。除非我想删除主表中的记录,否则一切都很完美。

删除主表中的记录时,删除详细信息中的所有记录,然后在主表中将字段“总计”更新为0,但不删除主表记录。如果我从主表中删除没有记录的记录,详细信息表将被顺利删除。

我一直在测试并且已经看到问题出现在Master表的UPDATE中是在我称之为CalculoAlbaranVenta的函数中完成的。

同样的系统在Firebird中完美运行。

此函数返回一个%ROWTYPE类型的变量,我用它来更新PHP屏幕。

这里我留下了带有触发器和函数的表的定义。

问题出在哪里?

提前问候和感谢。

CREATE OR REPLACE FUNCTION public."CalculoAlbaranVenta"
(
  IN  "cSerie"      public."Serie",
  IN  "nNumeroDoc"  public."NumeroDocumento"
)
RETURNS SETOF public."Totales" AS
$$
declare nBasImp "Importes";
declare nIva "Importes";
declare nRE "Importes";
declare nTotalBase "Importes";
declare nTotalIVA "Importes";
declare nTotalRE "Importes";
declare nTotalDtoBase "Importes";
declare nTotalDtoResto "Importes";
declare nTotalDtos "Importes";
declare nTotalLinea "Importes";
declare rRow RECORD;
declare rTotales "Totales"%ROWTYPE;

begin
  nBasImp        := 0;
  nIva           := 0;
  nRE            := 0;
  nTotalBase     := 0;
  nTotalIVA      := 0;
  nTotalRE       := 0;
  nTotalDtoBase  := 0;
  nTotalDtoResto := 0;
  nTotalDtos     := 0;
  nTotalLinea    := 0;

  FOR rRow IN SELECT "TotalUnidades", 
                     "Precio", 
                     "PorcentajeIVA", 
                     "PorcentajeRE", 
                     "DescuentoBase", 
                     "DescuentoResto"
              FROM "AlbaMov"
              WHERE ("Serie" = "cSerie") AND ("NumeroDoc" = "nNumeroDoc") AND
                    ("Referencia" IS NOT NULL)
  LOOP
    nTotalLinea    := Round((rRow."TotalUnidades" * rRow."Precio")::numeric, 3);
    nTotalDtoBase  := Round((nTotalLinea * (rRow."DescuentoBase" / 100))::numeric, 3);
    nTotalLinea    := nTotalLinea - nTotalDtoBase; 
    nTotalDtoResto := Round((nTotalLinea * (rRow."DescuentoResto" / 100))::numeric, 3);
    nTotalLinea    := nTotalLinea - nTotalDtoResto;
    nTotalDtos     := nTotalDtos + nTotalDtoBase + nTotalDtoResto; 

    nBasImp := Round(nTotalLinea::numeric, 2);

    nTotalBase := nTotalBase + nBasImp;
    nTotalIVA  := nTotalIVA  + (nBasImp * rRow."PorcentajeIVA" / 100);
    nTotalRE   := nTotalRE   + (nBasImp * rRow."PorcentajeRE" / 100);

  END LOOP;

  nTotalIVA  := Round(nTotalIVA::numeric, 2);
  nTotalRE   := Round(nTotalRE::numeric, 2);
  nTotalDtos := Round(nTotalDtos::numeric, 2);

  UPDATE "Albaran"
  SET "BaseImponible" = nTotalBase,
      "TotalDescuentos" = nTotalDtos,
      "IVA" = nTotalIVA,
      "RE" = nTotalRE,
      "Total" = nTotalBase + nTotalIVA + nTotalRE
  WHERE ("Serie" = "cSerie") AND ("NumeroDoc" = "nNumeroDoc");

  rTotales."TotalDescuentos" := nTotalDtos;
  rTotales."BaseImponible"   := nTotalBase;
  rTotales."TotalIVA"        := nTotalIVA;
  rTotales."TotalRE"         := nTotalRE;
  rTotales."Total"           := nTotalBase + nTotalIVA + nTotalRE;

  RETURN NEXT rTotales;

end
$$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 1;

CREATE OR REPLACE FUNCTION public."AlbaranBeforeDelete"()
RETURNS trigger AS
$$
begin
  DELETE FROM "AlbaMov"
  WHERE ("Serie" = OLD."Serie") AND ("NumeroDoc" = OLD."NumeroDoc");

  RETURN OLD;
end
$$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;

CREATE OR REPLACE FUNCTION public."AlbaranBeforeUpdate"()
RETURNS trigger AS
$$
begin

  NEW."Total" := Round((NEW."BaseImponible" + NEW."IVA" + NEW."RE")::numeric, 2);

  RETURN NEW;

end
$$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;

CREATE OR REPLACE FUNCTION public."AlbaMovAfterDelete"()
RETURNS trigger AS
$$
declare nTotalBase "Importes";
declare nTotalIVA "Importes";
declare nTotalRE "Importes";
declare nTotalDtoBase "Importes";
declare nTotalDtoResto "Importes";
declare nTotalDtos "Importes";
declare nTotalLinea "Importes";
declare cCliente "CodigoCliente";

begin
  PERFORM "CalculoAlbaranVenta"(OLD."Serie", OLD."NumeroDoc");

  nTotalLinea    := Round((OLD."TotalUnidades" * OLD."Precio")::numeric, 3);
  nTotalDtoBase  := Round((nTotalLinea * (OLD."DescuentoBase" / 100))::numeric, 3);
  nTotalLinea    := nTotalLinea - nTotalDtoBase; 
  nTotalDtoResto := Round((nTotalLinea * (OLD."DescuentoResto" / 100))::numeric, 3);
  nTotalLinea    := nTotalLinea - nTotalDtoResto;
  nTotalDtos     := nTotalDtos + nTotalDtoBase + nTotalDtoResto; 

  nTotalBase := Round(nTotalLinea::numeric, 2);
  nTotalIVA  := (nTotalBase * OLD."PorcentajeIVA" / 100);
  nTotalRE   := (nTotalBase * OLD."PorcentajeRE" / 100);

  nTotalIVA  := Round(nTotalIVA::numeric, 2);
  nTotalRE   := Round(nTotalRE::numeric, 2);
  nTotalDtos := Round(nTotalDtos::numeric, 2);

  PERFORM "SumaArticulo"(OLD."Referencia", OLD."TotalUnidades");

  SELECT "Cliente" INTO cCliente FROM "Albaran"
  WHERE ("Serie" = OLD."Serie") AND ("NumeroDoc" = OLD."NumeroDoc");

  PERFORM "RestaCliente"(cCliente, nTotalBase + nTotalIVA + nTotalRE);

  RETURN OLD;
end
$$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;


CREATE TABLE public."Albaran" (
  "NumeroDoc"        public."NumeroDocumento" NOT NULL,
  "Serie"            public."Serie" NOT NULL,
  "Fecha"            date NOT NULL,
  "Cliente"          public."CodigoCliProv" NOT NULL,
  "Nombre"           public."RazonSocial",
  "BaseImponible"    public."Importes",
  "IVA"              public."Importes",
  "RE"               public."Importes",
  "Notas"            public."Memo",
  "CodigoDir"        public."CodigoDireccion",
  "Direccion"        public."Direccion",
  "Poblacion"        public."Poblacion",
  "CodigoPostal"     public."CodigoPostal",
  "Provincia"        public."Provincia",
  "Pais"             public."Pais",
  "CIF"              public."CIF",
  "Total"            public."Importes",
  "Agente"           public."CodigoAgente",
  "SuNumeroPedido"   public."SuNumeroPedido",
  "Telefono"         public."Telefono",
  "Fax"              public."Telefono",
  "FormaPago"        public."FormaPago",
  "Transportista"    public."CodigoTransporte",
  "Repartidor"       public."CodigoRepartidor",
  "Portes"           public."Importes",
  "DebidosPagados"   public."Boolean",
  "Gastos"           public."Importes",
  "TotalDescuentos"  public."Importes",
  "TotalPesoNeto"    public."Peso",
  "TotalPesoBruto"   public."Peso",
  "Facturado"        public."Boolean",
  "Modificado"       public."Boolean"
  /* Llaves */
  CONSTRAINT "PK_Albaran"
    PRIMARY KEY ("Serie", "NumeroDoc")
) WITH (
    OIDS = FALSE
  );

CREATE INDEX "IDX_Albaran_Nombre"
  ON public."Albaran"
  ("Nombre");

CREATE TRIGGER "Albaran_BD"
  BEFORE DELETE
  ON public."Albaran"
  FOR EACH ROW
  EXECUTE PROCEDURE public."AlbaranBeforeDelete"();

CREATE TRIGGER "Albaran_BU"
  BEFORE UPDATE
  ON public."Albaran"
  FOR EACH ROW
  EXECUTE PROCEDURE public."AlbaranBeforeUpdate"();

CREATE TABLE public."AlbaMov" (
  "RecNo"              serial NOT NULL,
  "Serie"              public."Serie" NOT NULL,
  "NumeroDoc"          public."NumeroDocumento" NOT NULL,
  "Referencia"         public."CodigoArticulo" NOT NULL,
  "Descripcion"        public."Descripcion",
  "Cantidad"           public."Cantidad",
  "Precio"             public."Importes",
  "PrecioCosto"        public."Importes",
  "PorcentajeIVA"      public."Porcentaje",
  "PorcentajeRE"       public."Porcentaje",
  "Almacen"            public."CodigoAlmacen",
  "Lote"               public."Lote",
  "Unidades"           public."Cantidad",
  "TotalUnidades"      public."Cantidad",
  "CodigoPromocion"    public."CodigoArticuloOpcional",
  "Promocion"          public."Cantidad",
  "DescuentoBase"      public."Porcentaje",
  "DescuentoResto"     public."Porcentaje",
  "PesoNeto"           public."Peso",
  "PesoBruto"          public."Peso",
  "ReferenciaCliente"  public."CodigoArticuloOpcional",
  "Modificado"         public."Boolean",
  "FechaCaducidad"     date,
  "TotalLinea"         public."Importes",
  "SeriePedido"        public."Serie",
  "NumeroPedido"       public."NumeroDocumento",
  /* Llaves */
  CONSTRAINT "PK_AlbaMov"
    PRIMARY KEY ("RecNo")
) WITH (
    OIDS = FALSE
  );

CREATE INDEX "IDX_AlbaMov_SerieNumeroDoc"
  ON public."AlbaMov"
  ("Serie", "NumeroDoc", "RecNo");

CREATE TRIGGER "AlbaMov_AD"
  AFTER DELETE
  ON public."AlbaMov"
  FOR EACH ROW
  EXECUTE PROCEDURE public."AlbaMovAfterDelete"();

做测试,我发现如果我从这个函数中删除主表记录,它是否完美,为什么不破??我无法理解。

CREATE OR REPLACE FUNCTION public."Albaran2Factura"
(
  IN  "cSerieAlbaran"   public."SerieDocumento",
  IN  "nNumeroAlbaran"  public."NumeroDocumento"
)
RETURNS SETOF public."SerieNumeroDocumento" AS
$$
declare rDocumento "SerieNumeroDocumento"%ROWTYPE;
declare rMaster RECORD;
declare rDetail RECORD;
declare rConfig RECORD;
declare rIVA RECORD;
declare cRegimenIVA CHAR;
declare nNumeroFactura "NumeroDocumento";
declare nPorcentajeIVAPortes "Importes";
declare nPorcentajeREPortes "Importes";

begin
  rDocumento."Serie"     := '';
  rDocumento."NumeroDoc" := -1;

  SELECT * INTO rConfig FROM "Empresa" LIMIT 1;

  SELECT "PorcentajeIVA", "PorcentajeRE" INTO rIVA FROM "Iva"
  WHERE "Tipo" = rConfig."TipoIVAPortes";

  nPorcentajeIVAPortes := rIVA."PorcentajeIVA";
  nPorcentajeREPortes  := rIVA."PorcentajeRE";

  UPDATE "Numera" 
  SET "NumeroDoc" = "NumeroDoc" + 1
  WHERE ("TipoDocumento" = 'FV') AND ("Serie" = "cSerieAlbaran");

  SELECT "NumeroDoc" INTO nNumeroFactura FROM "Numera"
  WHERE ("TipoDocumento" = 'FV') AND ("Serie" = "cSerieAlbaran");

  SELECT * INTO rMaster FROM "Albaran"
  WHERE ("Serie" = "cSerieAlbaran") AND ("NumeroDoc" = "nNumeroAlbaran");

  SELECT "RegimenIVA" INTO cRegimenIVA FROM "Clientes"
  WHERE "Codigo" = rMaster."Cliente";

  IF ("cSerieAlbaran" <> 'ZZZ') THEN

    IF (cRegimenIVA = 'G') THEN
      nPorcentajeREPortes := 0;
    ELSIF (cRegimenIVA = 'E') THEN
      nPorcentajeIVAPortes := 0;
      nPorcentajeREPortes  := 0;
    END IF; /* IF (cRegimenIVA = 'G') */

  ELSE
    nPorcentajeIVAPortes := 0;
    nPorcentajeREPortes  := 0;
  END IF; /* IF ("cSerieAlbaran" <> 'ZZZ') */

  INSERT INTO "Factura" ("NumeroDoc",            
                         "Serie",                
                         "Fecha",                
                         "Cliente",              
                         "Nombre",               
                         "BaseImponible",        
                         "IVA",                  
                         "RE",                   
                         "Notas",                
                         "Direccion",            
                         "Poblacion",            
                         "CodigoPostal",         
                         "Provincia",            
                         "CIF",                  
                         "Total",                
                         "Agente",
                         "CodigoDir",            
                         "Pais",                 
                         "SuNumeroPedido",       
                         "Telefono",             
                         "Fax",                  
                         "FormaPago",            
                         "Transportista",        
                         "Repartidor",           
                         "Portes",               
                         "DebidosPagados",       
                         "Gastos",               
                         "TotalDescuentos",      
                         "TotalPesoNeto",        
                         "TotalPesoBruto",
                         "PorcentajeIVAPortes",  
                         "PorcentajeREPortes",   
                         "Albaranes",            
                         "Exportada",            
                         "Rapel",                
                         "Cobrada",              
                         "Modificado") 
  VALUES (nNumeroFactura,            
          "cSerieAlbaran",                
          current_date,                
          rMaster."Cliente",              
          rMaster."Nombre",               
          rMaster."BaseImponible",        
          rMaster."IVA",                  
          rMaster."RE",                   
          rMaster."Notas",                
          rMaster."Direccion",            
          rMaster."Poblacion",            
          rMaster."CodigoPostal",         
          rMaster."Provincia",            
          rMaster."CIF",                  
          rMaster."Total",                
          rMaster."Agente",
          rMaster."CodigoDir",            
          rMaster."Pais",                 
          rMaster."SuNumeroPedido",       
          rMaster."Telefono",             
          rMaster."Fax",                  
          rMaster."FormaPago",            
          rMaster."Transportista",        
          rMaster."Repartidor",           
          rMaster."Portes",               
          rMaster."DebidosPagados",       
          rMaster."Gastos",               
          rMaster."TotalDescuentos",      
          rMaster."TotalPesoNeto",        
          rMaster."TotalPesoBruto",
          nPorcentajeIVAPortes,  
          nPorcentajeREPortes,   
          'Albaran ' || "nNumeroAlbaran" || '/' || "cSerieAlbaran",            
          '0',            
          '0',                
          '0',              
          '1');

  FOR rDetail IN SELECT * FROM "AlbaMov"
                 WHERE ("Serie" = "cSerieAlbaran") AND ("NumeroDoc" = "nNumeroAlbaran")
                 ORDER BY "RecNo"
  LOOP

      INSERT INTO "FacMov" ("Serie",            
                            "NumeroDoc",        
                            "Referencia",       
                            "Descripcion",      
                            "Cantidad",         
                            "Precio",           
                            "PorcentajeIVA",    
                            "PorcentajeRE",     
                            "NumeroAlbaran",    
                            "SerieAlbaran",     
                            "FechaAlbaran",     
                            "NumeroPedido",     
                            "SeriePedido",      
                            "PrecioCosto",      
                            "Almacen",          
                            "Lote",             
                            "Unidades",         
                            "TotalUnidades",    
                            "CodigoPromocion",  
                            "Promocion",        
                            "DescuentoBase",    
                            "DescuentoResto",   
                            "PesoNeto",         
                            "PesoBruto",        
                            "ReferenciaCliente",
                            "Modificado",       
                            "FechaCaducidad",   
                            "NoDescontar",      
                            "Agente",           
                            "Repartidor")
             VALUES ("cSerieAlbaran",            
                     nNumeroFactura,        
                     rDetail."Referencia",       
                     rDetail."Descripcion",      
                     rDetail."Cantidad",         
                     rDetail."Precio",           
                     rDetail."PorcentajeIVA",    
                     rDetail."PorcentajeRE",     
                     rMaster."NumeroDoc",    
                     rMaster."Serie",     
                     rMaster."Fecha",     
                     rDetail."NumeroPedido",     
                     rDetail."SeriePedido",      
                     rDetail."PrecioCosto",      
                     rDetail."Almacen",          
                     rDetail."Lote",             
                     rDetail."Unidades",         
                     rDetail."TotalUnidades",    
                     rDetail."CodigoPromocion",  
                     rDetail."Promocion",        
                     rDetail."DescuentoBase",    
                     rDetail."DescuentoResto",   
                     rDetail."PesoNeto",         
                     rDetail."PesoBruto",        
                     rDetail."ReferenciaCliente",
                     '1',       
                     rDetail."FechaCaducidad",   
                     '0',      
                     rMaster."Agente",           
                     rMaster."Repartidor");

  END LOOP;


  /********************** Deleting master record work ****************/

  DELETE FROM "Albaran"
  WHERE ("Serie" = "cSerieAlbaran") AND ("NumeroDoc" = "nNumeroAlbaran");

  /**************************************/

  rDocumento."Serie"     := "cSerieAlbaran";
  rDocumento."NumeroDoc" := nNumeroFactura;

  RETURN NEXT rDocumento;


end
$$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER;

这不起作用:

CREATE OR REPLACE FUNCTION public."BorrarAlbaran"
(
  IN  "cSerie"      public."SerieDocumento",
  IN  "nNumeroDoc"  public."NumeroDocumento"
)
RETURNS void AS
$$
begin
  DELETE FROM "Albaran"
  WHERE ("Serie" = "cSerie") and ("NumeroDoc" = "nNumeroDoc");
end
$$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;

解决方法:

CREATE OR REPLACE FUNCTION public."BorrarAlbaranVenta"
(
  IN  "cSerie"      public."SerieDocumento",
  IN  "nNumeroDoc"  public."NumeroDocumento"
)
RETURNS void AS
$$
begin
  DELETE FROM "AlbaMov"
  WHERE ("Serie" = "cSerie") and ("NumeroDoc" = "nNumeroDoc");

  DELETE FROM "Albaran"
  WHERE ("Serie" = "cSerie") and ("NumeroDoc" = "nNumeroDoc");
end
$$
LANGUAGE 'plpgsql'
VOLATILE
CALLED ON NULL INPUT
SECURITY INVOKER
COST 100;

1 个答案:

答案 0 :(得分:2)

  

删除主表中的记录时,删除详细信息中的所有记录,然后在主表中将字段“总计”更新为0,但不删除主表记录。如果我从主表中删除没有记录的记录,详细信息表将被顺利删除。

这通常表示触发器在触发器触发器中重新插入行并带有副作用。

在Postgres中,更新实际上是删除后跟插入。一旦所有之前的触发器完成了它们的工作,旧的行/ ctid被标记为dead并且创建了一个新的row / ctid - 都来自txid_current()。然后触发后触发。

这里需要理解的是,你并没有操纵行本身。相反,您在给定时间操纵行的快照,后者由其ctid和各种元信息引用:

http://www.postgresql.org/docs/9.2/static/ddl-system-columns.html

无论如何,我只是粗略地看一下,但我的猜测是AlbaranBeforeDelete()是罪魁祸首。

在删除行/ ctid1之前,您可以在子表中级联删除行。当你这样做时,row / ctid1仍然被标记为活动,而不是已被标记为死...有充分的理由:如果你在删除前触发器中返回null,则不会删除该行。

此时,您的子表在删除后触发然后启动并更新row / ctid1。此语句在创建新的实时和更新的行/ ctid2时将row / ctid1标记为已死。

然后您的初始陈述将恢复。 Postgres将row / ctid1标记为已死(顺便说一句,它已经是),并在触发后触发。但是,你仍然留下了一个实时行/ ctid2,因为你的原始语句在触发每个后者之前计算出受影响的行/ ctids,并不知道。因此row / ctid2留下来活着。

修复方法是改变你的流程,使得之前的触发器没有任何副作用。副作用属于后触发器。

不可否认,人们可能会认为这是Postgres中的一个错误。它在几年前就让我感到困惑,当我这样做时它就被视为一种特征。


顺便说一句,如果上述情况不是100%明确的话,这是另一个正在发生的事情的典型例子:

create table if not exists test (
    id serial primary key
);

create table if not exists subtest (
    id serial primary key,
    test_id int references test(id) on delete cascade
);

create function break_pgsql() returns trigger as $$
begin
    return null;
end;
$$ language plpgsql;

create trigger break_pgsql before delete on subtest
    for each row
execute procedure break_pgsql();

insert into test default values;
insert into subtest (test_id) select id from test;
delete from test;
select * from test;    -- empty
select * from subtest; -- not empty

在上面的代码中,Postgres的内置触发器级联删除了相关的行。发出了生成的删除语句,但带有副作用的before触发器会乱用它,从而产生一个带有无效外键的行。