Salve a tutti, durante la progettazione di un modulo fatturazione per un CMS mi sono trovato ad affrontare il problema delle fatture, in particolare della loro numerazione:
- Numero univoco
- Azzeramento della numerazione a fine anno
- Problemi di concorrenza
Il semplice autoincrement non posso usarlo, perchè la numerazione non si può azzerare facilmente a fine anno.
Prelevare il "MAX(numero) FROM fatture" (per fare +1) potrebbe causare problemi di concorrenza se contemporaneamente viene aggiunto un altro record.
Una prima soluzione che mi è venuta in mente è stata quella di effettuare un LOCK (di tipo write) sulla tabella fatture, prelevare l'ultimo numero, inserire la fattura successiva e sbloccare la tabella.
Con questa soluzione non dovrebbero esserci particolari problemi (o mi sbaglio?), però cosa succede se durante il blocco della tabella deve essere generata una nuova fattura? Se il tutto avviene "abbastanza" velocamente la richiesta viene eseguita subito dopo lo sblocco della tabella? Avviene un errore? Va in timeout lo script?
Dalla documentazione
If the LOCK TABLES statement must wait due to locks held by other sessions on any of the tables, it blocks until all locks can be acquired.
Però non saprei cose succede nella pratica.
Con le transazioni invece dovrei poter gestire eventuali errori (uso codeigniter quindi uso la sua classe per gestire le transazioni):
Codice PHP:
$this->db->trans_start();
$this->db->query('SELECT MAX(numero)+1 AS new FROM fatture WHERE anno=YEAR(CURDATE())');
$new = ...;
$this->db->query('INSERT INTO fatture (anno,numero,..) VALUES (YEAR(CURDATE(), '.$new.', '...')');
$this->db->trans_complete();
if ($this->db->trans_status() === FALSE)
{
// ovviamente (anno,numero) sarebbe univoco, quindi in caso ci concorrenza verrebbe generato un errore
}
Questa soluzione non mi convince molto perchè a quanto so io le transazioni sono utilizzate per evitare che una serie di query collegate tra loro vengano eseguite solo in parte, in questo caso però ho solo una insert, quindi non penso faccia al caso mio, meglio la LOCK, almeno credo.
Altra soluzione che fa uso di una subquery:
Codice PHP:
INSERT INTO fatture (
anno,
numero,
...
)
VALUES (
YEAR(CURDATE()),
(
SELECT MAX(numero)+1
FROM fatture
WHERE anno=YEAR(CURDATE())
),
'...'
)';
In questo modo faccio tutto in colpo (almeno spero che le subquery vengano eseguite contestualmente alla query principale e quindi non credo che qualcuno posso inserire una nuova fattura tra la query e la subquery).
Infine un ultimo metodo che a me sembra più elegante (e funzionante), utilizzo una chiave primaria composta da (anno,numero) dove numero è auto_increment.
Codice PHP:
CREATE TABLE fatture (
anno INT(4) NOT NULL,
numero INT(10) NOT NULL AUTO_INCREMENT,
totale DECIMAL(8,2) NOT NULL,
PRIMARY KEY (anno,numero)
) ENGINE=MyISAM;
INSERT INTO `fatture` (
`anno` ,
`numero` ,
`emissione` ,
`totale`
)
VALUES (
'2010', NULL , '2010-09-56', '101.00'
);
INSERT INTO `fatture` (
`anno` ,
`numero` ,
`emissione` ,
`totale`
)
VALUES (
'2010', NULL , '2010-09-16', '101.00'
);
INSERT INTO `fatture` (
`anno` ,
`numero` ,
`emissione` ,
`totale`
)
VALUES (
'2011', NULL , '2011-01-01', '103.00'
);
Il risultato finale è il seguente:
codice:
2010 1 2010-09-15 100.00
2010 2 2010-09-16 101.00
2011 1 2011-01-01 103.00
Secondo voi quale di queste soluzioni è la migliore? Ne conoscete altre?
Fatemi sapere cosa ne pensate, in particolare dell'ultima.