TableAdapterManager: To παυσίπονο για τα hierarchical updates


To Visual Studio 2008 διαθέτει ένα νέο component που έρχεται να βοηθήσει στη χρήση των DataSets. Μέχρι σήμερα, αν είχαμε ένα DataSet που είχε δύο ή παραπάνω πίνακες με σχέσεις PK-FK και θέλαμε να κάνουμε update στα περιεχόμενα όλων των πινάκων θα έπρεπε να κάνουμε ένα ιεραρχικό update, δηλαδή να γίνει το update με σειρά όπως:

  • Insert στο master
  • Insert στο detail
  • Update στα details
  • Update στο master
  • Delete στα details
  • Delete στο master

Το παραπάνω γίνεται με λίγες γραμμές κώδικα, χρησιμοποιώντας την GetChanges. Για παράδειγμα, αν θέλουμε να πάρουμε μόνο τις εγγραφές για inserts, θα πρέπει να πούμε κάτι σαν το παρακάτω:

Dim newOrders As NorthwindDataSet.OrdersDataTable = _
    CType(NorthwindDataSet.Orders.GetChanges( _
          Data.DataRowState.Added), _
          NorthwindDataSet.OrdersDataTable)

OrdersTableAdapter.Update(newOrders)

Βέβαια, όταν υπάρχουν πιο περίπλοκες σχέσεις με περισσότερους πίνακες, τότε χρειάζεται να γραφτεί περισσότερος κώδικας και είναι ακόμα πιο δύσκολο να διατηρηθεί η σωστή σειρά στο ιεραρχικό update. Επιπρόσθετα, ενδεχομένως να χρειάζεται να διαχειριστούμε το connection state και το transaction καθώς και να κάνουμε χειροκίνητο refresh του DataSet.

Τη λύση στο πρόβλημα έρχεται να δώσει ο TableAdapterManager. Αυτό είναι ένα component που πλέον προστίθεται αυτόματα όταν κάνει κανείς drag’n’drop κάποιο datasource στη φόρμα. Ουσιαστικά, ο νέος DataSet designer κάνει generate αυτό το component εφόσον θέσουμε σε true το "Hierarchical Update" property στο DataSet. Κατόπιν, το μόνο που χρειάζεται είναι απλά να καλέσουμε την UpdateAll του TableAdapterManager. Το component έχει επίσης το property BackUpDataSetBeforeUpdate το οποίο όταν είναι true επαναφέρει στην προηγούμενη κατάσταση το DataSet σε περίπτωση αποτυχημένου transactional update.

Μπορείτε να διαβάσετε περισσότερα στο msdn


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.