LINQ to SQL introduces LINQ functionality for Microsoft SQL Server 2000 and 2005. Thanks to the IQueryable<T> interface, it's theoretically possible to create providers for other databases. Although it's not in the scope of this book, see articles from Matt Warren at his blog site for more information: http://blogs.msdn.com/mattwar/default.aspx.
LINQ to SQL defines new C# attributes, properties, and classes to let us interact with SQL Server databases by mapping database objects to objects in our programs. Three basic steps are required:
Create classes for the tables in the database that you want to use, decorating them with appropriate LINQ attributes. These classes are usually called entities.
Decorate the fields and properties in these classes so LINQ can use them and knows how to use them.
Create a DataContext object to mediate between the database tables and the classes that map to them.
The next three sections provide examples of each step.
Mapping a class to a database table allows us to use LINQ against the table. The Table attribute, defined in the System.Data.Linq namespace, informs LINQ about how to map a class to a database table.
NOTE
In this chapter we use a database called People, which has the same structure as the data source in Chapter 1. This allows you to focus on LINQ features and not on database complexities. Instructions for creating the database are included in the downloadable source code from the Source Code/Download area of www.apress.com. To run the code examples, you must create the People database first.
The following code simply declares a new public class named Person and associates it with the Person database table.
[Table(Name="Person")] public class Person
The Table attribute's Name property is optional. LINQ uses the class name as the default table name.
Mapping fields and properties to table columns makes the columns available to LINQ. Figure 2-1 shows the Person table's structure. We want to make all the columns available to LINQ as properties of the Person class. Note that the first column is the primary key. It's also an IDENTITY column, so SQL Server automatically sets its value.
For each column we want to use with LINQ, we need to declare a property and decorate it with the Column attribute. Since we're using properties, we also declare private fields for the underlying data. For the first column, ID, we declare a field, _ID, and a property, ID.
[Table(Name="Person")] public class Person { private int _ID; [Column(Name="ID", Storage="_ID", DbType="int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)] public int ID { get { return _ID; } set { _ID = value; } } }
The Column attribute has 11 properties (see Table 2-1), all of which are optional. We've used five of them. Name specifies the column name. DBType specifies not only the column's data type (int) but also that it's not nullable and is an IDENTITY column. The IsPrimaryKey property indicates that the column is part of the table's primary key. IsDbGenerated indicates that the column's value is generated by the database (which is true for all IDENTITY columns).
By default, LINQ uses a property's set and get accessors, but we can override this with the Storage property. For example, if we add the Storage property to the Column attribute for ID as follows, LINQ will use the underlying private field, _ID, instead of the accessors.
[Column(Name="ID", Storage="_ID", DbType="int NOT NULL IDENTITY", Id=true, IsDbGenerated=true)] public int ID { get { return _ID; } set { _ID = value; } } }
NOTE
LINQ can persist only class members marked with Column attributes.
Now declare the private fields and public properties for the rest of the columns. The full class code is in Listing 2-1.
[Table(Name="Person")] public class Person { private int _ID; private int _IDRole; private string _lastName; private string _firstName; [Column(Name="ID", Storage="_ID", DbType="int NOT NULL IDENTITY", IsPrimaryKey=true, IsDbGenerated=true)] public int ID { get { return _ID; } set { _ID = value; } } } [Column(Name="IDRole", Storage="_IDRole", DbType="int NOT NULL")] public int IDRole { get { return _IDRole; } set { _IDRole = value; } } [Column(Name="LastName", Storage="_lastName", DbType="nvarchar NOT NULL")] public string LastName { get { return _lastName; } set { _lastName = value; } } [Column(Name="FirstName", Storage="_firstName", DbType="nvarchar NOT NULL")] public string FirstName { get { return _firstName; } set { _firstName = value; } } } |
Property | Description |
---|---|
AutoSync | Specifies if the column is automatically synchronized from the value generated by the database on insert or update commands. Valid values for this tag are Default, Always, Never, OnInsert, and OnUpdate. |
CanBeNull | A Boolean value that indicates if the column can contain null values (true) or not (false). |
DbType | Specifies the column's data type in the database. If you omit this property, LINQ will infer the type from the class member. This property is mandatory only if you want to use the CreateDatabase method to create a new database instance. |
Expression | Defines the column as a computed column. Using this attribute you can define the formula used to compute the result. |
IsDbGenerated | Identifies a column whose value is generated by the database. Usually used in conjunction with primary key columns defined with the IDENTITY property. |
IsDiscriminator | Indicates that the member holds the discriminator value for an inheritance hierarchy. |
IsPrimaryKey | Specifies that a column is part of a table's primary (or unique) key. LINQ currently works only with tables that have primary (or unique) keys. |
IsVersion | Indicates the member is a database timestamp or version number. |
Name | Specifies the column's name in the database. Defaults to the member name. |
Storage | Specifies the name of the private field underlying a property. LINQ will bypass the property's get and set accessors and use the field instead. |
UpdateCheck | Specifies how LINQ detects optimistic concurrency conflicts. The possible values are Always, Never, and WhenChanged. If no member is marked with IsVersion=true, all members participate in detection unless explicitly specified otherwise. |
A data context is an object of type System.Data.Linq.DataContext. It supports database retrieval and update for objects known to LINQ. It handles the database connection and implements the SQO for database access. To use tables in LINQ, they must not only be mapped but must also be available in a data context. You can make them available in two ways.
One way is to create a data context and then use it to create an object that LINQ can use as a table. For example, the two lines
DataContext PeopleDataContext = new DataContext(connString); Table<Person> People = PeopleDataContext.GetTable<Person>();
create a data context, PeopleDataContext, and a Table collection, People (for the Person database table), available in that context.
A new generic collection class, Table<T>, in the System.Data.Linq namespace, is used to represent database tables. We used the data context's GetTable<T> method to create a People object of type Table<Person> in the PeopleDataContext context. The argument to the DataContext constructor is the same thing you provide an ADO.NET connection. Here is an example:
String connString = @" Data Source=.; Initial Catalog=People; Integrated Security=True ";
The result is that our database is known to LINQ as PeopleDataContext and the Person table is known as People.
NOTE
The Table<T> generic collection type implements IEnumerable<T> and IQueryable<T> as well as ITable, which implements both IEnumerable and IQueryable.
The other—and recommended—way is to use a strongly-typed data context, like the following:
public partial class PeopleDataContext : DataContext { public Table<Person> People; public PeopleDataContext(String connString) : base(connString) {} }
In this example we declare a class, PeopleDataContext, to represent the data context. The class has a field, People, for the database table Person.
The constructor calls the DataContext base constructor with the connection string.
To use the strongly typed context, we'd instantiate it before performing our first query, like this:
PeopleDataContext people = new PeopleDataContext(connString);
In this case our database is known to LINQ as people and the Person table is known as People.
We've now written all we need for LINQ to manage the Person database table as the People collection. Querying it will be similar to what we did in Chapter 1 to query in-memory objects.
The only difference in querying a database with respect to an in-memory object is that we need to instantiate our data context before our first query. In Listing 2-2, the first line of code in Main() does this.
class Program { static void Main(string[] args) { PeopleDataContext people = new PeopleDataContext(); var query = from p in people.People from s in people.Salaries where p.ID == s.ID select new { p.LastName, p.FirstName, s.Year, s.SalaryYear }; foreach(var row in query) { Console.WriteLine( "Name: {0}, {1} - Year: {2}", row.LastName,row.FirstName,row.Year); Console.WriteLine("Salary: {0}", row.SalaryYear); } } } |
The DataContext is the two-way channel by which LINQ queries the database and the results are turned into objects. Figure 2-2 shows the output of the code in Listing 2-2.
The DataContext class transforms the LINQ query into a SQL query. The Log property of the DataContext class is an easy way to determine the SQL query sent to the database. See the code snippet in Listing 2-3:
PeopleDataContext people = new PeopleDataContext(); people.Log = Console.Out; var query = from p in people.People from s in people.Salaries where p.ID == s.ID select new { p.LastName, p.FirstName, s.Year, s.SalaryYear }; foreach(var row in query) { Console.WriteLine( "Name: {0}, {1} - Year: {2}", row.LastName, row.FirstName, row.Year); Console.WriteLine("Salary: {0}", row.SalaryYear); } |
The second line redirects the log to the console, as shown in Figure 2-3.
There is another way to see the SQL sent by LINQ. Use the GetCommand method of DataContext to see SELECT statements. The method requires the LINQ query as an argument and returns an object of DBCommand class. You can use its CommandText property to retrieve the SQL command used by LINQ to SQL to retrieve records from the database. On the other hand, the GetChangeSet method is used for INSERT, UPDATE, and DELETE statements. It returns an object of ChangeSet class that contains Inserts, Updates, and Deletes properties. They give access to a list of objects that is changed after an object associated with a database table is modified. Listing 2-4 shows these two methods in action.
PeopleDataContext people = new PeopleDataContext(); var query = from p in people.People from s in people.Salaries where p.ID == s.ID select new { p.LastName, p.FirstName, s.Year, s.SalaryYear }; Console.WriteLine( people.GetCommand(query).CommandText); Console.WriteLine(); foreach(var row in query) { Console.WriteLine( "Name: {0}, {1} - Year: {2}", row.LastName, row.FirstName, row.Year); Console.WriteLine("Salary: {0}", row.SalaryYear); } Person person = new Person(); person.IDRole = 1; person.FirstName = "From"; person.LastName = "Code"; people.People. InsertOnSubmit (person); Console.WriteLine(); Console.WriteLine(people.GetChangeSet().ToString()); |
As you can see from the output shown in Figure 2-4, the ChangeSet object contains an inserted object. Using the Inserts property you can access to the new Person object added to the People collection.
|
As you can see in Listing 2-4, adding a new row consists of creating an object of a class that maps to a table, setting its values, and calling the InsertOnSubmit method on the appropriate Table<T> instance.
This is a classic object-oriented approach—adding an object to a collection—but this time you're adding a row to a database table! You don't write any SQL; everything is handled transparently by the data context. However, nothing happens in the database until you call the SubmitChanges method shown in Listing 2-5.
PeopleDataContext people = new PeopleDataContext(); Person person = new Person(); person.IDRole = 1; person.FirstName = "From"; person.LastName = "Code"; people.People.InsertOnSubmit(person); people.SubmitChanges(); var query = from p in people.People select p; foreach(var row in query) { Console.WriteLine( "Name: {0}, {1}", row.LastName, row.FirstName); } |
After the InsertOnSubmit method has modified the Person table with a new row, the SubmitChanges method will contact the database and will execute the related SQL statement. Also, the method will be able to substitute the generic @p0, @p1, and @p2 placeholders with the related value contained in the object. The output in Figure 2-5 shows that a new row has been added into the database.
Let's learn to modify and delete rows. To modify a row, we first have to retrieve the row and change the values, as in Listing 2-6.
PeopleDataContext people = new PeopleDataContext(); people.Log = Console.Out; var person = people.People.Single(p => p.ID == 5); person.FirstName = "Name"; person.LastName = "Modified"; people.SubmitChanges(); |
In the code snippet, using the Single method we retrieve the unique row whose ID is equal to 5. Then we change some attributes and call SubmitChanges() to update the database table. Figure 2-6 shows the SQL generated by LINQ.
As you can see, LINQ produces an UPDATE statement containing only the columns changed in the code. The first SELECT demonstrates that the Single method searches for the Person with an ID equal to 5.
Deleting a row is an easier process but it also involves a round trip to the database to first retrieve the row. We can use the Remove method of the DataContext class, and specify the object previously retrieved from a query. Listing 2-7 shows the code.
PeopleDataContext people = new PeopleDataContext(); people.Log = Console.Out; // Select the record to remove var person = from p in people.People where p.ID == 5 select p; people.People.DeleteOnSubmit(person.Single<Person>()); people.SubmitChanges(); |
The code retrieves a new Person object using the Single<T> generic method to retrieve only one record. Finally, the object is passed to the DeleteOnSubmit method and the changes are submitted with the SubmitChanges() method call.
3.147.71.94