* Your assessment is very important for improving the workof artificial intelligence, which forms the content of this project
Download More ASP.NET Database
Survey
Document related concepts
Transcript
More ASP.NET Database In a properly organized application, your data access code is never embedded directly in the code-behind for a page. Instead, it’s separated into a dedicated data component. Create a simple data access class, adding a separate method for each data task you need to perform. Disconnected data—the ADO.NET features that revolve around the DataSet and allow you to interact with data long after you’ve closed the connection to the datasource. The DataSet isn’t required in ASP.NET pages. However, it gives you more flexibility for navigating, filtering, and sorting your data. Building a Data Access Component • In professional applications, database code is not embedded directly in the client but encapsulated in a dedicated class. • To perform a database operation, the client creates an instance of this class and calls the appropriate method. Open and close connections quickly: • Open the database connection in every method call, and close it before the method ends. • Connections should never be held open between client requests, and the client should have no control over how connections are acquired or when they are released. • If the client does have this ability, it introduces the possibility that a connection might not be closed as quickly as possible or might be inadvertently left open, which hampers scalability. Implement error handling: • Use error handling to make sure the connection is closed even if the SQL command generates an exception. • Remember, connections are a finite resource, and using them for even a few extra seconds can have a major overall effect on performance. Follow stateless design practices: • Accept all the information needed for a method in its parameters, and return all the retrieved data through the return value. • If you create a class that maintains state, it cannot be easily implemented as a web service or used in a load-balancing scenario. • Also, if the database component is hosted out of the process, each method call has a measurable overhead, and using multiple calls to set properties will take much longer than invoking a single method with all the information as parameters. Don’t let the client specify connection string information: • This poses security risks, raises the possibility that an out-of-date client will fail, and compromises the ability of connection pooling, which requires matching connection strings. Don’t connect with the client’s user ID: • Introducing any variability into the connection string will thwart connection pooling. • Instead, rely on rolebased security or a ticketbased system whereby you authenticate users and prevent them from attempting to perform a restricted operation. • This model is also faster than trying to perform a database query under an invalid security account and waiting for an error. Don’t let the client use wideopen queries: • Every query should judiciously select only the columns it needs. • Also, you should restrict the results with a WHERE clause whenever possible. • A good, straightforward design for a database component uses a separate class for every database table (or logically related group of tables). • The common database access methods such as inserting, deleting, and modifying a record are all wrapped in separate stateless methods. • Finally, every database call uses a dedicated stored procedure. The following example demonstrates a simple database component. • Rather than placing the database code in the web page, it follows a much better design practice of separating the code into a distinct class that can be used in multiple pages. • This class can then be compiled as part of a separate component if needed. • Additionally, the connection string is retrieved from the <connectionStrings> section of the web.config file, rather than being hard-coded. • The data component actually consists of two classes—a data package class that wraps a single record of information and a database utility class that performs the actual database operations with ADO.NET code. The Data Package • To make it easier to shuffle information to the Northwind database and back, it makes sense to • create an EmployeeDetails class that provides all the fields as public properties. Here’s the full • code for this class: public class EmployeeDetails { private int employeeID; public int EmployeeID { get {return employeeID;} set {employeeID = value;} } private string firstName; The Data Package public string FirstName { get {return firstName;} set {firstName = value;} } private string lastName; public string LastName { get {return lastName;} set {lastName = value;} } The Data Package private string titleOfCourtesy; public string TitleOfCourtesy { get {return titleOfCourtesy;} set {titleOfCourtesy = value;} } public EmployeeDetails(int employeeID, string firstName, string lastName, string titleOfCourtesy) { this.employeeID = employeeID; this.firstName = firstName; this.lastName = lastName; this.titleOfCourtesy = titleOfCourtesy; } } The Stored Procedures • Before you can start coding the data access logic, you need to make sure you have the set of stored procedures you need in order to retrieve, insert, and update information. • The following code shows the five stored procedures that are needed: The Stored Procedures CREATE PROCEDURE InsertEmployee @EmployeeID int OUTPUT, @FirstName varchar(10), @LastName varchar(20), @TitleOfCourtesy varchar(25) AS INSERT INTO Employees (TitleOfCourtesy, LastName, FirstName, HireDate) VALUES (@TitleOfCourtesy, @LastName, @FirstName, GETDATE()); SET @EmployeeID = @@IDENTITY GO The Stored Procedures CREATE PROCEDURE DeleteEmployee @EmployeeID int AS DELETE FROM Employees WHERE EmployeeID = @EmployeeID GO The Stored Procedures CREATE PROCEDURE UpdateEmployee @EmployeeID int, @TitleOfCourtesy varchar(25), @LastName varchar(20), @FirstName varchar(10) AS UPDATE Employees SET TitleOfCourtesy = @TitleOfCourtesy, LastName = @LastName, FirstName = @FirstName WHERE EmployeeID = @EmployeeID GO The Stored Procedures CREATE PROCEDURE GetAllEmployees AS SELECT EmployeeID, FirstName, LastName, TitleOfCourtesy FROM Employees GO The Stored Procedures CREATE PROCEDURE CountEmployees AS SELECT COUNT(EmployeeID) FROM Employees GO The Stored Procedures CREATE PROCEDURE GetEmployee @EmployeeID int AS SELECT FirstName, LastName, TitleOfCourtesy FROM Employees WHERE EmployeeID = @EmployeeID GO The Data Utility Class • Finally, you need the utility class that performs the actual database operations. • This class uses the stored procedures that were shown previously. • In this example, the data utility class is named EmployeeDB. • It encapsulates all the data access code and database-specific details. The Data Utility Class public class EmployeeDB { private string connectionString; public EmployeeDB() { // Get default connection string. connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString; } public EmployeeDB(string connectionStringName) { // Get the specified connection string. connectionString = WebConfigurationManager.ConnectionStrings[ "connectionStringName"].ConnectionString; } The Data Utility Class public int InsertEmployee(EmployeeDetails emp) { ... } public void DeleteEmployee(int employeeID) { ... } public void UpdateEmployee(EmployeeDetails emp) { ... } public EmployeeDetails GetEmployee() { ... } public EmployeeDetails[] GetEmployees() { ... } public int CountEmployees() { ... } } The Data Utility Class Here’s the code for inserting a record: public int InsertEmployee(EmployeeDetails emp) { SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("InsertEmployee", con); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(new SqlParameter("@FirstName", SqlDbType.NVarChar, 10)); cmd.Parameters["@FirstName"].Value = emp.FirstName; cmd.Parameters.Add(new SqlParameter("@LastName", SqlDbType.NVarChar, 20)); cmd.Parameters["@LastName"].Value = emp.LastName; cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar, 25)); cmd.Parameters["@TitleOfCourtesy"].Value = emp.TitleOfCourtesy; The Data Utility Class cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4)); cmd.Parameters["@EmployeeID"].Direction = ParameterDirection.Output; try { con.Open(); cmd.ExecuteNonQuery(); return (int)cmd.Parameters["@EmployeeID"].Value; } catch (SqlException err) { // Replace the error with something less specific. // You could also log the error now. throw new ApplicationException("Data error."); } finally { con.Close(); } } The Data Utility Class • The method accepts data using the EmployeeDetails package. • Any errors are caught, and the sensitive internal details are not returned to the web-page code. • This prevents the web page from providing information that could lead to possible exploits. • This would also be an ideal place to call another method in a logging component to report the full information in an event log or another database. The Data Utility Class The GetEmployee() and GetEmployees() methods return the data using the EmployeeDetails package: public EmployeeDetails GetEmployee(int employeeID) { SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetEmployee", con); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.Add(new SqlParameter("@EmployeeID", SqlDbType.Int, 4)); cmd.Parameters["@EmployeeID"].Value = employeeID; try { con.Open(); SqlDataReader reader = cmd.ExecuteReader(CommandBehavior.SingleRow); The Data Utility Class // Get the first row. reader.Read(); EmployeeDetails emp = new EmployeeDetails( (int)reader["EmployeeID"], (string)reader["FirstName"], (string)reader["LastName"], (string)reader["TitleOfCourtesy"]); reader.Close(); return emp; } catch (SqlException err) { throw new ApplicationException("Data error."); } finally { con.Close(); } } The Data Utility Class public List<EmployeeDetails> GetEmployees() { SqlConnection con = new SqlConnection(connectionString); SqlCommand cmd = new SqlCommand("GetAllEmployees", con); cmd.CommandType = CommandType.StoredProcedure; // Create a collection for all the employee records. List<EmployeeDetails> employees = new List<EmployeeDetails>(); try { con.Open(); SqlDataReader reader = cmd.ExecuteReader(); The Data Utility Class while (reader.Read()) { EmployeeDetails emp = new EmployeeDetails( (int)reader["EmployeeID"], (string)reader["FirstName"], (string)reader["LastName"], (string)reader["TitleOfCourtesy"]); employees.Add(emp); } reader.Close(); return employees; } catch (SqlException err) { throw new ApplicationException("Data error."); } finally { con.Close(); } } Concurrency Strategies • In any multiuser application, including web applications, there’s the potential that more than oneuser will perform overlapping queries and updates. • This can lead to a potentially confusing situation where two users, who are both in possession of the current state for a row, attempt to commit divergent updates. • The first user’s update will always succeed. The success or failure of the second update is determined by your concurrency strategy. Concurrency Strategies • • There are several broad approaches to concurrency management. The most important thing to understand is that you determine your concurrency strategy by the way you write your UPDATE commands (particularly the way you shape the WHERE clause). • • Here are the most common examples: Last-in-wins updating: This is a less restrictive form of concurrency control that always commits the update (unless the original row has been deleted). Every time an update is committed, all the values are applied. Last-in-wins makes sense if data collisions are rare. For example, you can safely use this approach if there is only one person responsible for updating a given group of records. Usually, you implement a last-in-wins by writing a WHERE clause that matches the record to update based on its primary key. UPDATE Employees SET ... WHERE EmployeeID=@ID • • • • • • Concurrency Strategies • • • • • • • Match-all updating: To implement this strategy, you add a WHERE clause that tries to match the current values of every field in the record to your UPDATE statement. That way, if even a single field has been modified, the record won’t be matched and the change will not succeed. One problem with this approach is that compatible changes are not allowed. For example, if two users are attempting to modify different parts of the same record, the second user’s change will be rejected, even though it doesn’t conflict. Another, more significant, problem with the match-all updating strategy is that it leads to large, inefficient SQL statements. You can implement the same strategy more effectively with timestamps. UPDATE Employees SET ... WHERE EmployeeID=@ID AND FirstName=@FirstName AND LastName=@LastName ... Concurrency Strategies • Timestamp-based updating: Most database systems support a timestamp column, which the data source updates automatically every time a change is performed. • You do not need to modify the timestamp column manually. • However, you can examine it for changes and thereby determine if another user has recently applied an update. • If you write an UPDATE statement with a WHERE clause that incorporates the primary key and the current timestamp, you’re guaranteed to update the record only if it hasn’t been modified, just like with match-all updating. • UPDATE Employees SET ... WHERE EmployeeID=@ID AND TimeStamp=@TimeStamp Concurrency Strategies • Changed-value updating: This approach attempts to apply just the changed values in an UPDATE command, thereby allowing two users to make changes at the same time if these changes are to different fields. • The problem with this approach is it can be complex, because you need to keep track of what values have changed (in which case they should be incorporated in the WHERE clause) and what values haven’t. Testing the Component • Now that you've created the data component, you just need a simple test page to try it out. • As with any other component, you must begin by adding a reference to the component assembly. • Then you can import the namespace it uses to make it easier to implement the EmployeeDetails and EmployeeDB classes. • The only step that remains is to write the code that interacts with the classes. • In this example, the code takes place in the Page.Load event handler. • First, the code retrieves and writes the number and the list of employees by using a private WriteEmployeesList() method that translates the details to HTML. • Next, the code adds a record and lists the table content again. • Finally, the code deletes the added record and shows the content of the Employees table one more time. Testing the Component public partial class ComponentTest : System.Web.UI.Page { // Create the database component so it's available anywhere on the page. private EmployeeDB db = new EmployeeDB(); protected void Page_Load(object sender, System.EventArgs e) { WriteEmployeesList(); int empID = db.InsertEmployee( new EmployeeDetails(0, "Mr.", "Bellinaso", "Marco")); HtmlContent.Text += "<br />Inserted 1 employee.<br />"; WriteEmployeesList(); db.DeleteEmployee(empID); HtmlContent.Text += "<br />Deleted 1 employee.<br />"; WriteEmployeesList(); } Testing the Component private void WriteEmployeesList() { StringBuilder htmlStr = new StringBuilder(""); int numEmployees = db.CountEmployees(); htmlStr.Append("<br />Total employees: <b>"); htmlStr.Append(numEmployees.ToString()); htmlStr.Append("</b><br /><br />"); List<EmployeeDetails> employees = db.GetEmployees(); Testing the Component foreach (EmployeeDetails emp in employees) { htmlStr.Append("<li>"); htmlStr.Append(emp.EmployeeID); htmlStr.Append(" "); htmlStr.Append(emp.TitleOfCourtesy); htmlStr.Append(" <b>"); htmlStr.Append(emp.FirstName); htmlStr.Append("</b>, "); htmlStr.Append(emp.LastName); htmlStr.Append("</li>"); } htmlStr.Append("<br />"); HtmlContent.Text += htmlStr.ToString(); } }