Έχουν δημοσιευτεί Τετάρτη, 5 Νοεμβρίου 2008 8:42 μμ από το μέλος Markos

Μερικές σκέψεις πάνω στο θέμα της αποστολής Ιεραρχικών Μεταβολών (Hierarchical Changes) σε ADO.NET Databases

 

Εισαγωγή


Η ενημέρωση των μεταβολών που πραγματοποιούνται στα στοιχεία συνδεδεμένων πινάκων θεωρείται ως “advanced updating scenario”. Αδιαμφισβήτητα το ADO.NET είναι μια σπουδαία τεχνολογία, αλλά όπως είναι ήδη γνωστό, ταλαιπωρεί τους χρήστες όσον αφορά την αποστολή και αποθήκευση hierarchical data changes. Τα άρθρα και τα videos που έχουν κατά καιρούς δημοσιευτεί σε διάφορα blogs και fora, δεν μου παρέχουν μία αίσθηση πληρότητας και θεωρώ ότι το θέμα δεν έχει καθόλου, μα καθόλου, εξαντληθεί.

Ένας από τους λόγους που χρησιμοποιούνται οι βάσεις δεδομένων για την αποθήκευση της πληροφορίας, είναι η δυνατότητα διασύνδεσης των πινάκων για τη διατήρηση της σχεσιακής ακεραιότητας (referential integrity) των εγγραφών. Έτσι, λοιπόν, πιστεύω ότι δικαιούμαι να ζητάω από το ADO.NET πιο αποτελεσματική διαχείριση της πολυπλοκότητας της αποστολής των hierarchical changes προς τη βάση, ελαχιστοποιώντας την παρέμβαση του developer.

Το παρόν post φιλοδοξεί να επαναφέρει στο προσκήνιο ένα θέμα που φαίνεται να έχει ξεχαστεί λίγο και να δώσει το έναυσμα για νέες τοποθετήσεις και συζητήσεις. Όλα όσα πρόκειται ν' αναφερθούν, αφορούν στο ADO.NET 2.0 και στο Visual Studio 2005.



Θεωρητική περιγραφή


Με βάση την τεχνολογία ADO.NET η επιτυχής αποστολή και ενημέρωση των εγγραφών, όσον αφορά συνδεδεμένους πίνακες, σε μία βάση δεδομένων εξαρτάται από δύο παράγοντες: Ο ένας είναι η σειρά αποστολής των προσθηκών, μεταβολών και διαγραφών και ο άλλος είναι το SourceVersion των παραμέτρων στα Insert-, Update- και Delete- Commands των DataAdapters.

Όταν έχουμε συνδεδεμένους πίνακες (parent – child relationship), με cascade actions για update & delete, τόσο στη Βάση όσο και στο DataSet, το SourceVersion των παραμέτρων παίζει καθοριστικό ρόλο. Οι περισσότεροι πιστεύω ότι κάνουμε χρήση του DataSource Wizard για να προσθέσουμε πίνακες στον DataSet Designer. Για το λόγο αυτό, ας εξετάσουμε πρώτα τι συμβαίνει με τους TableAdapters που δημιουργεί ο Wizard μόλις προσθέσει τους πίνακες στον Designer.

Ο Wizard δημιουργεί τα Select-, Delete-, Insert- και Update- Commands τα οποία είναι υπεύθυνα για την ανάγνωση των δεδομένων από τη βάση, αλλά και την επιτυχημένη αποστολή των μεταβολών των εγγραφών που επεξεργάζεται ο χρήστης. Σ' αυτά τα Commands υπάρχουν παράμετροι. Οι παράμετροι αυτοί έχουν, συνήθως, το ίδιο όνομα με τα πεδία του πίνακα (βεβαίως, προηγείται πάντοτε ο χαρακτήρας @). Το ενδιαφέρον, τώρα, πρέπει να εστιαστεί σ' εκείνες τις παραμέτρους που έχουν πρόθεμα @Original_ και οι οποίες συναντώνται μόνο στα Delete- και Update- Commands. Κάπου εδώ τώρα, αρχίζει και γίνεται σημαντική η τιμή του SourceVersion αυτών των παραμέτρων.

Όταν αλλάζει η τιμή των πεδίων των primary keys στο parent table, αυτές οι αλλαγές γίνονται cascade στο child table (αναφέρομαι στο DataSet). Συνεπώς, όταν αποθηκεύσουμε αυτές της αλλαγές, εξαιτίας του cascade που προκαλεί η αλλαγή των τιμών των πεδίων στο parent table της βάσης, οι αλλαγές αυτές προωθούνται και στο child table της βάσης. Άρα, στο child του DataSet οι original τιμές των πεδίων του relation δε συμφωνούν με τις αντίστοιχες τιμές που έχει το child table στη βάση ύστερα από την αποθήκευση των μεταβολών στο parent. Όταν, λοιπόν, προσπαθήσουμε να αποθηκεύσουμε τις αλλαγές του child από το DataSet στη βάση, θα πάρουμε ένα ωραιότατο exception.

Για να το αποφύγουμε τη δυσάρεστη αυτή εξέλιξη, θα πρέπει το SourceVersion των παραμέτρων των πεδίων του relation στο child, για το UpdateCommand, να γίνει current. Αυτό δεν ισχύει για τις παραμέτρους του DeleteCommand μιας και οι διαγραφές αφορούν πάντα στις original τιμές. Στον πίνακα 1 που ακολουθεί, παρατίθενται οι τιμές του SourceVersion των παραμέτρων με πρόθεμα @Original_ για το UpdateCommand για συνδεδεμένους πίνακες. Η λογική που αναπτύχθηκε στην παρούσα παράγραφο για δύο πίνακες επεκτείνεται σε n-πίνακες.


Πίνακας 1. Τιμές που πρέπει να έχει το SourceVersion των παραμέτρων με πρόθεμα @Original_ του UpdateCommand για επιτυχημένη αποστολή ιεραρχικών μεταβολών (hierarchical changes) σε συνδεδεμένους πίνακες


Πίνακας

Πεδία Primary Keys

SourceVersion

Father

PK0

Original

Child(1)

PK1

PK0

Current

PK1'

Original

Child(2)

PK2

PK1 = PK0,1'

Current

PK2'

Original

|

|

|

|

Child(n)

PKn

PK(n-1) = PK0,1',...,(n-1)'

Current

PKn'

Original


Σημ.: Όπου PΚi τα πεδία που αποτελούν το PK του πίνακα-i, και PΚi' τα νέα πεδία του PK του πίνακα-i που προστίθενται σε εκείνα που 'κληρονομούνται' από το αμέσως προηγούμενο parent table.


Εντάξει με το SourceVersion των UpdateCommands. Με ποια σειρά, όμως, θα στείλουμε τις αλλαγές στη βάση; Για να απαντήσουμε σ' αυτό το ερώτημα θα χρησιμοποιήσουμε την “εις άτοπον απαγωγή”.

Ας εξετάσουμε τι θα συμβεί αν στείλουμε πρώτα τις προσθήκες. Υπάρχει περίπτωση αποτυχίας; Η απάντηση είναι ΝΑΙ! Αν κάπου στην ιεραρχία, δηλαδή σε κάποιο parent table, αλλάξει η τιμή των πεδίων του PK και στη συνέχεια προστεθούν εγγραφές στο child (φυσικά, με τις νέες τιμές για το PK), οι προσθήκες στο child ΔΕ θα αποθηκευτούν γιατί οι νέες τιμές του PK για το parent table δεν υπάρχουν ακόμα στη βάση.

Ωραία, ας στείλουμε τότε πρώτα τις μεταβολές. ΕΝΣΤΑΣΗ! Κι εδώ μπορεί να υπάρξει αποτυχία. Αν μεταβάλουμε την τιμή των πεδίων του PK στο parent και στη συνέχεια διαγράψουμε εγγραφές στο child, πως θα περάσουν οι διαγραφές στη βάση; Ας θυμηθούμε, το SourceVersion των DeleteCommands είναι original.

Ας ξεμπερδεύουμε, λοιπόν, πρώτα με τις διαγραφές. Έτσι, κι αλλιώς με SourceVersion original δεν πρόκειται ν' αποτύχουμε. Καλά μέχρι εδώ, αλλά με ποια φορά θα ξεκινήσουμε να διαγράφουμε; Μα φυσικά, από το τελευταίο child προς το αρχικό parent. Δηλαδή:

Φορά διαγραφών: Child(n) -> Child(n-1) ->-> Child(1) -> Parent


Ωραία... Πάμε τώρα να κάνουμε τις προσθήκες. Πάλι ένσταση! Ο λόγος αποτυχίας κατά την αποστολή των προσθηκών εξακολουθεί να υφίσταται. Άρα πρέπει να στείλουμε τις μεταβολές πριν τις προσθήκες. Με ποια φορά; Μα φυσικά αντίθετη από εκείνη με την οποία στείλαμε τις διαγραφές. Δηλαδή από το αρχικό parent προς το τελευταίο child:


Φορά μεταβολών: Parent -> Child(1) ->-> Child(n-1) -> Child(n)


Για το τέλος, όπως είναι πια προφανές, μας έμειναν οι προσθήκες οι οποίες κι αυτές θα γίνουν με τη φορά που έγιναν και οι μεταβολές. Δηλαδή:


Φορά προσθηκών: Parent -> Child(1) ->-> Child(n-1) -> Child(n)


Κάπου εδώ τελειώνει η θεωρητική περιγραφή. Για να δούμε, όμως, με ένα παράδειγμα πως υλοποιούνται όλα όσα περιγράφηκαν παραπάνω.



Από τη θεωρία στην πράξη


Έφτασε, λοιπόν, η στιγμή της υλοποίησης ενός παραδείγματος το οποίο θα επιβεβαιώνει όλα όσα αναφέρθηκαν στην προηγούμενη ενότητα και από πρακτικής απόψεως. Για να είναι πειστικό αυτό το παράδειγμα θα πρέπει να περιλαμβάνει τουλάχιστον τρεις πίνακες (father, child, grandchild). Όχι ότι υπάρχει κάποια διαφορά σε σχέση με το συνηθισμένο master – detail, αλλά έτσι για αλλαγή. Επίσης, καλόν είναι και τα δεδομένα να έχουν μια δόση αληθοφάνειας, μιας και οι τεχνητές “υλοποιήσεις” συνήθως είναι βαρετές.

Το παράδειγμα που επέλεξα βασίζεται στην ταξινομική των ειδών με βάση το σύστημα του Λινναίου. Στο σύστημα αυτό όλα τα είδη έχουν ονοματεπώνυμο. Συνοπτικά, οι κύριες ταξινομικές βαθμίδες των ζωντανών οργανισμών είναι Βασίλειο (πχ. Ζώα), Φύλο (πχ. Αρθρόποδα), Κλάση (πχ. Έντομα), Τάξη (πχ. Δίπτερα), Οικογένεια (πχ. Muscidae), Γένος (πχ. Musca) και Είδος (πχ. Musca domesticaοικιακή μύγα).

Ας υποθέσουμε, λοιπόν, ότι θέλουμε να φτιάξουμε μια βάση δεδομένων για φυτικά είδη που μας ενδιαφέρουν και επιθυμούμε να ιεραρχήσουμε τα είδη αυτά βάσει της ταξινομικής τους κατάταξης. Στο παράδειγμά μας θα αδιαφορήσουμε για τις ανώτερες βαθμίδες και θα επικεντρωθούμε στις τρεις τελευταίες (οικογένεια, γένος και είδος). Αμέσως, αμέσως έχουμε τρεις πίνακες με φανερή σχέση father – child – grandchild. Η βάση δεδομένων είναι απλή και τα relationships ξεκάθαρα. Για περισσότερες πληροφορίες ρίξτε μια ματιά στο συνημμένο.

Δυστυχώς, παρόλο που η υλοποίηση φαντάζει straightforward, το Visual Studio διαμαρτυρήθηκε(!?). Όταν επιχείρησα ν' αλλάξω το SourceVersion των παραμέτρων με πρόθεμα @Original_ στα UpdateCommands των πινάκων των γενών και των ειδών από τον Designer, σύμφωνα με τη θεωρητική περιγραφή, δε γινόταν build το project. Για κάποιο ανεξήγητο λόγο ο Wizard δημιουργούσε μεθόδους Update με λιγότερες παραμέτρους από εκείνες που πρέπει και το compilation αποτύγχανε. Προς στιγμή, νόμισα ότι κάτι συμβαίνει με το CommandText και γι' αυτό ζήτησα από το VS να κάνει generate τις αντίστοιχες Stored Procedures. Όπως ήταν λογικό, το πρόβλημα δε διορθώθηκε. Η μαϊμουδιά της διαγραφής των προβληματικών μεθόδων απευθείας στον generated κώδικα (μη φωνάζετε, ώρες – ώρες συμπεριφέρεται σα να το ζητάει ο οργανισμός του), κάνει compile, αλλά πετάει concurrency violation exception κατά την αποθήκευση των αλλαγών. Το γρήγορο fix, δεύτερη μαϊμουδιά, είναι να τεθεί στην ContinueUpdateOnError property του DataAdapter η τιμή true και το project παίζει. Δε ξέρω αν πρόκειται για κάποιο bug του VS2005. Θα ήταν, όμως, ενδιαφέρον εάν κάποιος αναγνώστης μπορεί να αναπαράγει το σφάλμα και να ενημερώσει για την ύπαρξή του. Ακόμη και εκείνοι που χρησιμοποιούν το VS2008 ας ενημερώσουν για το αν εμφανίζεται αντίστοιχο πρόβλημα. Πάντως, δεν υπάρχει λόγος ανησυχίας μιας και όλα μπορούν να υλοποιηθούν με τρόπο ορθόδοξο, χωρίς μαϊμουδιές.

Η λύση είναι απλή. Δεν πειράζουμε καθόλου την τιμή του SourceVersion των παραμέτρων από τον Designer και όλη τη δουλειά την κάνουμε προγραμματιστικά. Απλά, στο Load_Event της φόρμας που εμφανίζει τα δεδομένα, φροντίζουμε να καλέσουμε τις αντίστοιχες μεθόδους για να αλλάξουμε το SourceVersion (βλπ. κώδικα στο συνημμένο). Εκεί που χρησιμοποιώ methods εσείς χρησιμοποιήστε properties. Δεν είναι όμορφο να περνά ένα και μοναδικό enum σαν παράμετρος μιας μεθόδου. Ο κώδικας εμπεριέχει και κάποιο βασικό validation, αλλά όχι σπουδαία πράγματα. Υπάρχει μόνο για να μην crash-άρει η εφαρμογή. Παίξτε με τον κώδικα για να δείτε πως αντιδρά σ' αυτές τις μεταβολές το DataSet κατά την αποθήκευση.



Σχολιασμός


Η υλοποίηση που περιγράφηκε στην προηγούμενη ενότητα δίνει αρκετό υλικό για σχολιασμό, κυρίως όσον αφορά στην ίδια την τεχνολογία του ADO.NET. Ένα θέμα που με απασχολεί είναι το γεγονός ότι όταν γίνονται cascade οι αλλαγές των πεδίων του PK από το parent στο child, αλλάζει το RowVersion των DataRows στο child, ακόμα κι αν δεν υπάρχουν αλλαγές στα δεδομένα (πέραν εκείνων που αφορούν στα πεδία του PK). Αυτό έχει ως αποτέλεσμα να ζητάει το ADO.NET να γίνουν update στη βάση αλλαγές που ουσιαστικά δεν υπάρχουν και που η ίδια η βάση κάνει cascade από μόνη της από το parent στο child. Εύκολα υλοποιήσιμο workaround δε φαίνεται να υπάρχει, καθώς η AcceptChanges() αφορά σ' ολόκληρο το DataRow και όχι σε μεμονωμένα πεδία. Συνεπώς, δε μπορεί να κληθεί αβασάνιστα, επειδή υπάρχει το ενδεχόμενο οι τιμές κάποιων πεδίων των child rows να έχουν αλλάξει από το χρήστη.

Για τις διαγραφές είναι δυνατό να σκεφτεί κανείς ένα workaround. Απλά καλούμε την AcceptChanges() για διαγραφές που αφορούν σε children rows που έγιναν cascade όταν διαγράφηκε η parent row. Απαιτείται, όμως, σωστός σχεδιασμός και προσεγμένη υλοποίηση, μιας και αντιστρέφεται η σειρά αποστολής των διαγραφών στη βάση (από τα parents πια, στα children). Επίσης, ιδιαίτερη μέριμνα πρέπει να ληφθεί για την περίπτωση αποτυχίας της διαγραφής του parent record.

Υπάρχει, όμως, ακόμα ένα ζήτημα. Τελικά, πόσο επικίνδυνο είναι ο χρήστης να έχει πρόσβαση στις τιμές των PKs σ' όλα τα επίπεδα της ιεραρχίας; Η απάντηση είναι ΠΟΛΥ ΕΠΙΚΙΝΔΥΝΟ! Ο κώδικας του παραδείγματος που είναι υπεύθυνος για την αποστολή των μεταβολών στη βάση μπορεί να χαρακτηριστεί ως οδοστρωτήρας. Ακόμα κι αν οι τιμές των πεδίων των PKs είναι προστατευμένες, όπως στα walkthroughs του MSDN, αυτός ο ισοπεδωτικός τρόπος αποστολής των αλλαγών μπορεί να δημιουργήσει προβλήματα, εκτός κι αν αναφερόμαστε σε καθαρά single user εφαρμογές.

Όταν θέλουμε ν' αποθηκεύσουμε αλλαγές σε relational data, μάλλον πρέπει να σκεφτόμαστε με τη φιλοσοφία του object. Αν στο παράδειγμά μας θεωρήσουμε ότι το βασικό object είναι η οικογένεια, τότε για κάθε μία ξεχωριστά θα πρέπει να γίνει επιτυχημένη αποστολή των αλλαγών τόσο στην ίδια όσο και στα children μέσω ενός transaction. Αυτό σημαίνει ότι για κάθε μία οικογένεια θα πρέπει να αποσταλούν με τη σωστή σειρά οι διαγραφές, οι μεταβολές και οι προσθήκες μέσω ενός transaction με scope τη συγκεκριμένη οικογένεια. Έτσι, λοιπόν, ή θα πετύχουν όλες οι μεταβολές ή καμία. Μετά μπορούμε να προχωρήσουμε στην επόμενη οικογένεια κλπ. Σ' αυτή την περίπτωση πρέπει να ληφθεί μέριμνα για την ενημέρωση του DataSet όταν κάποιο(-α) transaction αποτύχει. Το DataSet είναι disconnected και δε γνωρίζει τίποτε για το “φόνο” και πρέπει να ενημερωθεί για το rollback των μεταβολών.

Συγκεκριμένη υλοποίηση κώδικα που θα επιδεικνύει την παραπάνω προσέγγιση δεν έχω προς το παρόν να σας δώσω. Ελπίζω να βρήκατε ενδιαφέροντα τα όσα αναφέρονται στην παρούσα δημοσίευση και θα χαρώ πολύ να διαβάσω τις δικές σας σκέψεις και τοποθετήσεις, μιας και το θέμα των hierarchical updates έχει πολλές ιδιαιτερότητες. Ζητώ προκαταβολικά συγνώμη για τα όποια λάθη και παραλείψεις μου.

 

Share



Καταχώρηση στις κατηγορίες:
Attachment(s): Taxonomy.zip

Σχόλια:

Χωρίς Σχόλια