229 messaggi dal 17 aprile 2006
Salve,

Ho un sistema di Creazione fatture (web).
Per essere sicuro di assegnare ad ogni utente un numero progressivo, che ogni anno riparta da 1, ho ideato questa soluzione, usando appunto @identity. (via allego script)

Dubbi:

se ci fosse il rollback?



CREATE TABLE [dbo].[tbInvoice](
[ID] [int] IDENTITY(1,1) NOT NULL,
[Company] [nvarchar](3) NOT NULL,
[Year] [nchar](4) NOT NULL,
[InvoiceNumber] [int] NOT NULL,
CONSTRAINT [PK_tbInvoice] PRIMARY KEY CLUSTERED 
(
[ID] ASC,
[Company] ASC,
[Year] ASC,
[InvoiceNumber] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]


create Procedure sp_CreateNextInvoiceNr

AS

BEGIN 
SET XACT_ABORT ON
DECLARE @nextNumReg int
DECLARE @YEAR VARCHAR(4)

SET @YEAR = (SELECT CAST(DATEPART(YEAR,GETDATE()) AS VARCHAR(4)))
BEGIN TRAN

INSERT INTO [tbInvoice]
([Company]
,[Year]
,[InvoiceNumber])
VALUES
('001'
,@YEAR
,0)

SET @nextNumReg = (SELECT [InvoiceNumber] + 1
FROM [tbInvoice] 
WHERE [Year] = DATEPART(YEAR,GETDATE()) AND ID = (SELECT SCOPE_IDENTITY() AS id ) - 1)
IF(@nextNumReg =0 OR @nextNumReg ='' OR @nextNumReg IS NULL)
BEGIN

SET @nextNumReg =1
END
UPDATE [tbInvoice] 
SET [InvoiceNumber] = @nextNumReg 
where ID =(SELECT SCOPE_IDENTITY() AS id )

COMMIT TRAN
END

Vincenzo Pesante
Software Engineer
1.976 messaggi dal 27 luglio 2005
Contributi
salve Vincenzo,
in caso di failure, lo slot di [InvoiceNumber] sara' disponibile per il prossimo richiedente, mentre [ID] no, e restera' valorizzato come da ultimo inserimento fallito, quindi avrai dei gap, anche se cio' ovviamente e' irrilevante in quanto l'unica nunerazione progressiva senza gap deve essere [InvoiceNumber] ...
al di la' di cio' non comprendo tutta la logica...
INSERT INTO [tbInvoice]
([Company]
,[Year]
,[InvoiceNumber])
VALUES
('001'
,@YEAR
,0)

e' sbagliato in quanto: 1) Id e' un numero nel dominio degli interi e tu gli vuoi assegnare '001'.. 2) Id, oltre ad essere un intero, ha anche impostata la proprieta' identity, e quindi non devi (e non puoi direttamente, a meno di non impostare SET IDENTITY INSERT {ON|OFF}) fornirgli un valore in quanto questo sara' gestito e generato direttamente dallo storage engine al momento del tentativo di inserimento...

SET @nextNumReg = (SELECT [InvoiceNumber] + 1
FROM [tbInvoice] 
WHERE [Year] = DATEPART(YEAR,GETDATE()) AND ID = (SELECT SCOPE_IDENTITY() AS id ) - 1)


non comprendo il senso dell'istruzione... recuperi il numero [InvoiceNumber] incrementadolo di 1 filtrando per Anno (giusto) e Id = riga appena inserita?

io non agirei cosi', e recupererei dinamicamente l'informazione al momento dell'inserimento, qualche cosa simile a
SET NOCOUNT ON;
USE tempdb;
GO
CREATE TABLE [dbo].[tbInvoice](
  [ID] [int] IDENTITY(1,1) NOT NULL,
  [Company] [nvarchar](3) NOT NULL,
  [Year] [nchar](4) NOT NULL,
  [InvoiceNumber] [int] NOT NULL,
CONSTRAINT [PK_tbInvoice] PRIMARY KEY CLUSTERED (
  [ID] ASC,
  [Company] ASC,
  [Year] ASC,
  [InvoiceNumber] ASC
  )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY];
GO
DECLARE @Company nvarchar(3) = 'abc';

INSERT INTO dbo.tbInvoice ([Company], [Year], [InvoiceNumber])
  SELECT @Company, CONVERT(varchar(4),DATEPART(YEAR, GETDATE()))
    , ISNULL(MAX(base.InvoiceNumber), 0) + 1
    FROM dbo.tbInvoice AS base
    WHERE [Year] = CONVERT(varchar(4),DATEPART(YEAR, GETDATE()));
GO
DECLARE @Company nvarchar(3) = 'abc';

INSERT INTO dbo.tbInvoice ([Company], [Year], [InvoiceNumber])
  SELECT @Company, CONVERT(varchar(4),DATEPART(YEAR, GETDATE()))
    , ISNULL(MAX(base.InvoiceNumber), 0) + 1
    FROM dbo.tbInvoice AS base
    WHERE [Year] = CONVERT(varchar(4),DATEPART(YEAR, GETDATE()));
GO
PRINT 'questa fallisce';
DECLARE @Company nvarchar(3) = 'abc';

INSERT INTO dbo.tbInvoice ([Company], [Year], [InvoiceNumber])
  SELECT @Company + 'x', CONVERT(varchar(4),DATEPART(YEAR, GETDATE()))
    , ISNULL(MAX(base.InvoiceNumber), 0) + 1
    FROM dbo.tbInvoice AS base
    WHERE [Year] = CONVERT(varchar(4),DATEPART(YEAR, GETDATE()));

GO
DECLARE @Company nvarchar(3) = 'abc';

INSERT INTO dbo.tbInvoice ([Company], [Year], [InvoiceNumber])
  SELECT @Company, CONVERT(varchar(4),DATEPART(YEAR, GETDATE()))
    , ISNULL(MAX(base.InvoiceNumber), 0) + 1
    FROM dbo.tbInvoice AS base
    WHERE [Year] = CONVERT(varchar(4),DATEPART(YEAR, GETDATE()));

GO
SELECT * FROM dbo.tbInvoice  
GO
DROP TABLE dbo.tbInvoice;
--<-------
questa fallisce
Msg 8152, Level 16, State 13, Line 4
String or binary data would be truncated.
The statement has been terminated.
ID          Company Year InvoiceNumber
----------- ------- ---- -------------
1           abc     2014 1
2           abc     2014 2
4           abc     2014 3

come vedi il terzo inserimento fallisce (al di la' del motivo), ma il gap nella numerazione di InvoiceNumber non c'e', mentre ovviamente c'e' nella colonna Id
ovviamente, visto che dopo l'inserimento in dbo.tbInvoice dovrai aggiungere righe in almeno altre 2 tabelle, racchiudi il tutto in una transazione esplicita, che ti consente di garantire adeguata protezione all'operazione ACID

perche' la colonna [Year] e' nchar? sicuramente non puo' essere un valore con carattere diverso da numeri, e quindi perche' non usare un intero con dominio vincolato da un costraint (BETWEEN 1999 AND 2050)... occupi "spazio" per niente ed il dominio dell'attributo e' sbagliato...

la chiave primaria, visto che hai utilizzato una chiave surrogata univoca basata su Identity (la colonna Id), la baserei eventualmente su quella, mentre farei un'altro indice su [Year] + [InvoiceNumber]...
la colonna [Company] nella chiave non ha tecnicamente alcun senso se non nel caso di tabella/database multiazienda e [Company] stia ad identificare un'azienda specifica... in tal caso farei la chiave primaria su [Company] + [Year] + [InvoiceNumber] e toglierei completamente l'attributo [Id]... l'unico motivo per mantenerlo e' per farci una chiave primaria clusterizzata visto che clusterizzare sul set di colonne [Company] + [Year] + [InvoiceNumber] sicuramente causerebbe elevata frammentazione

saluti

Andrea Montanari
http://www.hotelsole.com - http://www.hotelsole.com/asql/index.php
229 messaggi dal 17 aprile 2006
Ciao Andrea,

grazie della risposta.

In effetti l'esempio che ho postato e' una modifica della struttura originale, perchè il db e' multi aziendale ed in fatti la chiave primaria è:
CREATE TABLE [dbo].[tbInvoice](
[Company] [nvarchar](3) NOT NULL,
[Year] [nchar](4) NOT NULL,
[InvoiceNumber] [int] NOT NULL,
CONSTRAINT [PK_tbInvoice] PRIMARY KEY CLUSTERED 
(
[Company] ASC,
[Year] ASC,
[InvoiceNumber] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, FILLFACTOR = 90) ON [PRIMARY]
) ON [PRIMARY]




quello che voglio evitare è la concorrenza in caso piu utenti facciano richiesta di InvoiceNumber allo stesso tempo.

Cambiando strategia e mantendendo la stuttura originaria dici che una cosa cosi potrebbe funzionare?
In pratica faccio un ciclo while (MAX 2 volte), dove cerco il max InvoiceNumber e lo inserisco testando prima che non esista, tutto in una transazione.


declare @OKInvoiceNumber as bit,@NextInvoiceNumber as int, @CYCLE AS INT,
        @MAXCYCLE AS INT

set @OKInvoiceNumber = 0
set @NextInvoiceNumber = 0  
SET @CYCLE = 0
SET @MAXCYCLE = 2

BEGIN TRAN
WHILE @OKInvoiceNumber = 0
BEGIN
set @NextInvoiceNumber = (select MAX(InvoiceNumber) from tbInvoice where [Year]='2013' and Company= '001') + 1
IF(select COUNT(*) from tbInvoice where [Year]=2013 and Company= '001' and InvoiceNumber=@NextInvoiceNumber ) = 0
  BEGIN
  INSERT INTO 
           ([Company]
           ,[Year]
           ,[InvoiceNr])
     VALUES
           ('001'
           ,'2013'
           ,@NextInvoiceNumber)
  IF @@ERROR <>0
    BEGIN
    ROLLBACK TRAN
       IF @CYCLE = @MAXCYCLE
      BEGIN
        SET @OKInvoiceNumber = -1
        BREAK        
      END
      ELSE 
      BEGIN
      SET @CYCLE = @CYCLE + 1
      END
    END
    ELSE
    BEGIN 
    SET @OKInvoiceNumber =1
    END
  END
  ELSE
  BEGIN 
  IF @CYCLE = @MAXCYCLE
      BEGIN
        SET @OKInvoiceNumber = -1
        BREAK        
      END
  ELSE 
      BEGIN
      SET @CYCLE = @CYCLE + 1
      END
  END  
  END
END

-- OTTENUTO IL @NextInvoiceNumber EFFETTUO LE ALTRE INSERT/UPDATE SEMPRE ALL'INTERNO DELLA TRAN
-- SE QUALCOSA FA SCATTARE IL rollback si riazzera anche tbInvoice
COMMIT TRAN

Vincenzo Pesante
Software Engineer
1.976 messaggi dal 27 luglio 2005
Contributi
salve Vincenzo...
non mi piacciono molto "i cicli"....
immagino tu lo faccia per avere un "secondo tentativo" di recuperare il progressivo necessario per la nuova fattura...
tecnicamente questo fallisce in caso di lock corrente e la tua transazione vada fuori tempo massimo di esecuzione, ed io non ritenterei immediatamente, ma lascerei riprovare "piu' tardi", visto che comunque l'esecuzione anche di piu' istruzioni di INSERT dovute poi alla fattura stessa con il suo disegno Master-Detail sicuramente non e' cosi' impegnativo se non nei casi di inserimento massivo bulk, ed in questo caso direi che comunque "ciclare 2 volte" non ti salva dal problema...
quindi, in effetti, io aprirei una transazione esplicita, otterrei la numerazione, inserirei la riga in tbInvoice (che non dovrebbe mai fallire causa concorrenza, in quanto hai in effetti un lock sulla tabella sino alla chiusura della transazione)... se l'inserimento non fallisce causa altra problematica (e quindi @@ROWCOUNT = 1), passerei all'inserimento dei dati fattura Master-Detail...
se alla fine della manipolazione @@ROWCOUNT = 0, allora da qualche parte e' fallita l'operazione, tra l'ottenimento del numero all'inserimento dell'ultimo dettaglio..
in questo caso ROLLBACK, diversamente COMMIT
quindi, trivialmente,
SET NOCOUNT ON;
USE tempdb;
GO
CREATE TABLE [dbo].[tbInvoice](
  [Company] [nvarchar](3) NOT NULL,
  [Year] [nchar](4) NOT NULL,
  [InvoiceNumber] [int] NOT NULL,
CONSTRAINT [PK_tbInvoice] PRIMARY KEY CLUSTERED (
  [Company] ASC,
  [Year] ASC,
  [InvoiceNumber] ASC)
  );

CREATE TABLE [dbo].[InvoiceMasterData] (
  [Company] [nvarchar](3) NOT NULL,
  [Year] [nchar](4) NOT NULL,
  [InvoiceNumber] [int] NOT NULL,

  [IdCliente] int NOT NULL
    --CONSTRAINT fk_InvoiceMasterData_IdCliente$is$Cliente
    --  FOREIGN KEY 
    --  REFERENCES [dbo].[Clienti] (Id)
    ,
  [DataFattura] date NOT NULL
  );
GO
DECLARE @company nvarchar(3) = 'xyz';
DECLARE @Anno char(4);
DECLARE @newInvoiceNr int = 0;
DECLARE @rows int = 0;

SET NOCOUNT OFF;
BEGIN TRAN
SELECT @newInvoiceNr = ISNULL(MAX(i.[InvoiceNumber]), 0) + 1
  , @Anno = DATEPART(YEAR, GETDATE())
  FROM [dbo].[tbInvoice] i
  WHERE i.[Company] = @company
  AND i.[Year] = DATEPART(YEAR, GETDATE());

IF (@newInvoiceNr IS NULL) BEGIN
  SET @rows = 0;
  END;
ELSE BEGIN
  INSERT INTO [dbo].[tbInvoice]
    VALUES ( @company, @Anno, @newInvoiceNr);
  SET @rows = @@ROWCOUNT;

  IF (@rows = 1) BEGIN
    INSERT INTO [dbo].[InvoiceMasterData] 
    VALUES ( @company, @Anno, @newInvoiceNr
      , 1        -- cliente
      , GETDATE()    -- data fattura
    );
    END;
    SET @rows = @@ROWCOUNT;
  END;

IF (@rows = 0) BEGIN
  PRINT 'failure...';
  ROLLBACK;
  END
ELSE
  COMMIT;
GO
SELECT i.[Company], i.[Year], i.[InvoiceNumber]
  , m.[IdCliente], m.[DataFattura]
  FROM [dbo].[tbInvoice] i
    JOIN [dbo].[InvoiceMasterData] m 
      ON m.[Company] = i.[Company]
        AND m.[Year] = i.[Year]
        AND m.[InvoiceNumber] = i.[InvoiceNumber]
  WHERE i.[Company] = 'xyz';
GO
DROP TABLE [dbo].[InvoiceMasterData], dbo.[tbInvoice];


spero che questo tuo codice sia comunque di test e non codice effettivo, perche' diventerebbe a tutti gli effetti codice dinamico... io farei una bella stored procedure

restano pero' le mie perplessita' sui domini da te utilizzati per alcune colonne...
saluti

Andrea Montanari
http://www.hotelsole.com - http://www.hotelsole.com/asql/index.php
229 messaggi dal 17 aprile 2006
Ciao Andrea,


In effetti tutto andrà in un SP, sui nomi cosa suggeriresti?

Volevo chiederti alcune a prop del tuo codice (molto piu leggibile del mio :) )

1. SELECT @newInvoiceNr = ISNULL(MAX(i.[InvoiceNumber]), 0) + 1
, @Anno = DATEPART(YEAR, GETDATE())
FROM [dbo].[tbInvoice] i
WHERE i.[Company] = @company
AND i.[Year] = DATEPART(YEAR, GETDATE());

Con questa TRANS due utenti possono chiedeere lo stesso Max invoice number?

2. INSERT INTO [dbo].[tbInvoice]
VALUES ( @company, @Anno, @newInvoiceNr);
SET @rows = @@ROWCOUNT;

Perche non mettere anche alla fine di questo insert un controllo per eventuali ROLLBACK?

3. A livello applicativo in caso di ROLLBACK vorrei ricevere l'errore per cui potrei in questo caso fare?

IF (@rows = 0) or (@@error <> 0) BEGIN
SELECT 'failure...';
ROLLBACK;
END
ELSE
COMMIT;
GO
Modificato da bluland il 03 giugno 2014 20.11 -

Vincenzo Pesante
Software Engineer
1.976 messaggi dal 27 luglio 2005
Contributi
salve Vincenzo...
>In effetti tutto andrà in un SP, sui nomi cosa suggeriresti?
cosa intendi?? :)

>1)...
tecnicamente si...ovviamente c'e' il rischio ma comunque, come da te rilevato, il batch successivo ovviamente fallirebbe richiedendo un successivo roundtrip sul server... ma questo personalmente mi sta bene in quanto diversamente richiederebbe una protezione che non sempre sono disposto a pagare, quindi una transazione serializable ... ed e' un costo probabilmente molto caro ... un rapido sommario del livello di isolamento puo' essere reperito in http://gavindraper.com/2012/02/18/sql-server-isolation-levels-by-example/

>2)Perche non mettere anche alla fine di questo insert un controllo per eventuali ROLLBACK?

seguendo il flusso non e' necessario... e vedi anche dopo..

>3. A livello applicativo in caso di ROLLBACK vorrei ricevere l'errore per cui potrei in questo caso fare?

in una procedura puoi gestire meglio quest'eventulita'..
innesti tutto il codice in un gestore di errore... trivialmente similarmente a
CREATE PROCEDURE ..... (
  @params .....
  ......  
  )
AS BEGIN
  DECLARE @msg varchar(1000);
  SET @msg = CAST(OBJECT_NAME(@@PROCID) AS sysname)
  
  BEGIN TRY
    SET NOCOUNT OFF;
    BEGIN TRAN
    SELECT @newInvoiceNr = ISNULL(MAX(i.[InvoiceNumber]), 0) + 1
      , @Anno = DATEPART(YEAR, GETDATE())
      FROM [dbo].[tbInvoice] i
      WHERE i.[Company] = @company
      AND i.[Year] = DATEPART(YEAR, GETDATE());
  
    IF (@newInvoiceNr IS NULL) BEGIN
      SET @rows = 0;
      END;
    ELSE BEGIN
      INSERT INTO [dbo].[tbInvoice]
        VALUES ( @company, @Anno, @newInvoiceNr);
      SET @rows = @@ROWCOUNT;
    
      IF (@rows = 1) BEGIN
        INSERT INTO [dbo].[InvoiceMasterData] 
        VALUES ( @company, @Anno, @newInvoiceNr
          , 1        -- cliente
          , GETDATE()    -- data fattura
        );
        END;
        SET @rows = @@ROWCOUNT;
      END;
  
    IF (@rows = 0) BEGIN
    SET @msg = 'Impossibile inserire correttamente la fattura corrente.' 
          + CHAR(10) + 'Possibile conflitto di concorrenza.' 
          + CHAR(10) + @msg; 
      RAISERROR (@msg, 16, 1);
      END;
  
    -- tutto ok
    COMMIT;
    RETURN 0;
  END TRY
  
  BEGIN CATCH
    -- ritorna l'errore
    DECLARE @ErrorMSG varchar(2000);
    SET @ErrorMSG = ERROR_MESSAGE();
    RAISERROR (@ErrorMSG, 16, 1);
    RETURN -100;
  END CATCH
END;
GO  


il gestore eccezioni ti serve per il rethrow dell'errore, che potrai/dovrai poi gestire lato applicativo...
Modificato da Andrea Montanari il 05 giugno 2014 00.25 -

Andrea Montanari
http://www.hotelsole.com - http://www.hotelsole.com/asql/index.php
229 messaggi dal 17 aprile 2006
Non ho mai usato il try catch di SQL devo provare, ma gestisce l'errore però cmq lo devo racchiudere in una trans giusto?

Vincenzo Pesante
Software Engineer
1.976 messaggi dal 27 luglio 2005
Contributi
salve Vincenzo,
si, la logica procedurale non cambia, e devi proteggere l'atomicita' del batch completo con transazioni esplicite...
salut

Andrea Montanari
http://www.hotelsole.com - http://www.hotelsole.com/asql/index.php

Torna al forum | Feed RSS

ASPItalia.com non è responsabile per il contenuto dei messaggi presenti su questo servizio, non avendo nessun controllo sui messaggi postati nei propri forum, che rappresentano l'espressione del pensiero degli autori.