Wykład 12 ADO.NET model bezpołączeniowy Klasa DataSet kolekcja DataTable Obiekty DataColumn Obiekty DataRow Ładowanie danych do obiektu DataSet użycie DataReader i DataAdapter Aktualizacja bazy za pomocą obiektu DataAdapter Definiowanie powiązań między tabelami w obiekcie DataSet Model połączeniowy czy bezpołączeniowy? 1
ADO.NET model bezpołączeniowy Model bezpołączeniowy wykorzystuje obiekt DataSet jako pamięć podręczną. Obiekt DataAdapter służy jako pośrednik pomiędzy obiektem DataSet a źródłem danych, z którego pochodzą dane w pamięci. Po załadowaniu danych, DataAdapter zwraca obiekt połączenia do puli, odłączając w ten sposób dane od źródła danych. DataAdapter jest klasą osłonową wokół obiektów DataReader dostawców, które wykonują rzeczywiste ładowanie danych. 2
3
Klasa DataSet Obiekt klasy DataSet pełni rolę bazy danych w pamięci. Jego właściwość Tables udostępnia kolekcję obiektów DataTables, które zawierają dane i opisujące je szablony. Właściwość Relations zwraca kolekcję obiektów DataRelation, które definiują powiązania między tabelami. Metody Copy(), Merge() i Clear() klasy DataSet odpowiadają za kopiowanie, scalanie i usuwanie zawartości obiektów tej klasy. DataSet i DataTable to podstawowe elementy architektury ADO.NET i w przeciwieństwie do klas, Connection, DataReader i DataAdapter nie są powiązane z konkretnym dostawcą danych. 4
Aplikacja może tworzyć, definiować i zapełniać obiekty DataSet pochodzące z dowolnego źródła danych. Hierarchia klasy DataSet 5
Klasa PropertyCollection jest zbiorem niestandardowych właściwości przechowywanych i udostępnianych jako wartość właściwości DataSet.ExtendedProperties. Tej klasy używa się do przechowywania oznaczeń czasowych lub opisowych informacji, na przykład wymagań dotyczących sprawdzania poprawności kolumn w tabelach zbioru danych. Kolekcja DataTable Kolekcja obiektów DataTable (klasa DataTableCollection) jest dostępna za pośrednictwem właściwości DataSet.Tables. Obiekty tej klasy przechowują dane w formacie wiersz - kolumna (naśladują tabele relacyjnych baz danych). Dzięki swoim właściwościom i metodom może stanowić samodzielne źródło danych lub część kolekcji tabel w obiekcie DataSet. Najważniejsze właściwości tej klasy Columns i Rows definiują układ i zawartość tabeli. 6
Obiekty DataColumn Właściwość DataTable.Columns udostępnia kolekcję obiektów DataColumn, które reprezentują wszystkie pola danych obiektu DataTable. Właściwości kolumn zebrane razem tworzą szablon danych tabeli. Właściwość ColumnName DataType MaxLength AllowDBNull ReadOnly Unique Expression Caption DataTable Opis Nazwa kolumny Typ danych przechowywanych w kolumnie Przykład: col1.datatype = System.Type.GetType("System.String") Maksymalna długość kolumny tekstowej (-1 jeżeli maksymalna długość jest nieokreślona) Określa, czy kolumna może zawierać wartości NULL Określa czy można modyfikować zawartość kolumny Określa, czy kolumna musi zawierać unikalne wartości Wyrażenie definiujące sposób obliczania wartości kolumny Przykład colvat.expression = colnetto*0.22 Nagłówek wyświetlany w interfejsie użytkownika Nazwa obiektu DataTable zawierającego daną kolumnę 7
Kolumny obiektu DataTable są tworzone automatycznie w czasie zapełniania tabeli wynikami zapytania do bazy danych lub w wyniku odczytu danych z pliku XML. W przypadku kiedy aplikacja zapełnia tabele dynamicznie (na podstawie danych podawanych przez użytkownika lub pobieranych w czasie rzeczywistym), konieczne może być napisanie kodu definiującego strukturę tabeli. Przykład przedstawia fragment kodu, który tworzy obiekt DataTable, obiekty DataColumn, przypisuje wartości do kolumn, a następnie dodaje te kolumny do obiektu DataTable. Kod zawiera również definicję kolumny wyliczanej : 8
DataTable tabela = new DataTable("Zamowienie"); DataColumn kolumna = new DataColumn("ID", Type.GetType("System.Intl6")); kolumna.unique = true; //Wartość identyfikatora musi hyc // niepowtarzalna dla każdego wiersza danych kolumna.allowdbnull = false; tabela.columns.add(kolumna); kolumna = new DataColumn("Cena", Type.GetType("System.Decimal")); tabela.columns.add(kolumna); kolumna = new DataColumn("Ilosc", Type.GetType("System.Intl6")); tabela.columns.add(kolumna); kolumna = new DataColumn("Razem", Type.GetType("System.Decimal")); kolumna.expression = "Cena*Ilosc"; tabela.columns.add(kolumna); // Wyświetla nazwy kolumn i typ danych foreach (DataColumn dc in tabela.columns) { Console.WriteLine(dc.ColumnName); Console.writeLine(dc.DataType.ToString()); } 9
Obiekty DataRow Dane są dodawane do tabeli po utworzeniu nowego obiektu DataRow, zapełnianie go danymi z kolumn i dodawanie go do kolekcji Rows tabeli. Fragment kodu przedstawia przykład umieszczenia danych w tabeli utworzonej w poprzednim przykładzie: DataRow wiersz; wiersz = tb.newrow(); // Tworzenie obiektu DataRow wiersz["cena"] = 21.85; wiersz["ilosc"] = 2; wiersz["id"] = 12001; tabela.rows.add(wiersz); //Dodawanie wiersza do kolekcji Rows Console.WriteLine(tabela.Rows[0]["Razem"].ToString(); // 43,90 10
Klasa DataTable ma metody, które umożliwiają zatwierdzanie i anulowanie zmian danych w tabeli. Obiekty tej klasy przechowują stan każdego wiersza we właściwości DataRow.RowState. Może ona przyjmować jedną z pięciu wartości wyliczenia DataRowState: Added, Deleted, Detached, Modified lub Unchanged. Rozszerzenie poprzedniego przykładu pokazuje, jak ustawiać te wartości: tabela.rows.add (wiersz); tabela.acceptchanges(); Console.Write(wiersz.RowState); tabela.rows[0].delete(); //Anulowanie usunięcia tabela.rejectchanges(); //Dodawanie //Zatwierdzanie zmian //Niezmieniony wpis // Usunięcie wpisu //Anulowanie zmian Console.Write(tabela.Rows[0].RowState); // Niezmieniony wpis DataRow myrow; myrow = tabela.newrow(): // Tworzenie obiektu 11
Metody AcceptChanges() i RejectChanges() klasy DataTable to odpowiedniki zatwierdzania i anulowania operacji w bazie danych. Metody te dotyczą wszystkich zmian wprowadzonych od czasu załadowania tabeli lub poprzedniego wywołania metody AcceptChanges(). W pokazanym przykładzie można było przywrócić usunięty wiersz, ponieważ program nie zatwierdził usunięcia przed wywołaniem metody RejectChanges(). Zmiany są wprowadzane w obiekcie DataTable, a nie w oryginalnym źródle danych. Dla każdej kolumny wiersza ADO.NET przechowuje oryginalną i aktualną wartość. Po wywołaniu metody RejectChanges() aktualne wartości są zmieniane na oryginalne wartości. W przypadku wywołania metody AcceptChanges() to wartości oryginalne zostają przekształcone na wartości aktualne. Dostęp do dwóch zbiorów wartości (przed i po zatwierdzeniu zmian) można uzyskać za pomocą wartości wyliczeniowych Current i Original właściwości DataRowVersion: 12
DataRow r = tb.rows[0]; r["cena"] = 24.95; r.acceptchanges(); r["cena"] = 19.61; Console.WriteLine("Aktualna: {0} Oryginalna: {1} ", r["cena", DataRowVersion.Current], r["cena". DataRowVersion.Original]); //Dane wyjściowe: Aktualna: 19,61 Oryginalna: 24,95 13
Ładowanie danych do obiektu DataSet użycie obiektów DataReader i DataAdapter Obiektu DataReader można używać wraz z obiektami DataSet i DataTable do zapełniania tabeli wierszami danych wygenerowanymi przez zapytanie. Wymaga to utworzenia obiektu DataReader i przekazania go jako parametru do metody DataTable.Load(): cmd.commandtext = "SELECT * FROM studenci WHERE data_ur < 1980"; DbDataReader czytacz = cmd.executereader(commandbehavior.closeconnection); DataTable tabela = new DataTable("studenci"); tabela.load(czytacz); //Ładowanie danych i schematów do tabeli Console.WriteLine(czytacz.IsClosed); //True 14
Obiekt DataReader jest zamykany automatycznie po załadowaniu wszystkich wierszy. Parametr CloseConnection gwarantuje, że zamknięte zostanie także połączenie. Jeśli tabela już zawiera dane, metoda Load() scala nowe dane z istniejącymi wierszami. Scalanie zachodzi tylko wtedy, kiedy wiersze współdzielą klucz główny. Jeśli nie ma zdefiniowanego klucza głównego, nowe wiersze zostaną dodane na koniec tabeli. Przeciążona wersja metody Load() przyjmuje drugi parametr, który określa, w jaki sposób wiersze są scalane. Ten parametr to typ wyliczeniowy LoadOption, przyjmujący jedną z trzech wartości: OverwriteChanges, PreserveChanges lub Upsert. Te opcje określają, czy w wyniku scalania zostanie nadpisany cały wiersz, tylko oryginalne wartości czy tylko aktualne wartości. 15
Poniższy fragment kodu ilustruje, jak odbywa się scalanie danych z istniejącymi wierszami w celu nadpisania aktualnych wartości kolumn: cmd.commandtext = "SELECT * FROM studenci WHERE data_ur < 1980"; DbDataReader czytacz = cmd.executereader(); DataTable tabela = new DataTable("studenci"): tabela.load(czytacz); //Ładowanie wierszy do tabeli Console.Write(tabela.Rows[0][ num_stud"]); // Przypisanie klucza głównego, aby możliwe było scalenie // wierszy DataColumn[] kolumny = new DataColumn[1]; kolumny[0] = tabela.columns["num_stud"]; tabela.primarykey = kolumny; DataRow wiersz = tabela.rows[0]; //Pobranie pierwszego wiersza danych wiersz["num_stud"] = 99999; //Zmiana aktualnej wartości kolumny // Ponieważ obiekt DataReader jest zamknięty, trzeba go ponownie zapełnić czytacz = cmd.executereader(commandbehavior.closeconnection); //Scalanie danych z aktualnymi wierszami. Nadpisanie aktualnych wartości tabela.load(czytacz.loadoption.upsert); //Zaktualizowane wartości zostały nadpisane Console.Write(tabela.Rows[0]["num_stud"]); 16
Obiekt DataAdapter może służyć do zapełniania wynikami zapytania istniejącej tabeli lub do utworzenia nowej tabeli i zapełnienia jej tymi wynikami. Pierwszy etap tej operacji to utworzenie instancji obiektu DataAdapter dla konkretnego dostawcy danych. Do dyspozycji jest kilka przeciążonych wersji konstruktora: // (1) Najłatwiejszy sposób: przekazanie jako argumentów // zapytania i ciągu znaków połączenia String sql = "SELECT * FROM studenci"; SqlDataAdapter da = new SqlDataAdapter(sql, connstr); // (2) Przypisanie obiektu polecenia do właściwości // SelectCommand SqlDataAdapter da = new SqlDataAdapter(); SqlConnection conn = new SqlConnection(connStr); da.selectcommand = new SqlCommand(sql, conn); // (3) Przekazanie ciągu znaków zapytania i obiektu połączenia SqlConnection conn = new SqlConnection(connStr); SqlDataAdapter da = new SqlDataAdapter(sql, conn); 17
Konstruktor w pierwszej wersji przyjmuje dwa ciągi znaków polecenie i połączenie. Konstruktor tworzy na ich podstawie obiekt SqlCommand i przypisuje go do właściwości SelectCommand. Nie trzeba tu pisać kodu, który jawnie tworzy obiekty SqlCommand i SqlConnection. W wersjach przeciążonych, które przyjmują jako parametr obiekt połączenia, za otwieranie i zamykanie połączenia odpowiada obiekt DataAdapter. Jeśli doda się instrukcję jawnie otwierającą połączenie, trzeba także dodać kod, który je zamyka. W przeciwnym razie obiekt DataAdapter pozostawi połączenie otwarte, co blokuje dane w bazie danych. 18
Po utworzeniu obiektu DataAdapter wykonywana jest metoda Fill(), która ładuje dane do nowej lub istniejącej tabeli: DataSet ds = new DataSet(); // Tworzenie obiektu DataTable, ładowanie danych i //dodawanie ich do DataSet // Można użyć instrukcji da.fill(ds, "studenci"), aby // określić nazwę tabeli int ilerekordow = da.fill(ds); //Zwraca liczbę załadowanych rekordów W przypadku istniejących tabel działanie polecenia Fill zależy od tego, czy tabela ma klucz główny. Jeśli tak, wiersze mające klucz pasujący do klucza nowych danych zostają zastąpione. Nowe wiersze, które nie pasują do istniejących, są dodawane do obiektu DataTable. 19
Aktualizacja bazy za pomocą obiektu DataAdapter Po zakończeniu ładowania danych przez obiekt DataAdapter, służące do tego połączenie zostaje zamknięte. Zmiany w danych są odzwierciedlane jedynie w obiekcie DataSet, a nie w oryginalnym źródle danych. Aby wprowadzić te zmiany do źródła danych, należy użyć obiektu DataAdapter w celu odtworzenia połączenia i przesłania zmienionych wierszy do oryginalnej bazy danych. Do wykonania tego zadania można użyć tego samego obiektu DataAdapter, który służył do zapełnienia obiektu DataSet. 20
Klasa DataAdapter ma trzy właściwości przypisane do określonych poleceń SQL, które wykonują zadania odpowiadające nazwom tych właściwości. InsertCommand (wstawianie), DeleteCommand (usuwanie) UpdateCommand (aktualizacja). Te polecenia są wykonywane po wywołaniu metody Update() obiektu DataAdapter. Trudność polega na utworzeniu poleceń SQL, które wysyłają zmiany, i przypisaniu ich do odpowiednich właściwości obiektu DataAdapter. Wszyscy dostawcy danych zawierają implementację klasy CommandBuilder, której można użyć do automatycznej obsługi tych operacji. 21
Obiekt CommandBuider generuje polecenia niezbędne do aktualizacji źródła danych po wprowadzeniu zmian w obiekcie DataSet. Klasa CommandBuilder jest samowystarczalna. Można utworzyć jej obiekt, przekazując do konstruktora odpowiedni obiekt DataAdapter. Po wywołaniu metody DataAdapter.Update(), zostaną wygenerowane i wykonane polecenia SQL. Przykłady: DataTable tabela = ds.tables["studenci"]; // (1) Obiektu CommandBuilder używamy do generowania poleceń // aktualizacji SqlCommandBuilder sb = new SqlCommandBuilder(da); // (2) Dodawanie studenta do tabeli DataRow wiersz = tabela.newrow(); wiersz["nazwisko"] = "Koniecpolski"; wiersz["imie"] = "Krzysztof"; wiersz["data_ur ] = "1567-11-23 ; Wiersz["plec ] = "M ; tabela.rows.add(drow); 22
// (3) Usuwanie wiersza z tabeli tabela.rows[4].delete(); // (4) Zmiana wartości kolumny tabela.rows[5]["data_ur"] = "1944": // (5) Aktualizacja oryginalnej tabeli w bazie (na serwerze) int updates = da.update(ds, "studenci"); MessageBox.Show("Zmienione wiersze: " + updates.tostring(); Jest kilka ograniczeń, o których trzeba pamiętać, korzystając z obiektu CommandBuilder. Polecenie Select powiązane z obiektem DataAdapter musi dotyczyć pojedynczej tabeli, a tabela źródłowa w bazie danych musi zawierać klucz główny lub kolumnę zawierającą niepowtarzalne wartości. Ta kolumna (lub kolumny) musi znaleźć się w oryginalnym poleceniu Select. 23
Zastosowanie obiektu DataAdapter upraszcza i automatyzuje proces aktualizacji bazy danych (dowolnego innego magazynu danych). Występują jednak problemy z aktualizacjami wielu użytkowników (współbieżne przetwarzanie transakcji). Model bezpołączeniowy wykorzystuje zasadę optymistycznej równoległości. Wiersze oryginalnego źródła danych nie są blokowane między odczytem a aktualizacją. W czasie tej przerwy inny użytkownik może zaktualizować źródło danych. Metoda Update() rozpoznaje, czy po ostatnim odczycie miały miejsce jakieś zmiany i nie aktualizuje zmienionych wierszy. Są dwie podstawowe techniki radzenia sobie z błędami przetwarzania równoległego, powstającymi przy zatwierdzaniu wielu aktualizacji. anulowanie wszystkich zmian w przypadku naruszenia zasad zatwierdzenie aktualizacji, które nie powodują błędów i wykrycie tych, które powodują, dzięki czemu można je ponownie przetworzyć. 24
Jeśli właściwość DataAdapter.ContinueUpdateOnError ma wartość false, to wówczas gdy aktualizacja wiersza nie jest możliwa, program zgłasza wyjątek. Zapobiega to wprowadzaniu kolejnych aktualizacji, ale nie wpływa na aktualizacje sprzed zgłoszenia wyjątku. Ponieważ aktualizacje mogą zależeć od siebie, aplikacje często wymagają zastosowania techniki "wszystko albo nic". Najłatwiejszy sposób na jej wdrożenie to utworzenie transakcji.net, w której wykonywane są wszystkie polecenia aktualizacji. W tym celu należy utworzyć obiekt SqlTransaction i przekazać do niego polecenie SqlDataAdapter.SelectCommand, co wiąże to polecenie z transakcją. Jeśli program zgłosi wyjątek, można użyć metody Rollback() do anulowania wszystkich zmian. Jeśli wszystkie operacje się powiodą, program wykona metodę Commit() zatwierdzającą wszystkie polecenia aktualizacji. 25
SqlDataAdapter da = new SqlDataAdapter(); SqlCommandBuilder sb = new SqlCommandBuilder(da); SqlConnection conn = new SqlConnection(connStr); conn.open (); // Trzeba otworzyć połączenie, aby użyć go do transakcji //(1) Tworzenie transakcji SqlTransaction tran = conn.begintransaction(); // (2) Wiązanie polecenia z transakcją da.selectcommand = new SqlCommand(sql, conn, tran); DataSet ds = new DataSet(); da.fill(ds, "studenci"); //Kod w poniższym fragmencie aktualizuje wiersze w DataSet try { int updates = da.update(ds, "studenci"); MessageBox.Show("Aktualizacje: " + updates.tostringo); } 26
//(3) Jeśli zostanie zgłoszony wyjątek, wszystkie aktualizacje // w transakcji zostaną anulowane catch (Exception ex) { MessageBox.Show(ex.Message); //Błąd w czasie aktualizacji } if (tran!= null) { tran. Rollback(); //Anuluje wszystkie aktualizacje } finally { tran = null; MessageBox.Show("Wszystkie aktualizacje anulowano."); // (4) Jeśli nie było błędów, wszystkie aktualizacje zostają // zatwierdzone if (tran!= null) { tran.commit(); } MessageBox.Show("Wszystkie aktualizacje zakończyły się powodzeniem."); tran = null; } conn.close(); 27
Jeżeli właściwość DataAdapter.ContinueUpdateOnError ma wartość true, przetwarzanie nie zatrzymuje się, jeśli nie można zaktualizować wiersza. Wtedy obiekt DataAdapter aktualizuje wszystkie wiersze, które nie powodują błędów. W tej sytuacji programista odpowiada za identyfikację problematycznych wierszy i określa, jak poradzić sobie z ich przetwarzaniem. Wiersze, których aktualizacja się nie powiodła, można łatwo zidentyfikować, posługując się właściwością DataRowState. Wiersze, których aktualizacja się nie powiodła, mają wartość Unchanged. Niezaktualizowane wiersze mają oryginalne wartości sprzed próby modyfikacji: Added, Deleted lub Modified. 28
// Obiekt da typu SqlDataAdapter ładuje tabelę studenci da.continueupdateonerror = true; DataSet ds = new DataSet(); try { da.fill(ds, "studenci"); } DataTable dt = ds.tables["studenci"]; SglCommandBuilder sb = new SqlCommandBuilder(da); //Przykładowe aktualizacje dt.rows[29].delete() ; //Usunięcie dt.rows[30]["data_ur"] = "1983"; //Aktualizacja dt.rows[30]["nazwisko"] = "King Kong"; //Aktualizacja dt.rows[31]["nazwisko"] = "Fantasia"; //Aktualizacja DataRow drow = dt.newrow(); drow["nazwisko"] = "Mann"; drow["imie ] = "Thomas ; drow["data_ur"] = "1897-12-13"; Drow["plec"] = "M"; dt.rows.add(drow); //Wstawianie 29
//Przesłanie aktualizacji int updates = da.update(ds, studenci"): //Poniższe wyrażenie przyjmuje wartość true, jeśli któraś z //aktualizacji się nie powiodła if (ds.haschanged()) { // Ładuje problematyczne wiersze do obiektu DataSet DataSet failures = ds.getchanges(); int rowsfailed = failures.tables[0].rows.count; Console.WriteLine("Nieudane aktualizacje: " + rowsfailed); foreach (DataRow r in failures.tables[0].rows) { string state = r.rowstate.tostring(); // Trzeba odrzucić zmiany, aby wyświetlić usunięty wiersz if (r.rowstate == DataRowState.Deleted) { r.rejectchanges(); } string ID = ((int)r["num_stud"]).tostring(); string msg = state + " ID filmu: " + ID; Console.WriteLine(msg); } } 30
Definiowanie powiązań między tabelami w obiekcie DataSet DataRelation to relacja tabela nadrzędna - tabela podrzędna między dwoma obiektami DataTable. Definiuje się ją na podstawie pasujących kolumn w obu tabelach. Kolumny muszą mieć ten sam typ danych (DataType), a kolumna w tabeli nadrzędnej musi zawierać niepowtarzalne wartości. Składnia konstruktora obiektu DataRelation wygląda następująco: public DataRelation (string nazwarelacji, DataColumn kolumnanadrzedna, DataColumn kolumnapodrzedna) Klasa DataSet ma właściwość Relations, która daje dostęp do kolekcji obiektów DataRelation zdefiniowanych dla tabel obiektu DataSet. Do umieszczania relacji w kolekcji służy metoda Relations.Add(). Przykład pokazany dalej zawiera kod, który ustanawia relację nadrzędna-podrzędna między tabelami studenci i rejestr, co pozwala na wyświetlenie listy kursów, na które zapisał się każdy ze studentów. 31
DataSet ds = new DataSet(); // (1) Zapełnianie tabeli kursami string sql = "SELECT * FROM Rejestr"; SąlConnection conn = new SqlConnection(connStr); SqlCommand cmd = new SqlCommand(); SqlDataAdapter da = new SqlDataAdapter(sql, conn); da.fill(ds, "rejestr"); // (2) Zapełnianie tabeli studentami sql = "SELECT num_stud, nazwisko, imie FROM studenci"; da.selectcommand.commandtext = sql; da.fill(ds, "studenci"); // (3) Definiowanie relacji między tabelami DataTable parent = ds.tables["studenci"]; DataTable child = ds.tables["rejestr"]; 32
DataRelation relation = new DataRelation("studenci_rejestr", parent.columns["num_stud"], child.columns["num_stud"]); // (4) Dodanie relacji do obiektu DataSet ds.relations.add(relation); // (5) Wyświetla wszystkich studentów i ich kursy foreach (DataRow r in parent.rows) { Console.WriteLine(r["nazwisko"] + r["imie ]); // Nazwisko i Imię studenta { } foreach (DataColumn rc in Console.WriteLine(" " + rc["kurs"]); r.getchildrows("studenci_rejestr")) } 33
Kiedy między dwiema tabelami zdefiniowana jest relacja, powoduje to dodanie więzów ForeignKeyConstraint do kolekcji Constraints podrzędnej tabeli DataTable. Określają one, jak zmiana lub usunięcie wierszy w tabeli nadrzędnej wpływa na tabelę podrzędną. Oznacza to, że po usunięciu wiersza w tabeli nadrzędnej wiersz w tabeli podrzędnej także może zostać usunięty lub jego klucz może przyjąć wartość null. W podobny sposób, jeśli w tabeli nadrzędnej zmianie ulegnie wartość klucza, wartość klucza wiersza w tabeli podrzędnej może ulec takiej samej zmianie lub może przyjąć wartość null. 34
Do określania obowiązujących zasad służą wartości właściwości DeleteRule i UpdateRule więzów. Mogą one przyjmować jedną z czterech wartości wyliczenia Rule: Cascade usuwa lub aktualizuje wiersze w tabeli podrzędnej. Jest to ustawienie domyślne. None nie powoduje żadnych zmian. SetDefault ustawia wartości kluczy w wierszach podrzędnych na domyślne wartości kolumny. SetNull ustawia wartości kluczy w wierszach podrzędnych na null. 35
// (1) Próba dodania do tabeli podrzędnej wiersza z // nowym kluczem DataRow row = child.newrow(); row[ num_stud"] = 999; child.rows.add (row); // Próba nieudana //Niepowodzenie 999 nie istnieje w tabeli nadrzędnej // (2) Usuwanie wiersza w tabeli nadrzędnej row = parent.rows[0]; row. Delete (); //Powoduje usunięcie wierszy w tabeli // o tym samym kluczu w tabeli podrzędnej // (3) Zwolnienie więzów i ponowna próba dodania wiersza ds.enforceconstraints = false; row[ num_stud"] = 999; child.rows.add(row); //Próba kończy się powodzeniem ds.enforceconstraints = true; //Ponowne włączenie więzów // (4) Zmiana działania więzów powodująca wstawianie // null po zmianie w tabeli nadrzędnej ((ForeignKeyConstraint)child.Constraints[0]).DeleteRule = Rule.SetNull; 36
Model połączeniowym czy bezpołączeniowy? Klasa DataReader pozwala na dostęp sekwencyjny w trybie tylko do odczytu. Przetwarzanie po jednym wierszu minimalizuje wymagania związane z pamięcią. Klasa DataSet umożliwia dostęp do danych do odczytu i do zapisu, ale wymaga wystarczającej ilości pamięci, aby pomieścić kopię danych pobranych ze źródła danych. Jeśli aplikacja nie wymaga aktualizowania źródła danych i służy jedynie do wyświetlania i wyboru danych, lepiej zastosować klasę DataReader. Jeśli aplikacja ma aktualizować dane, należy użyć klasy DataSet. Jeśli źródło danych zawiera dużą liczbę rekordów, obiekt DataSet może wymagać zbyt wielu zasobów. Jeśli dane wymagają niewielkiej liczby zmian, lepiej wykorzystać obiekty DataReader i Command do wykonywania aktualizacji. 37
Klasa DataSet stanowi dobry wybór w następujących sytuacjach: Dane są serializowane lub przesyłane przez łącza za pomocą HTTP. Formularz zawiera wiele powiązanych ze źródłem danych kontrolek tylko do odczytu. Kontrolki formularza, jak GridView lub DataView, są powiązane ze źródłem danych, które można aktualizować. Aplikacja musi zmieniać, dodawać lub usuwać wiersze danych. 38
Klasę DataReader lepiej stosować wtedy gdy: - Potrzebna jest obsługa dużej liczby rekordów, przez co wymagania związane z pamięcią i długi czas ładowania powodują, że klasa DataSet jest nieprzydatna. - Dane są tylko do odczytu i są powiązane z kontrolką listy formularza Windows lub Web. - Baza danych podlega częstym zmianom, przez co zawartość obiektu DataSet wymagałaby częstych aktualizacji. 39