[ Team LiB ] |
11.2 Manipulating Data OfflineDespite all these classes, there are times when you won't want to write SQL for every database operation, and you'll want to be able to manipulate entire sets of data as a whole without maintaining an open database connection. The mechanism for such operations is the DataSet. A DataSet is an in-memory representation of a database. Just as a database contains tables, a DataSet contains a DataTableCollection (a collection of DataTable objects). Just as tables are related to one another, a DataSet contains a DataRelationCollection (a collection of DataRelation objects). Each DataTable contains a DataColumnCollection, which represents the table's columns, and a DataRowCollection, which represents the table's rows. The DataTable also contains references to various DataRelation and Constraint objects, which reflect the underlying table's relations and constraints. You can create a DataSet from scratch and fill it with data, or you can use an IDbDataAdapter to map a DataSet to a database. Figure 11-2 shows the structure of the DataSet class and its related classes. Figure 11-2. The DataSet object modelHere's where the talk about the databases meets the subject of this book, XML. You can serialize the data and structure of a DataSet to XML. You can generate a DataSet subclass from an XML Schema. You can read data from a DataSet as if it were an XML document. And finally, you can use the DataSet to track changes to a database using the DiffGram, which is, you guessed it, an XML document. 11.2.1 Creating a DataSetThe most obvious way to create a DataSet is to construct each of its objects and add them to the appropriate collections. First, create a new instance of DataSet named "AngusHardware." The DataSet represents the entire database schema: DataSet dataSet = new DataSet("AngusHardware"); Next, add a table named "customers" to the DataSet. The DataTableCollection.Add( ) method has several overloads; by passing a string parameter, you're creating a new DataTable with the given name, and adding it to the DataSet's Tables property. Add( ) returns the newly created DataTable, which you'll use to create columns: DataTable customers = dataSet.Tables.Add("customers"); Next, add a column to the "customers" table. The DataColumnCollection.Add( ) method returns the newly created DataColumn, which you'll use in a minute to assign the primary key. This Add( ) method, like the one on DataTableCollection, has several overloads. The one used here simply takes the name of the database column and the Type of the data it contains: DataColumn customersCustomerId = customers.Columns.Add("customer_id", typeof(long)); The process is similar for each column. Note that some columns are nullable in the database and others are not; the AllowDBNull property indicates whether the column is nullable: customers.Columns.Add("name",typeof(string)).AllowDBNull = false; customers.Columns.Add("address1",typeof(string)).AllowDBNull = false; customers.Columns.Add("address2",typeof(string)); customers.Columns.Add("address3",typeof(string)); customers.Columns.Add("city",typeof(string)).AllowDBNull = false; customers.Columns.Add("state",typeof(string)).AllowDBNull = false; customers.Columns.Add("zip",typeof(string)).AllowDBNull = false; The last step for the "customers" table is to set the primary key, using the customersCustomerId DataColumn created a minute ago. Although this table has a simple, one-column primary key, the DataSet allows for concatenated primary keys via an array of DataColumn objects: customers.PrimaryKey = new DataColumn [ ] {customersCustomerId}; A very similar process creates the "coupons" table: DataTable coupons = dataSet.Tables.Add("coupons"); DataColumn couponCouponCode = coupons.Columns.Add("coupon_code", typeof(string)); coupons.Columns.Add("discount_amount", typeof(Double)).AllowDBNull = false; coupons.Columns.Add("discount_type", typeof(int)).AllowDBNull = false; coupons.Columns.Add("expiration_date", typeof(DateTime)).AllowDBNull = false; coupons.PrimaryKey = new DataColumn [ ] {couponCouponCode}; And again for the "coupon_redemptions" table: DataTable couponRedemptions = dataSet.Tables.Add("coupon_redemptions"); DataColumn couponRedemptionsCouponCode = couponRedemptions.Columns.Add("coupon_code", typeof(string)); couponRedemptions.Columns.Add("total_discount", typeof(Double)).AllowDBNull = false; couponRedemptions.Columns.Add("redemption_date", typeof(DateTime)).AllowDBNull = false; DataColumn couponRedemptionsCustomerId = couponRedemptions.Columns.Add("customer_id", typeof(long)); Now that all the tables are created, it's time to assign the relations between them. The DataSet has a DataRelationCollection, whose Add( ) method has several overloads. The one used here takes the parent DataColumn and the child DataColumn. There are two relations in this example; one between coupons.coupon_code and coupon_redemptions.coupon_code, and one between customers.customer_id and coupon_redemptions.customer_id: dataSet.Relations.Add(couponCouponCode, couponRedemptionsCouponCode); dataSet.Relations.Add(customersCustomerId, couponRedemptionsCustomerId); Finally, this line writes an XML Schema document that describes the DataSet to a file: dataSet.WriteXmlSchema("Coupons.xsd"); The XML Schema that this program saved in Coupons.xsd is a normal XML Schema, and it can be used to recreate the DataSet in memory. The code to read in a DataSet's structure from a schema is very simple. In fact, it can be expressed succinctly in two statements: DataSet dataSet = new DataSet( ); dataSet.ReadXmlSchema("Coupons.xsd"); Example 11-4 shows the complete program that creates the DataSet for the coupon database, and saves an XML Schema for it. Example 11-4. Creating a DataSet for the coupon databaseusing System; using System.Data; public class CreateDataSet { public static void Main(string [ ] args) { DataSet dataSet = new DataSet("AngusHardware"); DataTable customers = dataSet.Tables.Add("customers"); DataColumn customersCustomerId = customers.Columns.Add("customer_id", typeof(long)); customers.Columns.Add("name",typeof(string)).AllowDBNull = false; customers.Columns.Add("address1",typeof(string)).AllowDBNull = false; customers.Columns.Add("address2",typeof(string)); customers.Columns.Add("address3",typeof(string)); customers.Columns.Add("city",typeof(string)).AllowDBNull = false; customers.Columns.Add("state",typeof(string)).AllowDBNull = false; customers.Columns.Add("zip",typeof(string)).AllowDBNull = false; customers.PrimaryKey = new DataColumn [ ] {customersCustomerId}; DataTable coupons = dataSet.Tables.Add("coupons"); DataColumn couponCouponCode = coupons.Columns.Add("coupon_code", typeof(string)); coupons.Columns.Add("discount_amount", typeof(Double)).AllowDBNull = false; coupons.Columns.Add("discount_type", typeof(int)).AllowDBNull = false; coupons.Columns.Add("expiration_date", typeof(DateTime)).AllowDBNull = false; coupons.PrimaryKey = new DataColumn [ ] {couponCouponCode}; DataTable couponRedemptions = dataSet.Tables.Add("coupon_redemptions"); DataColumn couponRedemptionsCouponCode = couponRedemptions.Columns.Add("coupon_code", typeof(string)); couponRedemptions.Columns.Add("total_discount", typeof(Double)).AllowDBNull = false; couponRedemptions.Columns.Add("redemption_date", typeof(DateTime)).AllowDBNull = false; DataColumn couponRedemptionsCustomerId = couponRedemptions.Columns.Add("customer_id", typeof(long)); dataSet.Relations.Add(couponCouponCode, couponRedemptionsCouponCode); dataSet.Relations.Add(customersCustomerId, couponRedemptionsCustomerId); dataSet.WriteXmlSchema("Coupons.xsd"); } } 11.2.2 Populating a DataSetThe DataSet is now ready to use just as if you had created it procedurally. You can create new rows in each of its tables, using the DataTable.NewRow( ) and DataTable.Rows.Add( ) methods, as shown in Example 11-5. Example 11-5. Populating the DataSetusing System; using System.Data; public class CreateData { public static void Main(string [ ] args) { DataSet dataSet = new DataSet( ); dataSet.ReadXmlSchema("Coupons.xsd"); DataTable couponsTable = dataSet.Tables["coupons"]; DataRow couponRow = couponsTable.NewRow( ); couponRow["coupon_code"] = "763FF"; couponRow["discount_amount"] = 0.5; couponRow["discount_type"] = DiscountType.Fixed; couponRow["expiration_date"] = new DateTime(2002,12,31); couponsTable.Rows.Add(couponRow); dataSet.WriteXml("Coupons.xml"); } } Some important highlights of this program are listed below. First, a new DataSet instance is created, and its structure is populated with the saved Coupons.xsd schema: DataSet dataSet = new DataSet( ); dataSet.ReadXmlSchema("Coupons.xsd"); Next, the "coupons" table is retrieved using the DataTableCollection's string indexer: DataTable couponsTable = dataSet.Tables["coupons"]; You can only create a new row using the DataTable's NewRow( ) factory method. This is because the columns must be populated according to the database schema stored in the DataTable. Note that the NewRow( ) method does not actually add the new DataRow to the DataTable; that happens later: DataRow couponRow = couponsTable.NewRow( ); Now you can access each column from the new DataRow and set its value: couponRow["coupon_code"] = "763FF"; couponRow["discount_amount"] = 0.5; couponRow["discount_type"] = DiscountType.Fixed; couponRow["expiration_date"] = new DateTime(2002,12,31); Now that the DataRow is fully populated with data, it's time to add it to the DataTable's DataRowCollection. If some constraint or relation was not satisfied at this point, a specific DataException is thrown, giving details as to what constraint or relation was violated: couponsTable.Rows.Add(couponRow); Finally, the last line writes the entire DataSet to an XML file: dataSet.WriteXml("Coupons.xml"); The Coupons.xml file generated by the last line is shown in Example 11-6. You can see that it's a normal XML file, and it is valid according to the schema in Coupons.xsd. Example 11-6. Coupons.xml file<?xml version="1.0" standalone="yes"?> <AngusHardware> <coupons> <coupon_code>763FF</coupon_code> <discount_amount>0.5</discount_amount> <discount_type>1</discount_type> <expiration_date>2002-12-31T00:00:00.0000000-05:00</expiration_date> </coupons> </AngusHardware> Remember, you can always verify that any XML file is valid according to a DTD or XML Schema with the XmlValidatingReader: XmlSchema schema = XmlSchema.Read( new FileStream("Coupons.xsd", FileMode.Open), null); XmlValidatingReader reader = new XmlValidatingReader( new XmlTextReader("Coupons.xml")); reader.Schemas.Add(schema); reader.ValidationType = ValidationType.Schema; while (reader.Read( )) { // this will throw an exception if invalid } You can also create an XML file that contains both the schema to define the DataSet structure and the data to populate it. The DataSet.WriteXml( ) method takes an additional optional parameter, an XmlWriteMode enumeration instance. The following list shows its values and what effect they have:
Reading a DataSet's structure and contents is done in a similar fashion. The DataSet.ReadXml( ) method takes an optional XmlReadMode enumeration parameter. The following lists its possible values and their effects:
11.2.3 Generating a DataSetI said the generated schema is a normal XML Schema document, and it is. It does, however, contain a few extra attributes with the msdata prefix. These attributes help the XSD tool to generate a subclass of DataSet with convenience methods to access tables and columns in a more type-safe manner. After running the CreateDataSet program, execute this command: xsd /dataset Coupons.xsd
The resulting file, Coupons.cs, contains the class AngusHardware, which extends DataSet, as well as a number of support classes. It's a much more complex structure than the one we generated in Chapter 8 and Chapter 9, and with good reason; it is used to create a DataSet, not just to load XML data with XmlSerializer. You can see the benefit of the generated DataSet if you compare the code in Example 11-5 with that in Example 11-7. Example 11-7. Populating a DataSet generated by xsdusing System; public class CreateData { public static void Main(string [ ] args) { AngusHardware dataSet = new AngusHardware( ); dataSet.coupons.AddcouponsRow( "763FF", 0.5, (int)DiscountType.Fixed, new DateTime(2002,12,31)); dataSet.WriteXml("Coupons.xml"); } } The generated DataSet class contains members named after the tables and columns in the schema. To start with, the name of the main class, AngusHardware, reflects the name of the DataSet. Each DataTable of the DataSet is represented by a generated private class within the DataSet; the "coupons" table, for example, is represented by the generated class AngusHardware.couponsDataTable. The AngusHardware.coupons property provides direct access to the "coupons" DataTable instance. The AngusHardware.couponsDataTable class has a method called AddcouponsRow( ), whose parameters match the columns of the table in the order in which they were added. The object model for the generated AngusHardware Dataset is represented by Figure 11-3. Figure 11-3. Generated DataSet object model11.2.4 Connecting a DataSet to the DatabaseI haven't yet shown you how to actually connect the DataSet to an actual database. This is achieved using the IDataAdapter interface, which serves as an intermediate layer between the database table and the DataSet. You specify the SQL commands to select, insert, update, and delete from each table, and then use the Fill( ) method to fill the DataSet with data from the database, or the Update( ) method to update the database with data from the DataSet. The first step is create a database connection, a SqlDataAdapter, and an AngusHardware DataSet: SqlConnection connection = new SqlConnection( "Initial Catalog=AngusHardware; User ID=sa"); SqlDataAdapter adapter = new SqlDataAdapter( ); AngusHardware dataSet = new AngusHardware( ); After that, you can create the select command for the SqlDataAdapter. This is the SqlCommand that will be used to populate the DataSet with data from the database: adapter.SelectCommand = new SqlCommand("SELECT coupon_code, " + "discount_amount, discount_type, expiration_date FROM coupons", connection); Because you'll be updating some of the data in this example and you would like those changes to be reflected in the database, the next step is to set the SqlDataAdapter's UpdateCommand property. Again, this is a normal SqlCommand, but unlike the SelectCommand it is necessary to add SqlParameters so that any updates get mapped to the correct columns: adapter.UpdateCommand = new SqlCommand( "UPDATE coupons SET coupon_code = @couponCode, discount_amount = " + "@discountAmount, discount_type = @discountType, expiration_date = " + "@expirationDate WHERE coupon_code = @couponCode", connection); adapter.UpdateCommand.Parameters.Add("@couponCode", SqlDbType.Char,10,"coupon_code"); adapter.UpdateCommand.Parameters.Add("@discountAmount", SqlDbType.Float,8,"discount_amount"); adapter.UpdateCommand.Parameters.Add("@discountType", SqlDbType.TinyInt,1,"discount_type"); adapter.UpdateCommand.Parameters.Add("@expirationDate", SqlDbType.DateTime,8,"expiration_date");
With the SqlDataAdapter all set up, the Fill( ) method is used to fill the DataSet with data from the database using the SelectCommand. The second parameter to Fill( ) tells the SqlDataAdapter the name of the DataTable to fill with data; this name can differ from the name of the database table: adapter.Fill(dataSet, "coupons"); Updating a row of data is a simple matter of locating the row of interest and setting its properties. Here we set the expiration date to the current date and time: dataSet.coupons[0].expiration_date = DateTime.Now; Since some of the data were changed, the SqlDataAdapter.Update( ) method causes the database to be updated with the changes currently in the DataSet: adapter.Update(dataSet, "coupons"); Note that although in this case the DataSet was filled, modified, and updated within the span of a single database session, the operation could just as easily have spanned a larger time. The DataSet is a disconnected view of the database, which means that a connection need not be maintained while the data are modified. Example 11-8 shows the complete program. Example 11-8. Creating a DataSet with IDataAdapterusing System; using System.Data; using System.Data.SqlClient; using System.Data.SqlTypes; public class FillDataSet { public static void Main(string [ ] args) { SqlConnection connection = new SqlConnection( "Initial Catalog=AngusHardware; User ID=sa"); SqlDataAdapter adapter = new SqlDataAdapter( ); AngusHardware dataSet = new AngusHardware( ); adapter.SelectCommand = new SqlCommand("SELECT coupon_code, " + "discount_amount, discount_type, expiration_date FROM coupons", connection); adapter.UpdateCommand = new SqlCommand( "UPDATE coupons SET coupon_code = @couponCode, discount_amount = " + "@discountAmount, discount_type = @discountType, expiration_date = " + "@expirationDate WHERE coupon_code = @couponCode", connection); adapter.UpdateCommand.Parameters.Add("@couponCode", SqlDbType.Char,10,"coupon_code"); adapter.UpdateCommand.Parameters.Add("@discountAmount", SqlDbType.Float,8,"discount_amount"); adapter.UpdateCommand.Parameters.Add("@discountType", SqlDbType.TinyInt,1,"discount_type"); adapter.UpdateCommand.Parameters.Add("@expirationDate", SqlDbType.DateTime,8,"expiration_date"); adapter.Fill(dataSet, "coupons"); dataSet.coupons[0].expiration_date = DateTime.Now; adapter.Update(dataSet, "coupons"); } } 11.2.5 Tracking Changes to a DataSetWhen making changes to a DataSet, it is often useful to keep a record of the changes. That way you can make a set of related changes on the client machine, then transmit just the changes back to the server. This technique saves network time, because the changes are all transmitted at once, and it saves bandwidth, because only the changes are transmitted. You could add another line of code to Example 11-8 to see that the DataSet maintains a "before" and "after" view of the data. Add this line before the Update( ) statement: dataSet.WriteXml(Console.Out, XmlWriteMode.DiffGram); And you'll see the following output when you run the program: <diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr= "urn:schemas-microsoft-com:xml-diffgram-v1"> <AngusHardware> <coupons diffgr:id="coupons1" msdata:rowOrder="0" diffgr:hasChanges="modified"> <coupon_code>077GH </coupon_code> <discount_amount>15</discount_amount> <discount_type>0</discount_type> <expiration_date>2002-11-09T14:17:41.6372544-05:00</expiration_date> </coupons> <coupons diffgr:id="coupons2" msdata:rowOrder="1"> <coupon_code>665RQ </coupon_code> <discount_amount>15</discount_amount> <discount_type>0</discount_type> <expiration_date>2002-11-30T00:00:00.0000000-05:00</expiration_date> </coupons> <coupons diffgr:id="coupons3" msdata:rowOrder="2"> <coupon_code>81BIN </coupon_code> <discount_amount>10</discount_amount> <discount_type>1</discount_type> <expiration_date>2003-01-31T00:00:00.0000000-05:00</expiration_date> </coupons> <coupons diffgr:id="coupons4" msdata:rowOrder="3"> <coupon_code>99GGY </coupon_code> <discount_amount>5</discount_amount> <discount_type>0</discount_type> <expiration_date>2002-12-31T00:00:00.0000000-05:00</expiration_date> </coupons> </AngusHardware> <diffgr:before> <coupons diffgr:id="coupons1" msdata:rowOrder="0"> <coupon_code>077GH </coupon_code> <discount_amount>15</discount_amount> <discount_type>0</discount_type> <expiration_date>2002-11-09T14:01:24.1830000-05:00</expiration_date> </coupons> </diffgr:before> </diffgr:diffgram> This is the DiffGram, and it shows the current state ("after") of the data in the DataSet, as well as a "before" state in the diffgr:before element. The DiffGram is an XML document that has three sections. The first, the current data instance, is represented by an XML element whose name matches the DataSet name; in this case, that's the AngusHardware element. Under the data instance element, the current state of each row in each of the DataSet's DataTables is serialized as a simple XML element. The second section, diffgr:before, lists the values of any rows that have changed before the change. And the third section, diffgr:errors, shows any errors that occurred during the generation of the DiffGram. Example 11-9 shows the general format of the DiffGram. Example 11-9. The DiffGram format<diffgr:diffgram xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" xmlns:diffgr= "urn:schemas-microsoft-com:xml-diffgram-v1"> <DataSetName> <DataTableName diffgr:id="DataTableName 1" msdata:rowOrder="0" diffgr:hasChanges="modified"> <DataColumnName>DataColumnValue </DataColumnName> <DataColumnName>DataColumnValue </DataColumnName> ... </DataTableName> <DataTableName diffgr:id="DataTableName 2" msdata:rowOrder="1"> <DataColumnName>DataColumnValue </DataColumnName> <DataColumnName>DataColumnValue </DataColumnName> ... </DataTableName> </DataSetName> <diffgr:before> <DataTableName diffgr:id="DataTableName n" msdata:rowOrder="DataRown"> <DataColumnName>DataColumnValue </DataColumnName> <DataColumnName>DataColumnValue </DataColumnName> ... </DataTableName> ... </diffgr:before> <diffgr:errors> <DataTableName diffgr:id="DataTableName n" diffgr:Error="ErrorText"/> ... </diffgr:errors> </diffgr:diffgram> The following details the DiffGram elements, attributes, and content:
Although the DiffGram is used internally by .NET for remoting and web services, it can also be used by any external system that needs to communicate database changes to a .NET DataSet. |
[ Team LiB ] |