More ASP.NET Database

Download Report

Transcript More ASP.NET Database

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();
}
}