Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống
1
/ 72 trang
THÔNG TIN TÀI LIỆU
Thông tin cơ bản
Định dạng
Số trang
72
Dung lượng
447,42 KB
Nội dung
422 Thinking in C# www.ThinkingIn.NET relational table data and XML) as well as making it much easier to program widely- distributed database applications. However, this model increases the possibility that two users will make incompatible modifications to related data – they’ll both reserve the last seat on the flight, one will mark an issue as resolved while the other will expand the scope of the investigation, etc. So even a minimal introduction to ADO.NET requires some discussion of the issues of concurrency violations. Getting a handle on data with DataSet The DataSet class is the root of a relational view of data. A DataSet has DataTables, which have DataColumns that define the types in DataRows. The relational database model was introduced by Edgar F. Codd in the early 1970s. The concept of tables storing data in rows in strongly-typed columns may seem to be the very definition of what a database is, but Codd’s formalization of these concepts and others such such as normalization (a process by which redundant data is eliminated and thereby ensuring the correctness and consistency of edits) was one of the great landmarks in the history of computer science. While normally one creates a DataSet based on existing data, it’s possible to create one from scratch, as this example shows: //:c10:BasicDataSetOperations.cs using System; using System.Data; class BasicDataSetOperations { public static void Main(string[] args){ DataSet ds = BuildDataSet(); PrintDataSetCharacteristics(ds); } private static DataSet BuildDataSet() { DataSet ds = new DataSet("MockDataSet"); DataTable auTable = new DataTable("Authors"); ds.Tables.Add(auTable); DataColumn nameCol = new DataColumn("Name", typeof(string)); auTable.Columns.Add(nameCol); DataRow larryRow = auTable.NewRow(); Chapter 10: Collecting Your Objects 423 larryRow["Name"] = "Larry"; auTable.Rows.Add(larryRow); DataRow bruceRow = auTable.NewRow(); bruceRow["Name"] = "Bruce"; auTable.Rows.Add(bruceRow); return ds; } private static void PrintDataSetCharacteristics( DataSet ds){ Console.WriteLine( "DataSet \"{0}\" has {1} tables", ds.DataSetName, ds.Tables.Count); foreach(DataTable table in ds.Tables){ Console.WriteLine( "Table \"{0}\" has {1} columns", table.TableName, table.Columns.Count); foreach(DataColumn col in table.Columns){ Console.WriteLine( "Column \"{0}\" contains data of type {1}", col.ColumnName, col.DataType); } Console.WriteLine( "The table contains {0} rows", table.Rows.Count); foreach(DataRow r in table.Rows){ Console.Write("Row Data: "); foreach(DataColumn col in table.Columns){ string colName = col.ColumnName; Console.Write("[{0}] = {1}", colName, r[colName]); } Console.WriteLine(); } } } }///:~ The .NET classes related to DataSets are in the System.Data namespace, so naturally we have to include a using statement at the beginning of the program. 424 Thinking in C# www.MindView.net The Main( ) method is straightforward: it calls BuildDataSet( ) and passes the object returned by that method to another static method called PrintDataSetCharacteristics( ). BuildDataSet( ) introduces several new classes. First comes a DataSet, using a constructor that allows us to simultaneously name it “MockDataSet.” Then, we declare and initialize a DataTable called “Author” which we reference with the auTable variable. DataSet objects have a Tables property of type DataTableCollection, which implements ICollection. While DataTableCollection does not implement IList, it contains some similar methods, including Add, which is used here to add the newly created auTable to ds’s Tables. DataColumns, such as the nameCol instantiated in the next line, are associated with a particular DataType. DataTypes are not nearly as extensive or extensible as normal types. Only the following can be specified as a DataType: Boolean DateTime Decimal Double Int16 Int32 Int64 SByte Single String TimeSpan UInt16 UInt32 UInt64 In this case, we specify that the “Name” column should store strings. We add the column to the Columns collection (a DataColumnCollection) of our auTable. One cannot create rows of data using a standard constructor, as a row’s structure must correspond to the Columns collection of a particular DataTable. Instead, DataRows are constructed by using the NewRow( ) method of a particular DataTable. Here, auTable.NewRow( ) returns a DataRow appropriate to our “Author” table, with its single “Name” column. DataRow does not implement ICollection, but does overload the indexing operator, so assigning a value to a column is as simple as saying: larryRow["Name"] = "Larry". The reference returned by NewRow( ) is not automatically inserted into the DataTable which generates it; that is done by: Chapter 10: Collecting Your Objects 425 auTable.Rows.Add(larryRow); After creating another row to contain Bruce’s name, the DataSet is returned to the Main( ) method, which promptly passes it to PrintDataSetCharacteristics( ). The output is: DataSet "MockDataSet" has 1 tables Table "Authors" has 1 columns Column "Name" contains data of type System.String The table contains 2 rows Row Data: [Name] = Larry Row Data: [Name] = Bruce Connecting to a database The task of actually moving data in and out of a store (either a local file or a database server on the network) is the task of the IDbConnection interface. Specifying which data (from all the tables in the underlying database) is the responsibility of objects which implement IDbCommand. And bridging the gap between these concerns and the concerns of the DataSet is the responsibility of the IDbAdapter interface. Thus, while DataSet and the classes discussed in the previous example encapsulate the “what” of the relational data, the IDataAdapter, IDbCommand, and IDbConnection encapsulate the “How”: What How DataColumn DataRow IDbCommand IDbConnection DataTable 1 *1 * 0 *0 * IDataAdapter * 11 DataSet 0 *0 * * Figure 10-7: ADO.NET separates the “What data” classes from the “How we get it” classes The .NET Framework currently ships with two managed providers that implement IDataAdapter and its related classes. One is high-performance provider 426 Thinking in C# www.ThinkingIn.NET optimized for Microsoft SQL Server; it is located in the System.Data.SqlClient namespace. The other provider, in the System.Data.OleDb namespace, is based on the broadly available Microsoft JET engine (which ships as part of Windows XP and is downloadable from Microsoft’s Website). Additionally, you can download an ODBC-suppporting managed provider from msdn.microsoft.com. One suspects that high-performance managed providers for Oracle, DB2, and other high-end databases will quietly become available as .NET begins to achieve significant market share. For the samples in this chapter, we’re going to use the OleDb classes to read and write an Access database, but we’re going to upcast everything to the ADO.NET interfaces so that the code is as general as possible. The “Northwind” database is a sample database from Microsoft that you can download from http://msdn.microsoft.com/downloads if you don’t already have it on your hard-drive from installing Microsoft Access. The file is called “nwind.mdb”. Unlike with enterprise databases, there is no need to run a database server to connect to and manipulate an Access database. Once you have the file you can begin manipulating it with .NET code. This first example shows the basic steps of connecting to a database and filling a dataset: //:c10:DBConnect.cs using System; using System.Data; using System.Data.OleDb; class BasicDataSetOperations { public static void Main(string[] args){ DataSet ds = Employees("Nwind.mdb"); Console.WriteLine( "DS filled with {0} rows", ds.Tables[0].Rows.Count); } private static DataSet Employees(string fileName){ OleDbConnection cnctn = new OleDbConnection(); cnctn.ConnectionString= "Provider=Microsoft.JET.OLEDB.4.0;" + "data source=" + fileName; DataSet ds = null; try { Chapter 10: Collecting Your Objects 427 cnctn.Open(); string selStr = "SELECT * FROM EMPLOYEES"; IDataAdapter adapter = new OleDbDataAdapter(selStr, cnctn); ds = new DataSet("Employees"); adapter.Fill(ds); } finally { cnctn.Close(); } return ds; } }///:~ After specifying that we’ll be using the System.Data and System.Data.OleDb namespaces, the Main( ) initializes a DataSet with the results of a call to the static function Employees( ). The number of rows in the first table of the result is printed to the console. The method Employees( ) takes a string as its parameter in order to clarify the part of the connection string that is variable. In this case, you’ll obviously have to make sure that the file “Nwind.mdb” is in the current directory or modify the call appropriately. The ConnectionString property is set to a bare minimum: the name of the provider we intend to use and the data source. This is all we need to connect to the Northwind database, but enterprise databases will often have significantly more complex connection strings. The call to cnctn.Open( ) starts the actual process of connecting to the database, which in this case is a local file read but which would typically be over the network. Because database connections are the prototypical “valuable non-memory resource,” as discussed in Chapter 11, we put the code that interacts with the database inside a try…finally block. As we said, the IDataAdapter is the bridge between the “how” of connecting to a database and the “what” of a particular relational view into that data. The bridge going from the database to the DataSet is the Fill( ) method (while the bridge from the DataSet to the database is the Update( ) method, which we’ll discuss in our next example). How does the IDataAdapter know what data to put into the DataSet? The answer is actually not defined at the level of IDataAdapter. The 428 Thinking in C# www.MindView.net OleDbAdapter supports several possibilities, including automatically filling the DataSet with all, or a specified subset, of records in a given table. The DBConnect example shows the use of Structured Query Language (SQL), which is probably the most general solution. In this case, the SQL query SELECT * FROM EMPLOYEES retrieves all the columns and all the data in the EMPLOYEES table of the database. The OleDbDataAdapter has a constructor which accepts a string (which it interprets as a SQL query) and an IDbConnection. This is the constructor we use and upcast the result to IDataAdapter. Now that we have our open connection to the database and an IDataAdapter, we create a new DataSet with the name “Employees.” This empty DataSet is passed in to the IDataAdapter.Fill( ) method, which executes the query via the IDbConnection, adds to the passed-in DataSet the appropriate DataTable and DataColumn objects that represent the structure of the response, and then creates and adds to the DataSet the DataRow objects that represent the results. The IDbConnection is Closed within a finally block, just in case an Exception was thrown sometime during the database operation. Finally, the filled DataSet is returned to Main( ), which dutifully reports the number of employees in the Northwind database. Fast reading with IDataReader The preferred method to get data is to use an IDataAdapter to specify a view into the database and use IDataAdapter.Fill( ) to fill up a DataSet. An alternative, if all you want is a read-only forward read, is to use an IDataReader. An IDataReader is a direct, connected iterator of the underlying database; it’s likely to be more efficient than filling a DataSet with an IDataAdapter, but the efficiency requires you to forego the benefits of a disconnected architecture. This example shows the use of an IDataReader on the Employees table of the Northwind database: //:c10:DataReader.cs using System; using System.Data; using System.Data.OleDb; class DataReader { public static void Main(){ EnumerateEmployees("Nwind.mdb"); } Chapter 10: Collecting Your Objects 429 private static void EnumerateEmployees(string fileName){ OleDbConnection cnctn = new OleDbConnection(); cnctn.ConnectionString= "Provider=Microsoft.JET.OLEDB.4.0;" + "data source=" + fileName; IDataReader rdr = null; try { cnctn.Open(); IDbCommand sel = new OleDbCommand("SELECT * FROM EMPLOYEES", cnctn); rdr = sel.ExecuteReader(); while (rdr.Read()) { Console.WriteLine(rdr["FirstName"] + " " + rdr["LastName"]); } } finally { rdr.Close(); cnctn.Close(); } } }///:~ The EnumerateEmployees( ) method starts like the code in the DBConnect example, but we do not upcast the OleDbConnection to IDbConnection for reasons we’ll discuss shortly. The connection to the database is identical, but we declare an IDataReader rdr and initialize it to null before opening the database connection; this is so that we can use the finally block to Close( ) the IDataReader as well as the OleDbConnection. After opening the connection to the database, we create an OleDbCommand which we upcast to IDbCommand. In the case of the OleDbCommand constructor we use, the parameters are a SQL statement and an OleDbConnection (thus, our inability to upcast in the first line of the method). The next line, rdr = sel.ExecuteReader( ), executes the command and returns a connected IDataReader. IDataReader.Read( ) reads the next line of the query’s result, returning false when it runs out of rows. Once all the data is read, the method enters a finally block, which severs the IDataReader’s connection with rdr.Close( ) and then closes the database connection entirely with cnctn.Close( ). 430 Thinking in C# www.ThinkingIn.NET CRUD with ADO.NET With DataSets and managed providers in hand, being able to create, read, update, and delete records in ADO.NET is near at hand. Creating data was covered in the BasicDataSetOperations example – use DataTable.NewRow( ) to generate an appropriate DataRow, fill it with your data, and use DataTable.Rows.Add( ) to insert it into the DataSet. Reading data is done in a flexible disconnected way with an IDataAdapter or in a fast but connected manner with an IDataReader. Update and delete The world would be a much pleasanter place if data never needed to be changed or erased 2 . These two operations, especially in a disconnected mode, raise the distinct possibility that two processes will attempt to perform incompatible manipulation of the same data. There are two options for a database model: ♦ Assume that any read that might end in an edit will end in an edit, and therefore not allow anyone else to do a similar editable read. This model is known as pessimistic concurrency. ♦ Assume that although people will edit and delete rows, make the enforcement of consistency the responsibility of some software component other than the database components. This is optimistic concurrency, the model that ADO.NET uses. When an IDbAdapter attempts to update a row that has been updated since the row was read, the second update fails and the adapter throws a DBConcurrencyException (note the capital ‘B’ that violates .NET’s the naming convention). As an example: 1. Ann and Ben both read the database of seats left on the 7 AM flight to Honolulu. There are 7 seats left. 2. Ann and Ben both select the flight, and their client software shows 6 seats left. 3. Ann submits the change to the database and it completes fine. 4. Charlie reads the database, sees 6 seats available on the flight. 2 Not only would it please the hard drive manufacturers, it would provide a way around the second law of thermodynamics. See, for instance, http://www.media.mit.edu/physics/publications/papers/96.isj.ent.pdf Chapter 10: Collecting Your Objects 431 5. Ben submits the change to the database. Because Ann’s update happened before Ben’s update, Ben receives a DBConcurrencyException. The database does not accept Ben’s change. 6. Charlie selects a flight and submits the change. Because the row hasn’t changed since Charlie read the data, Charlie’s request succeeds. It is impossible to give even general advice as to what to do after receiving a DBConcurrencyException. Sometimes you’ll want to take the data and re-insert it into the database as a new record, sometimes you’ll discard the changes, and sometimes you’ll read the new data and reconcile it with your changes. There are even times when such an exception indicates a deep logical flaw that calls for a system shutdown. This example performs all of the CRUD operations, rereading the database after the update so that the subsequent deletion of the new record does not throw a DBConcurrencyException: //:c10:Crud.cs using System; using System.Data; using System.Data.OleDb; class Crud { public static void Main(string[] args){ Crud myCrud = new Crud(); myCrud.ReadEmployees("NWind.mdb"); myCrud.Create(); myCrud.Update(); //Necessary to avoid DBConcurrencyException myCrud.Reread(); myCrud.Delete(); } OleDbDataAdapter adapter; DataSet emps; private void ReadEmployees(string pathToAccessDB){ OleDbConnection cnctn = new OleDbConnection(); cnctn.ConnectionString = "Provider=Microsoft.JET.OLEDB.4.0;" + "data source=" + pathToAccessDB; [...]... container that encapsulates an array of string, and that only adds strings and gets strings, so that there are no casting issues during use If the internal array isn’t big enough for the next add, your container should Thinking in C# www.MindView.net automatically resize it In Main( ), compare the performance of your container with an ArrayList holding strings 5 Create a class containing two string... MyException(string msg, Exception inner) : base(msg, inner){} } public class FullConstructors { public static void F() { Console.WriteLine( "Throwing MyException from F()"); throw new MyException(); } public static void G() { Console.WriteLine( "Throwing MyException from G()"); throw new MyException("Originated in G()"); } public static void H(){ try { I(); 4 46 Thinking in C# www.ThinkingIn.NET } catch... helplink The Exception class contains a string property called HelpLink This property is intended to hold a URI and the NET Framework SDK documentation suggests that you might refer to a helpfile explaining the error On the other hand, as we’ll 444 Thinking in C# www.MindView.net discuss in Chapter 18, a URI is all you need to call a Web Service One can imagine using Exception.HelpLink and a little ingenuity... what to do when things go wrong! 450 Thinking in C# www.ThinkingIn.NET C# s lack of checked exceptions Some languages, notably Java, require a method to list recoverable exceptions it may throw Thus, in Java, reading data from a stream is done with a method that is declared as int read() throws IOException while the equivalent method in C# is simply int read() This does not mean that C# somehow avoids... is: Throwing MyException2 from F() at ExtraFeatures.F() in D:\tic\exceptions\ExtraFeatures.cs:line 23 at ExtraFeatures.Main(String[] args) in C:\Documents and Settings\larry\My Documents\ExtraFeatures.cs:line 38 Throwing MyException2 from G() at ExtraFeatures.G() in D:\tic\exceptions\ExtraFeatures.cs:line 28 at ExtraFeatures.Main(String[] args) in D:\tic\exceptions\ExtraFeatures.cs:line 43 Throwing MyException2... is: ThreeException In finally clause ThreeException In finally clause ThreeException In finally clause No exception In finally clause Whether an exception is thrown or not, the finally clause is always executed What’s finally for? Since C# has a garbage collector, releasing memory is virtually never a problem So why do you need finally? finally is necessary when you need to set something other than memory... static void Main() { while (true) { try { if (count++ < 3) { throw new ThreeException(); 4 56 Thinking in C# www.MindView.net } Console.WriteLine("No exception"); } catch (ThreeException ) { Console.WriteLine("ThreeException"); } finally { Console.Error.WriteLine( "In finally clause"); //! if(count == 3) break; 3) break; } } } ///:~ This program also gives a hint for how... relational model have any concept of inheritance Worse, it’s become apparent over the years that there’s no single strategy for mapping between objects and tables that is appropriate for all needs Thinking in Databases would be a very different book than Thinking in C# The object and relational models are very different, but contain just enough similarities so that the pain hasn’t been enough to trigger... 454 Thinking in C# www.ThinkingIn.NET } } ///:~ In this example, the class Transaction has an exception class that is at its same level of abstraction in TransactionFailureException The try…catch(Exception e) construct in Transaction.Process( ) makes for a nice and explicit contract: “I try to return void, but if anything goes awry in my processing, I may throw a TransactionFailedException.” In order... 452 Thinking in C# www.MindView.net methods from object (everybody’s base type) The one that might come in handy for exceptions is GetType( ), which returns an object representing the class of this You can in turn read the Name property of this Type object You can also do more sophisticated things with Type objects that aren’t necessary in exception handling Type objects will be studied later in this . normal types. Only the following can be specified as a DataType: Boolean DateTime Decimal Double Int 16 Int32 Int64 SByte Single String TimeSpan UInt 16 UInt32 UInt64 In this case, we specify. 430 Thinking in C# www.ThinkingIn.NET CRUD with ADO.NET With DataSets and managed providers in hand, being able to create, read, update, and delete records in ADO.NET is near at hand. Creating. manipulate the DataSet. The final line calls IDataAdapter.Update( ), which attempts to commit the changes in the DataSet 434 Thinking in C# www.ThinkingIn.NET to the backing store (it is this method