0

まず、私の下手な英語で申し訳ありません。

PostgreSQL 9.2 を使用する新しいアプリケーションを作成しています。Firebird で使用されているのと同じ「ロジック」を使用しようとしていますが、明らかに PostgreSQL では機能しません。

「Albaran」と呼ばれるマスターテーブルと「AlbaMov」と呼ばれる他の詳細テーブルがあります。詳細テーブルのレコードを変更すると、マスター テーブルが更新する、対応する義務を持ついくつかのトリガーを定義しました。マスター テーブルのレコードを削除する場合を除いて、すべてが完全に機能します。

マスター テーブルのレコードを削除すると、詳細からすべてのレコードが削除され、マスター テーブルの「合計」フィールドが 0 に更新されますが、マスター テーブルのレコードは削除されません。マスターテーブルからレコードを削除すると、詳細テーブルのレコードがスムーズに削除されます。

私はテストを行っており、Master テーブルへの UPDATE が CalculoAlbaranVenta と呼ばれる関数で行われていることに問題があることがわかりました。

この同じシステムは、Firebird でも完全に機能します。

この関数は、PHP 画面を更新するために使用する type% ROWTYPE の変数を返します。

ここでは、トリガーと関数を含むテーブルの定義を残します。

どこに問題がありますか?

よろしくお願いします。

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

1 に答える 1

2

マスター テーブルのレコードを削除すると、詳細からすべてのレコードが削除され、マスター テーブルの「合計」フィールドが 0 に更新されますが、マスター テーブルのレコードは削除されません。マスターテーブルからレコードを削除すると、詳細テーブルのレコードがスムーズに削除されます。

これは通常、トリガーがカスケードされており、副作用のあるトリガー前に行を再挿入していることを示しています。

Postgres では、更新は実際には削除とそれに続く挿入です。すべての before トリガーが処理を完了すると、古い行/ctid は無効としてマークされ、新しい行/ctid が作成されます (両方とも txid_current() 以降)。そして、アフタートリガーが作動します。

ここで理解しておくべきポイントは、行自体を操作していないということです。むしろ、特定の時点で行のスナップショットを操作しており、後者はその ctid とあらゆる種類のメタ情報によって参照されます。

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

とにかく、ざっと見ただけですが、AlbaranBeforeDelete() が原因だと思います。

行/ctid1 が削除される前に、子テーブルの削除行をカスケードします。実行すると、行/ctid1 は、既にデッドとしてマークされているのではなく、まだライブとしてマークされています... 正当な理由もあります: before delete トリガーで null を返すと、行は削除されません。

この時点で、サブテーブルの after delete トリガーが開始され、row/ctid1 が更新されます。このステートメントは、新しい有効な更新された行/ctid2 を作成するときに、行/ctid1 を無効としてマークします。

その後、最初のステートメントが再開されます。Postgres は、row/ctid1 を無効としてマークし (ちなみに、既に無効です)、トリガーが起動した後にマークします。ただし、後者のそれぞれでトリガーを起動する前に影響を受ける行/ctid を処理した元のステートメントが知らなかったため、まだライブ行/ctid2 が残っています。したがって、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 の組み込みトリガーが関連する行をカスケード削除します。その結果、delete ステートメントが発行されますが、副作用のある before トリガーがそれをいじり、無効な外部キーを持つ行が生成されます。

于 2013-05-03T09:34:06.947 に答える