TableAdapter’s Insert Update Delete Generated Commands και concurrency


Στο χθεσινό event είχαμε μια ενδιαφέρουσα συζήτηση σχετικά με τα Insert/Update/Delete commands που παράγονται κατά το configuration ενός TableAdapter και απ’ ότι είδα, ενώ τα ORMs μπαίνουν όλο και περισσότερο στην καθημερινότητά μας, υπάρχει ακόμα ανάγκη κατανόηση στον τρόπο που δουλεύουν τα DataSets, οπότε back to basics…

Λοιπόν, όταν κατασκευάζουμε ένα DataSet μπορούμε να ακολουθήσουμε την τεχνική drag / drop του πίνακα από το Server Explorer παράθυρο ή να κάνουμε δεξί κλικ στο designer και να επιλέξουμε το wizard “Add Table Adapter”. Ας πάμε με τον δεύτερο τρόπο. Επιλέγουμε ένα Northwind connection και κατόπιν “Use SQL Statements” και στο query γράφουμε:

SELECT ShipperID, CompanyName, Phone
FROM Shippers 

Πατάμε “Advanced Options” και βγάζουμε το check από τα “Use optimistic concurrency” και “Refresh the data table”. Πατάμε Next και Finish κι έχουμε φτιάξει το πρώτο DataTable που ονομάζεται “Shippers”. Ξανακάνουμε την ίδια διαδικασία χωρίς να πειράξουμε το πρώτο DataTable ώστε να φτιάξουμε ένα δεύτερο Shippers (αυτή τη φορά θα ονομαστεί “Shippers1”) χωρίς όμως να βγάλουμε τα check από τα “Use optimistic concurrency” και “Refresh the data table”. Τι έχουμε κάνει λοιπόν… Έχουμε φτιάξει δύο DataTables, το πρώτο έχει “pessimistic concurrency” και το δεύτερο “optimistic concurrency”. Τι σημαίνει αυτό: Το ADO.NET μέσω του DataSet δουλεύει σε disconnected mode. Πράγμα που σημαίνει ότι συνδεόμαστε στη βάση, παίρνουμε τα data, τα επεξεργαζόμαστε και κατόπιν τα ξαναρίχνουμε πίσω. Κατά το στάδιο της επεξεργασίας, τα data δεν έχουν κλειδωθεί στη βάση καθώς εμείς έχουμε αποσυνδεθεί, οπότε υπάρχει το ενδεχόμενο κάποιος άλλος να έχει κάνει το ίδιο πράγμα με εμάς. Έτσι για παράδειγμα υπάρχει η περίπτωση να επιχειρήσω να κάνω update σε data που υπήρχαν μεν όταν τα διάβασα αλλά πλέον έχουν σβηστεί από άλλον χρήστη. Ας δούμε λοιπόν πως χειρίζονται αυτό το θέμα τα δύο concurrency μοντέλα. Με το pessimistic concurrency υποθέτουμε ότι δεν υπάρχουν μεγάλες πιθανότητες κάποιος άλλος να αλλάξει τα data που έχω πάρει εγώ. Όταν λοιπόν επιχειρήσω να κάνω update μέσω του TableAdapter, για κάθε γραμμή από το DataTable που έχω αλλάξει, προσθέσει ή αφαιρέσει, στέλνει αντίστοιχα ένα Update, Insert ή Delete statement. Τα statements αυτά φαίνονται παρακάτω:

UPDATE Shippers 
SET CompanyName = @CompanyName, Phone = @Phone 
WHERE (ShipperID = @Original_ShipperID) 

INSERT INTO [Shippers] ([CompanyName], [Phone]) 
VALUES (@CompanyName, @Phone) 

DELETE FROM [Shippers] 
WHERE (([ShipperID] = @Original_ShipperID)) 

Παρατηρούμε ότι για να πετύχει το update και το delete θα πρέπει απλά να βρεθεί η εγγραφή για την οποία επιχειρώ να κάνω το operation. Δηλαδή

WHERE (([ShipperID] = @Original_ShipperID))

Με το optimistic concurrency υποθέτουμε υπάρχουν μεγάλες πιθανότητες κάποιος άλλος να αλλάξει τα data που έχω πάρει εγώ, οπότε θέλω να είμαι σίγουρος ότι δεν θα υπάρξει μπέρδεμα και γι αυτό πρέπει να βεβαιωθώ ότι πριν κάνω κάτι τα data δεν έχουν πειραχθεί. Έτσι τα statements έχουν ως εξής:

UPDATE [Shippers] 
SET [CompanyName] = @CompanyName, [Phone] = @Phone 
WHERE (([ShipperID] = @Original_ShipperID) AND ([CompanyName] = @Original_CompanyName) AND 
((@IsNull_Phone = 1 AND [Phone] IS NULL) OR ([Phone] = @Original_Phone))); SELECT ShipperID, CompanyName, Phone FROM Shippers WHERE (ShipperID = @ShipperID) INSERT INTO [Shippers] ([CompanyName], [Phone]) VALUES (@CompanyName, @Phone); SELECT ShipperID, CompanyName, Phone FROM Shippers WHERE (ShipperID = SCOPE_IDENTITY()) DELETE FROM [Shippers] WHERE (([ShipperID] = @Original_ShipperID) AND ([CompanyName] = @Original_CompanyName) AND
((@IsNull_Phone = 1 AND [Phone] IS NULL) OR ([Phone] = @Original_Phone)))

Κατ’ αρχήν παρατηρούμε ότι τα statements για Insert και Update είναι διπλά καθώς ακολουθεί ένα SELECT. Αυτό οφείλεται στην ενεργοποίηση της επιλογής “Refresh the data table” και θα δούμε παρακάτω που χρησιμεύει. Ερχόμαστε στο ενδιαφέρον κομμάτι: Για να πετύχει το Update και το Delete δεν αρκεί πλέον να βρεθεί η εγγραφή βάσει του @Original_ShipperID. Πρέπει να διασφαλίσουμε ότι όλα τα πεδία είναι όπως τα είχαμε διαβάσει και γι αυτό ελέγχονται ένα προς ένα. Το πρόβλημα είναι ότι ο έλεγχος πρέπει να λάβει υπόψη το NULL (αν δέχεται κάποιο πεδίο). Αν δεν είχαμε αυτό το θέμα, το WHERE θα ήταν κάπως έτσι:

WHERE (([ShipperID] = @Original_ShipperID) AND ([CompanyName] = @Original_CompanyName) 
AND ([Phone] = @Original_Phone)))

Όταν όμως έχουμε null, όπως στην περίπτωση του Phone πεδίου, το παραπάνω WHERE δεν δουλεύει. Γι αυτό ο code generator παράγει το παρακάτω:

WHERE (([ShipperID] = @Original_ShipperID) AND ([CompanyName] = @Original_CompanyName) 
AND ((@IsNull_Phone = 1 AND [Phone] IS NULL) OR ([Phone] = @Original_Phone)))

Ουσιαστικά, η προσθήκη είναι το

(@IsNull_Phone = 1 AND [Phone] IS NULL) 

Βλέπουμε ότι το παραπάνω expression γίνεται πάντα evaluate σε true ή false και ποτέ κάποιο τμήμα του δεν γίνεται evaluate σε null όπως υποστήριξε κάποιος στο event ότι συμβαίνει.

Αυτός είναι ο τρόπος που πιάνουμε τα προβλήματα στο concurrency. Για κάθε αποτυχημένο query έχουμε κι από ένα ConcurrencyException οπότε από εκεί και πέρα χρειάζεται να πάρουμε μια απόφαση για το τι θα κάνουμε. Εδώ μας βοηθάει αυτό το SELECT που ακολουθεί καθώς μας επιστρέφει τις τρέχουσες τιμές της εγγραφής κι έτσι μπορούμε να υλοποιήσουμε οποιοδήποτε μοντέλο business logic mitigation.

To θέμα του concurrency είναι πολύ σημαντικό και χρειάζεται εξοικείωση με τις τεχνικές για την αντιμετώπισή του. Τα DataSets δίνουν μια καλή ευκαιρία για να μάθει κανείς τις τεχνικές καθώς τα ίδια concepts υπάρχουν ακόμα κι αν χρησιμοποιεί κανείς κάποιο ORM.

Advertisements