Tải bản đầy đủ
Objective 2.3: Query data by using LINQ to Entities

Objective 2.3: Query data by using LINQ to Entities

Tải bản đầy đủ

This objective covers how to:
■■

Query data using LINQ operators (for example, project, skip, aggregate, filter,
and join)

■■

IQueryable versus IEnumerable

■■

Log queries

Querying data using LINQ operators
Unlike the current version of the EF, LINQ to Entities enables similar behavior, but items derive
from the ObjectContext class.
Remember that a LINQ query consists of three distinct actions:
■■

Obtain the data.

■■

Create the query.

■■

Execute the query.

For an item to be queryable through LINQ, it must implement either the IEnumerable or
IQueryable interface. LINQ to Entities uses the ObjectQuery class, which happens to implement IQueryable.
A LINQ to Entities query does almost the same thing. To create a LINQ to Entities query,
the following happens:
1. An ObjectQuery class instance is created from the ObjectContext class.
2. Compose a LINQ to Entities query using the ObjectQuery instance.
3. Convert to LINQ standard query operators or Expression Trees.
4. Execute the query.
5. Return the result to the client.

The semantics of an ObjectQuery are virtually identical to those of DbContext. You can use
an ObjectQuery or derivative directly, or you can just use one of the interfaces passing in the
object type you want to query. For instance, using the same model you have been using but
instead using LINQ to Entities, you’d query the Account entities and return an Account with
an AccountAlias property as follows:
IQueryable query = from acct in Context.Accounts
where acct.AccountAlias == "Primary"
select acct;

If you want to do the same by just retrieving one property (for instance, CreatedDate),
simply make the following change, just as you would when using the EF and DbContext:
IQueryable query = from acct in Context.Accounts
where acct.AccountAlias == "Primary"
select acct.CreatedDate;

128

CHAPTER 2

Querying and manipulating data by using the Entity Framework

You can use the same features to populate your own objects (although usually that sort of
defeats the purpose). Following is the first example creating a new Anonymous type:
IQueryable query = from acct in Context.Accounts
where acct.AccountAlias == "Primary"
select new{
AccountAlias = acct.AccountAlias,
CreatedDate = acct.CreatedDate
};

You could walk through hundreds of LINQ samples; for the purposes of this exam, just remember the difference. You use the ObjectContext class (or a derivative of it) when using LINQ
to Entities; you use the DbContext (or derivatives) when using the current version of the EF.
EXAM TIP

You can use the var keyword, but an exam question might call it out with either IQueryable
or IEnumerable. Note, however, that this doesn’t mean one or the other is being used; both
support either/both interface types.

The ObjectQuery class implements the IOrderedQueryable, IQueryable, IEnumerable, and
IListSource interfaces. As indicated previously, the two return types you can use are IQueryable and IEnumerable, which are implemented in the previous examples. As such, an ObjectQuery is the most powerful of the bunch, but each is a valid option to use when querying
LINQ to EDMs.

IEnumerable versus IQueryable
There are a few critical things you need to be able to distinguish on the exam. IEnumerable
items are characterized by:
■■

Usage in LINQ to Objects or LINQ to XML

■■

Are performed in-memory

■■

Are performed on the heap

IQueryable items are notably different in most cases. They are characterized by:
■■

Run out of process

■■

Support several different datasources including remote ones

■■

Are used in LINQ to Entity Framework, LINQ to DataServices

Logging queries
Precisely because the Entity Framework or LINQ to SQL abstract queries from the developer,
it’s often important to be able to see what SQL Is being generated. There are several ways to
accomplish this.



Objective 2.3: Query data by using LINQ to Entities

CHAPTER 2

129

To log a query when using an ObjectQuery, you can simply use the ToTraceString method.
Take a look at the following:
IQueryable Query = from Acct in Context.Accounts
where Acct.AccountAlias == "Primary"
select Acct;

You can cast this query explicitly to an ObjectQuery; upon doing so, you can trace the
query information using the following addition:
String Tracer = (Query as ObjectQuery).ToTraceString();

The same can be accomplished using the DbContext in a similar manner:
String Output = (from Acct in ContextName.ContextSet
select Acct).ToString();

Thought experiment
What can you do to examine the SQL statements being
generated to determine if they are the problem?
In the following thought experiment, apply what you’ve learned about this objective to predict what steps you need to take to build an application. You can find
answers to these questions in the “Answers” section at the end of this chapter.
You’re using LINQ to Entities and are experiencing serious performance problems.
Your database administrators (DBAs) insist that your queries are the problem, but
your developers tell you the opposite.
With this in mind, answer the following questions:

1. How can you determine the problem source?
2. What features should you implement?

Objective summary
■■

■■

■■

130

The primary difference between the current EF and LINQ to Entities is the class from
which the context inherits. LINQ to Entities inherits from ObjectContext; EF inherits
from DbContext.
Return types for LINQ to Entities must implement either the IQueryable or IEnumerable
interfaces. You can also use the generic form specifying the name of the class you want
to query.
You can retrieve the information about the query being constructed by calling the
ToTraceString method of the ObjectQuery instance.

CHAPTER 2

Querying and manipulating data by using the Entity Framework

Objective review
Answer the following questions to test your knowledge of the information in this objective.
You can find the answers to these questions and explanations of why each answer choice is
correct or incorrect in the “Answers” section at the end of this chapter.
1. You are executing a query against a LINQ to EDM that contains an Account entity,

among others. Which of the following are valid choices for storing the data?
A. IListCollection
B. ObjectQuery
C. ObjectContext.Account
D. IQueryable
2. Which interfaces must be implemented in order for something to be queried by LINQ?

(Choose all that apply.)
A. IEnumerable
B. IQueryable
C. IEntityItem
D. IDbContextItem

Objective 2.4: Query and manipulate data by using
ADO.NET
Although the EF is clearly the predominant data access path, Microsoft appears to have chosen, ADO.NET has been around for roughly 12 years and is a tried-and-true technology. There
are countless books written on ADO.NET. Although each new ADO.NET version adds features,
the basic workings and concepts remain the same.

This objective covers how to:
■■



Query data using Connection, DataReader, Command, DataAdapter, and
DataSet

■■

Perform synchronous and asynchronous operations

■■

Manage transactions (API)

Objective 2.4: Query and manipulate data by using ADO.NET

CHAPTER 2

131

Querying data using Connection, DataReader, Command,
DataAdapter, and DataSet
The Connection, DataReader, Command, DataAdapter, and Dataset/DataTable implementations are essential components of ADO.NET. Each implements a respective interface, as shown
in Table 2-3.
TABLE 2-3  ADO.NET items

Type

Interface, implementation examples

Connection

IDbConnection is the interface that all Connection objects must implement. Examples
include System.Data.SqlClient.SqlConnection and System.Data.OleDb.OleDbClient.
OleDbConnection.

DataReader

DbDataReader is the base class that all provider-specific DataReader items inherit.
Examples include System.Data.SqlClient.SqlDataReader, System.Data.OracleClient.
OracleDataReader, and System.Data.OleDb.OleDbDataReader.

Command

IDbCommand is the interface that each Command object implements. Examples include
System.Data.SqlClient.SqlCommand and System.Data.OleDb.OleDbCommand.

DataAdapter

IDbDataAdapter is the interface that DataAdapters implement. Examples include System.
Data.SqlClient.SqlDataAdapter and System.Data.OleDb.OleDbDataAdapter.

DataTable

Part of the System.Data namespace. There are no provider-specific DataTable
implementations.

DataSet

Part of the System.Data namespace. There are no provider-specific DataSet
implementations.

Although there are provider-specific implementations of each of these, the exam focuses
on the Microsoft-specific item if it is available. As such, those are the items that are covered.

SqlConnection
Any operation needs a connection to the database to do any processing. Information about
the connection can be specified through the instance’s properties directly; you can specify a
name used in the section of the .config file or it can be specified inline.
All you need to do is ensure that the connection that is opened is closed by explicitly calling
the Close method in a try/finally block or a using block:
using (SqlConnection connection = new SqlConnection("ConnectionStringName"))
{
connection.Open();
}

132

CHAPTER 2

Querying and manipulating data by using the Entity Framework

NOTE  BE SURE TO CLOSE YOUR CONNECTIONS

Although actual performance will vary from vendor to vendor, most commercial database systems are built to efficiently open and close connections. Microsoft SQL Server, for
example, actually includes a feature known as connection pooling to optimize the opening
and closing of connections with the same connection string (a more in-depth discussion is
available here: http://msdn.microsoft.com/en-us/library/vstudio/8xx3tyca(v=vs.100).aspx).
Although these systems are built to efficiently handle open/close operations, to function
correctly, connections need to close when you’re finished with them. A rule of thumb is to
open a connection at the last possible moment and close it at the first opportunity. First,
you can wrap the instantiation of any connection object in a using block. Or you can make
sure you include a try/catch/finally or try/finally block in your code, where a call to close
the connection is made in the finally block. If you don’t do either of these steps, you run
the risk of an exception being thrown somewhere along the processing path and leaving
your connections open. If this happens and your system supports any sizable degree of
volume, it won’t take long before the complaints start coming in regarding poor database
performance. This is an easy problem to avoid, so just make it a habit and include checks
for it in your team’s code reviews.

Several factors outside of your control can impede the creation and opening of a
SqlConnection. As such, you should wrap any calls to the Open method in a try/catch or try/
finally block.

SqlCommand
There are just a few major points to touch on with a SqlCommand, as follows:
■■
■■

■■

■■



A SqlCommmand needs an open and available SqlConnection to operate.
Other than the Connection property, CommandText is the only other item that you
need to call. You can specify a stored procedure name instead of a SQL statement,
but you need to specify CommandType.StoredProcedure if you are using a stored
procedure.
You should never concatenate values to build SQL strings. It leads to injection attack
vulnerabilities, and even if you can protect the input sufficiently, it is wasted effort. If
any dynamic values need to be added, use the SqlParameter class and include it in the
SqlCommand’s Parameters collection.
There is much debate (and mythology) regarding the benefits of stored procedures
versus “dynamic SQL.” If SQL commands are built using parameterization, they can
take advantage of cached execution plans that can lead to substantial performance
increases. The argument of “Stored procedures versus dynamic SQL” has raged on for
years (and probably will continue to do so), but as long as the dynamic SQL is parameterized, it will both perform better and be less vulnerable to attack.

Objective 2.4: Query and manipulate data by using ADO.NET

CHAPTER 2

133

■■

■■

■■

The SqlCommand has several execution options. You can use it as an input for a
SelectCommand property in a SqlDataAdapter, you can call the ExecuteScalar method to retrieve an individual value, or you can call the ExecuteReader to retrieve a
SqlDataReader.
Each execution method features an asynchronous counterpart that is indicated by the
prefix “Begin” at the beginning of it.
Make sure that your SqlCommand is disposed of properly, so either call Dispose when
you’re done with it or wrap it in a using block.

The following code shows how to specify both a SqlConnection and SqlCommand using
one SqlParameter named “@ID”:
using (SqlConnection sqlConnection = new SqlConnection("ConnectionStringName"))
{
connection.Open();
using (SqlCommand sqlCommand = new SqlCommand("SELECT * FROM Transactions WHERE id =
@ID", Connection))
{
sqlCommand.Parameters.AddWithValue("@ID", "IDValue")
}
}

SqlDataReader
SqlDataReader is a forward-only cursor that is the underpinning of all retrieval methods in
ADO.NET. You invoke it by declaring an instance and calling the ExecuteReader or Begin​
ExecuteReader methods of the SqlCommand class. You need an open and available connection to use it, and you should always make sure that you explicitly close any SqlDataReader
that you open.
Additionally, if you call ExecuteReader, the buffer is loaded, but it is not streamed back to
the client until you actually iterate it by calling the Read method (typically, you'll wrap the
Read method in a while loop to ensure that you iterate the complete set). You can tell if you
have values by examining the HasRows property, but there's no way to tell how many records
a SqlDataReader has without iterating it, which empties the buffer in the process.
using (SqlConnection Connection = new SqlConnection("ConnectionStringName"))
{
Connection.Open();
using (SqlCommand Command = new SqlCommand("SELECT * FROM Transactions WHERE id = @
ID", Connection))
{
Command.Parameters.AddWithValue("@ID", "IDValue");
SqlDataReader Reader = Command.ExecuteReader(CommandBehavior.CloseConnection);
while (Reader.Read())
{
// Do something.
}
}
}

134

CHAPTER 2

Querying and manipulating data by using the Entity Framework

SqlDataAdapter
SqlDataAdapter is the disconnected cousin of the SqlDataReader. After you iterate a
SqlDataReader, you cannot iterate the same query again because it is empty. The SqlDataAdapter, on the other hand, populates a DataSet (with one DataTable per query specified in
the SelectCommand property), and you can iterate the table as many times in as many directions as you see fit. You can even use LINQ semantics on it if you prefer. Additionally, if you
insert rows in the underlying SQL Server table while a SqlDataReader is iterating (assume that
you performed a query with no Restriction clause), the number of rows you end up with could
be different than what it otherwise would have been if the INSERT had not happened. The
adapter executes a query and builds the DataTable(s). You can immediately check the Count
property of the DataTable Rows property to see the number of results, which is just one more
thing differentiating the SqlDataAdapter from the SqlDataReader.
DataAdapters are designed to support the entirety of Create, Retrieve, Update, Delete
(CRUD) operations. However, they can be used comprehensively or partially, meaning that
you can use an adapter to simply perform retrieval operations, update operations, or insert
operations. Adapters can also be used with either typed or untyped datasets , which should
come as no surprise because typed datasets are simply descendants of untyped ones.

Data object lifecycle with a DataAdapter
When you call the Fill method of the DataAdapter (the most basic way to use the adapter),
you can choose to have either a typed or an untyped dataset populated.

Untyped DataSets and DataTables
Assuming that you have a valid SqlCommand (with a correctly built connection and a valid
SQL Statement) and you call Fill, the one line of code that executes actually causes a tremendous amount of work to happen. The adapter connects to the database, executes the command specified, and then handles the creation of the DataTable(s).
NOTE  AUTOGENERATION OF DATACOLUMNS

By default, the DataAdapter uses the SELECT query’s columns to generate DataColumns in
the DataTable it is populating. If there are multiple columns defined in the query that have
the same name (typically done by using an alias repeatedly), the adapter adds each of the
columns, sequentially appending an integer value to the end of the column name. For the
sake of illustration, assume that you had the following query:
SELECT FirstName, FirstName, LastName as 'FirstName' FROM SampleTable

The resulting DataTable would have three DataColumns added: FirstName1, FirstName2,
and FirstName3, respectively.



Objective 2.4: Query and manipulate data by using ADO.NET

CHAPTER 2

135

If the CommandText contains only one SELECT statement, only one DataTable is created (if
it does not exist already) and added to the DataSet. If a DataTable is used instead, it proceeds
to create the schema and populate the rows. Each column specified in the SELECT query is
examined, and a corresponding DataColumn value is added to the DataTable just created
if the schema is not already specified. It interrogates the schema information and sets the
corresponding DataColumn type. By default, DataColumns are the only items created by the
adapter. However, the overloaded Fill method can be called, specifying a value of AddWithKey to the MissingSchemeAction enumeration that will result in adding a primary key and
integrity constraints (provided, of course, that they exist in the database table).
NOTE  SETTING A PRIMARY KEY IN A DATATABLE

If a SELECT statement does not use a subquery or join, the adapter can be used to automatically generate a primary key or other constraints. If the results of the SELECT include
an OUTER JOIN or subquery, however, there could easily be more than one key involved.
There’s no natural way for the adapter to know which key should take precedence, so
it does not automatically generate one. In such cases, you need to manually create the
primary key on the DataTable object. If you are using typed DataSets, each table can have
a key designated at design time, and the integrity rules will be respected when the Fill
method is called.

In addition to adding typed DataColumns based on the query statement, keys can also be
generated by specifying the MissingSchemaAction property and setting it to Add. After the
DataTable and DataColumn items are created, it takes the query values and builds a DataRow
for each record returned. At this point, let’s make a minor digression.
If you are just using a DataAdapter to execute SELECT queries, the state of the rows and
the data they contain don’t matter. Using this approach, it is your responsibility to track the
changes to the data and decide whether it will be submitted to the database. If you plan
instead to use the adapter to update the database, the DataTable in conjunction with the
adapter will handle change tracking for you. The mechanism that’s used to handle this change
tracking is called DataRowState (which is accessed via the RowState property of a DataRow).

136

CHAPTER 2

Querying and manipulating data by using the Entity Framework

Although it depends on how InsertCommand, DeleteCommand, and UpdateCommand
are generated, each value included in the SELECT command will map back to a column in
the DataTable. A SqlParameter is created corresponding to each value, and when Update is
called, each row is interrogated and the column in the DataRow is mapped to the respective
parameter.
When the call to Fill is returned (unless you opt to set the AcceptChangesDuringFill property of the adapter to false, which is discussed in Table 2-5), each row that is returned from
the database has a RowState value of Unchanged. So assume that you had the following code:
using (SqlConnection sqlConnection = new SqlConnection("ConnectionStringName"))
{
using (SqlCommand sqlCommand = new SqlCommand("SELECT * FROM Transactions WHERE id =
@ID", sqlConnection))
{
sqlCommand.Parameters.AddWithValue("@ID", "IDValue");
using (SqlDataAdapter sqlAdapter = new SqlDataAdapter(sqlCommand)
{
DataSet currentSet = new DataSet("CurrentSet");
sqlAdapter.Fill(currentSet);
sqlAdapter.Update(currentSet);
}
}
}

The call to Update would effectively do nothing because the RowState of each value in the
DataSet would be Unchanged. If, on the other hand, you simply changed the AcceptChanges​
DuringFill property to false, as shown here, the call to Update would attempt to execute an
Update statement for each row that was returned by calling the Fill method:
sqlAdapter.AcceptChangesDuringFill = false;
sqlAdapter.Fill(CurrentSet);
sqlAdapter.Update(CurrentSet);

If a typed DataSet or DataTable is used, the overall process is virtually identical, with the
exception that the adapter isn’t responsible for creating individual tables and columns.



Objective 2.4: Query and manipulate data by using ADO.NET

CHAPTER 2

137

NOTE  TYPED VERSUS UNTYPED DATASETS AND DATATABLES

If you search .NET–related blogs, forums, and articles, there’s a lot of discussion about
the use of typed versus untyped DataSets and DataTables. There’s a lot of misinformation
regarding the respective costs and benefits of each. In many ways, the strengths of each
approach are also the weaknesses of the same approach.
Assuming that you have a valid connection and permissions, if you use a SELECT * query
with an untyped DataSet, the operation will be successful whether the underlying column
names stay the same, columns are removed, or columns are added. If on the other hand,
you use a typed DataSet/DataTable and the database columns change, you’ll likely encounter an exception. If you have a column named CurrentAge defined in a typed DataSet that’s
typed as an integer and then the database column changes to a VARCHAR, it will cause a
problem. If the same happened with an untyped DataSet/DataTable, the client-side type
would just change, and processing would occur without incident. At the same time, if your
code references the CurrentAge property and expects an integer, you can be assured that,
if the Fill operation completes successfully, the field type is an integer. If it was changed to
VARCHAR and someone entered “Sample” instead of an integer, you would encounter an
exception when Fill was called. If you used an untyped DataSet/DataTable, the Fill operation would complete successfully, but you’d encounter an exception when your code tried
to use the value.
Additionally, if you use untyped DataSets/DataTables, you can reference the rows value
only as a generically typed object. You will not get IntelliSense support, and you will have
to handle type conversion on your own. In either case, you’ll need to take steps to ensure
that the returned value is what you expect it to be, but the primary difference is when this
validation happens. If you aren’t familiar with the differences between DataSet types, you
might want to look at the following article (the typed versus untyped DataSets section in
particular): http://msdn.microsoft.com/en-us/library/8bw9ksd6(v=vs.110).aspx.

Table 2-4 shows some interesting members you should be aware of.
TABLE 2-4  Refining Sqldata adapter behavior

138

Member

Comments

AcceptChangesDuringFill

Each DataRow in a DataTable has a RowState property that indicates the state of the row. By default, the RowState property is set to Unchanged when you call the Fill method. If you set
AcceptChangesDuringFill to false, all the rows returned will have a
RowState of Added. So if you had the adapter configured with an
INSERT COMMAND, set AcceptChangesDuringFill to false, populated a
DataTable, and then passed the same DataTable back to the adapter’s
Update method, you’d end up with attempted inserts for each of those
rows (which would likely fail because of key constraints).

CHAPTER 2

Querying and manipulating data by using the Entity Framework