Working with Computed Columns (VB)
When creating a database table, Microsoft SQL Server allows you to define a computed column whose value is calculated from an expression that usually references other values in the same database record. Such values are read-only at the database, which requires special considerations when working with TableAdapters. In this tutorial we learn how to meet the challenges posed by computed columns.
Introduction
Microsoft SQL Server allows for computed columns, which are columns whose values are calculated from an expression that usually references the values from other columns in the same table. As an example, a time tracking data model might have a table named ServiceLog
with columns including ServicePerformed
, EmployeeID
, Rate
, and Duration
, among others. While the amount due per service item (being the rate multiplied by the duration) could be calculated through a web page or other programmatic interface, it might be handy to include a column in the ServiceLog
table named AmountDue
that reported this information. This column could be created as a normal column, but it would need to be updated anytime the Rate
or Duration
column values changed. A better approach would be to make the AmountDue
column a computed column using the expression Rate * Duration
. Doing so would cause SQL Server to automatically calculate the AmountDue
column value whenever it was referenced in a query.
Since a computed column s value is determined by an expression, such columns are read-only and therefore cannot have values assigned to them in INSERT
or UPDATE
statements. However, when computed columns are part of the main query for a TableAdapter that uses ad-hoc SQL statements, they are automatically included in the auto-generated INSERT
and UPDATE
statements. Consequently, the TableAdapter s INSERT
and UPDATE
queries and InsertCommand
and UpdateCommand
properties must be updated to remove references to any computed columns.
One challenge of using computed columns with a TableAdapter that uses ad-hoc SQL statements is that the TableAdapter s INSERT
and UPDATE
queries are automatically regenerated any time the TableAdapter Configuration wizard is completed. Therefore, the computed columns manually removed from the INSERT
and UPDATE
queries will reappear if the wizard is re-run. Although TableAdapters that use stored procedures don t suffer from this brittleness, they do have their own quirks that we will address in Step 3.
In this tutorial we will add a computed column to the Suppliers
table in the Northwind database and then create a corresponding TableAdapter to work with this table and its computed column. We will have our TableAdapter use stored procedures instead of ad-hoc SQL statements so that our customizations aren t lost when the TableAdapter Configuration wizard is used.
Let s get started!
Step 1: Adding a Computed Column to theSuppliers
Table
The Northwind database does not have any computed columns so we will need to add one ourselves. For this tutorial let s add a computed column to the Suppliers
table called FullContactName
that returns the contact s name, title, and the company they work for in the following format: ContactName
(ContactTitle
, CompanyName
). This computed column might be used in reports when displaying information about suppliers.
Start by opening the Suppliers
table definition by right-clicking on the Suppliers
table in the Server Explorer and choosing Open Table Definition from the context-menu. This will display the columns of the table and their properties, such as their data type, whether they allow NULL
s, and so forth. To add a computed column, start by typing in the name of the column into the table definition. Next, enter its expression into the (Formula) textbox under the Computed Column Specification section in the Column Properties window (see Figure 1). Name the computed column FullContactName
and use the following expression:
[!code-sqlMain]
1: ContactName + ' (' + CASE WHEN ContactTitle IS NOT NULL THEN
2: ContactTitle + ', ' ELSE '' END + CompanyName + ')'
Note that strings can be concatenated in SQL using the +
operator. The CASE
statement can be used like a conditional in a traditional programming language. In the above expression the CASE
statement can be read as: If ContactTitle
is not NULL
then output the ContactTitle
value concatenated with a comma, otherwise emit nothing. For more on the usefulness of the CASE
statement, see The Power of SQL CASE
Statements.
[!NOTE] Instead of using a
CASE
statement here, we could have alternatively usedISNULL(ContactTitle, '')
.ISNULL(checkExpression, replacementValue)
returns checkExpression if it is non-NULL, otherwise it returns replacementValue. While eitherISNULL
orCASE
will work in this instance, there are more intricate scenarios where the flexibility of theCASE
statement cannot be matched byISNULL
.
After adding this computed column your screen should look like the screen shot in Figure 1.
Figure 1: Add a Computed Column Named FullContactName
to the Suppliers
Table (Click to view full-size image)
After naming the computed column and entering its expression, save the changes to the table by clicking the Save icon in the toolbar, by hitting Ctrl+S, or by going to the File menu and choosing Save Suppliers
.
Saving the table should refresh the Server Explorer, including the just-added column in the Suppliers
table s column list. Furthermore, the expression entered into the (Formula) textbox will automatically adjust to an equivalent expression that strips unnecessary whitespace, surrounds column names with brackets ([]
), and includes parentheses to more explicitly show the order of operations:
[!code-sqlMain]
1: (((([ContactName]+' (')+case when [ContactTitle] IS NOT NULL
2: then [ContactTitle]+', ' else '' end)+[CompanyName])+')')
For more information on computed columns in Microsoft SQL Server, refer to the technical documentation. Also check out the How to: Specify Computed Columns for a step-by-step walkthrough of creating computed columns.
[!NOTE] By default, computed columns are not physically stored in the table but are instead recalculated each time they are referenced in a query. By checking the Is Persisted checkbox, however, you can instruct SQL Server to physically store the computed column in the table. Doing so allows an index to be created on the computed column, which can improve the performance of queries that use the computed column value in their
WHERE
clauses. See Creating Indexes on Computed Columns for more information.
Step 2: Viewing the Computed Column s Values
Before we start work on the Data Access Layer, let s take a minute to view the FullContactName
values. From the Server Explorer, right-click on the Suppliers
table name and choose New Query from the context-menu. This will bring up a Query window that prompts us to choose what tables to include in the query. Add the Suppliers
table and click Close. Next, check the CompanyName
, ContactName
, ContactTitle
, and FullContactName
columns from the Suppliers table. Finally, click the red exclamation point icon in the Toolbar to execute the query and view the results.
As Figure 2 shows, the results include FullContactName
, which lists the CompanyName
, ContactName
, and ContactTitle
columns using the format ContactName
(ContactTitle
, CompanyName
) .
Figure 2: The FullContactName
Uses the Format ContactName
(ContactTitle
, CompanyName
) (Click to view full-size image)
Step 3: Adding theSuppliersTableAdapter
to the Data Access Layer
In order to work with the supplier information in our application we need to first create a TableAdapter and DataTable in our DAL. Ideally, this would be accomplished using the same straightforward steps examined in earlier tutorials. However, working with computed columns introduces a few wrinkles that merit discussion.
If you are using a TableAdapter that uses ad-hoc SQL statements, you can simply include the computed column in the TableAdapter s main query via the TableAdapter Configuration wizard. This, however, will auto-generate INSERT
and UPDATE
statements that include the computed column. If you attempt to execute one of these methods, a SqlException
with the message The column ColumnName cannot be modified because it is either a computed column or is the result of a UNION operator will be thrown. While the INSERT
and UPDATE
statement can be manually adjusted through the TableAdapter s InsertCommand
and UpdateCommand
properties, these customizations will be lost whenever the TableAdapter Configuration wizard is re-run.
Due to the brittleness of TableAdapters that use ad-hoc SQL statements, it is recommended that we use stored procedures when working with computed columns. If you are using existing stored procedures, simply configure the TableAdapter as discussed in the Using Existing Stored Procedures for the Typed DataSet s TableAdapters tutorial. If you have the TableAdapter wizard create the stored procedures for you, however, it is important to initially omit any computed columns from the main query. If you include a computed column in the main query, the TableAdapter Configuration wizard will inform you, upon completion, that it cannot create the corresponding stored procedures. In short, we need to initially configure the TableAdapter using a computed column-free main query and then manually update the corresponding stored procedure and the TableAdapter s SelectCommand
to include the computed column. This approach is similar to the one used in the Updating the TableAdapter to UseJOIN
s tutorial.
For this tutorial, let s add a new TableAdapter and have it automatically create the stored procedures for us. Consequently, we will need to initially omit the FullContactName
computed column from the main query.
Start by opening the NorthwindWithSprocs
DataSet in the ~/App_Code/DAL
folder. Right-click in the Designer and, from the context-menu, choose to add a new TableAdapter. This will launch the TableAdapter Configuration wizard. Specify the database to query data from (NORTHWNDConnectionString
from Web.config
) and click Next. Since we have not yet created any stored procedures for querying or modifying the Suppliers
table, select the Create new stored procedures option so that the wizard will create them for us and click Next.
Figure 3: Choose the Create new stored procedures Option (Click to view full-size image)
The subsequent step prompts us for the main query. Enter the following query, which returns the SupplierID
, CompanyName
, ContactName
, and ContactTitle
columns for each supplier. Note that this query purposefully omits the computed column (FullContactName
); we will update the corresponding stored procedure to include this column in Step 4.
[!code-sqlMain]
1: SELECT SupplierID, CompanyName, ContactName, ContactTitle
2: FROM Suppliers
After entering the main query and clicking Next, the wizard allows us to name the four stored procedures it will generate. Name these stored procedures Suppliers_Select
, Suppliers_Insert
, Suppliers_Update
, and Suppliers_Delete
, as Figure 4 illustrates.
Figure 4: Customize the Names of the Auto-Generated Stored Procedures (Click to view full-size image)
The next wizard step allows us to name the TableAdapter s methods and specify the patterns used to access and update data. Leave all three checkboxes checked, but rename the GetData
method to GetSuppliers
. Click Finish to complete the wizard.
Figure 5: Rename the GetData
Method to GetSuppliers
(Click to view full-size image)
Upon clicking Finish, the wizard will create the four stored procedures and add the TableAdapter and corresponding DataTable to the Typed DataSet.
Step 4: Including the Computed Column in the TableAdapter s Main Query
We now need to update the TableAdapter and DataTable created in Step 3 to include the FullContactName
computed column. This involves two steps:
- Updating the
Suppliers_Select
stored procedure to return theFullContactName
computed column, and - Updating the DataTable to include a corresponding
FullContactName
column.
Start by navigating to the Server Explorer and drilling down into the Stored Procedures folder. Open the Suppliers_Select
stored procedure and update the SELECT
query to include the FullContactName
computed column:
[!code-sqlMain]
1: SELECT SupplierID, CompanyName, ContactName, ContactTitle, FullContactName
2: FROM Suppliers
Save the changes to the stored procedure by clicking the Save icon in the Toolbar, by hitting Ctrl+S, or by choosing the Save Suppliers_Select
option from the File menu.
Next, return to the DataSet Designer, right-click on the SuppliersTableAdapter
, and choose Configure from the context-menu. Note that the Suppliers_Select
column now includes the FullContactName
column in its Data Columns collection.
Figure 6: Run the TableAdapter s Configuration Wizard to Update the DataTable s Columns (Click to view full-size image)
Click Finish to complete the wizard. This will automatically add a corresponding column to the SuppliersDataTable
. The TableAdapter wizard is smart enough to detect that the FullContactName
column is a computed column and therefore read-only. Consequently, it sets the column s ReadOnly
property to true
. To verify this, select the column from the SuppliersDataTable
and then go to the Properties window (see Figure 7). Note that the FullContactName
column s DataType
and MaxLength
properties are also set accordingly.
Figure 7: The FullContactName
Column is Marked as Read-Only (Click to view full-size image)
Step 5: Adding aGetSupplierBySupplierID
Method to the TableAdapter
For this tutorial we will create an ASP.NET page that displays the suppliers in an updateable grid. In past tutorials we have updated a single record from the Business Logic Layer by retrieving that particular record from the DAL as a strongly-typed DataTable, updating its properties, and then sending the updated DataTable back to the DAL to propagate the changes to the database. To accomplish this first step - retrieving the record being updated from the DAL - we need to first add a GetSupplierBySupplierID(supplierID)
method to the DAL.
Right-click on the SuppliersTableAdapter
in the DataSet Design and choose the Add Query option from the context-menu. As we did in Step 3, let the wizard generate a new stored procedure for us by selecting the Create new stored procedure option (refer back to Figure 3 for a screenshot of this wizard step). Since this method will return a record with multiple columns, indicate that we want to use a SQL query that is a SELECT which returns rows and click Next.
Figure 8: Choose the SELECT which returns rows Option (Click to view full-size image)
The subsequent step prompts us for the query to use for this method. Enter the following, which returns the same data fields as the main query but for a particular supplier.
[!code-sqlMain]
1: SELECT SupplierID, CompanyName, ContactName, ContactTitle, FullContactName
2: FROM Suppliers
3: WHERE SupplierID = @SupplierID
The next screen asks us to name the stored procedure that will be auto-generated. Name this stored procedure Suppliers_SelectBySupplierID
and click Next.
Figure 9: Name the Stored Procedure Suppliers_SelectBySupplierID
(Click to view full-size image)
Lastly, the wizard prompts us for the data access patterns and method names to use for the TableAdapter. Leave both checkboxes checked, but rename the FillBy
and GetDataBy
methods to FillBySupplierID
and GetSupplierBySupplierID
, respectively.
Figure 10: Name the TableAdapter Methods FillBySupplierID
and GetSupplierBySupplierID
(Click to view full-size image)
Click Finish to complete the wizard.
Step 6: Creating the Business Logic Layer
Before we create an ASP.NET page that uses the computed column created in Step 1, we first need to add the corresponding methods in the BLL. Our ASP.NET page, which we will create in Step 7, will allow users to view and edit suppliers. Therefore, we need our BLL to provide, at minimum, a method to get all of the suppliers and another to update a particular supplier.
Create a new class file named SuppliersBLLWithSprocs
in the ~/App_Code/BLL
folder and add the following code:
[!code-vbMain]
1: Imports NorthwindWithSprocsTableAdapters
2: <System.ComponentModel.DataObject()> _
3: Public Class SuppliersBLLWithSprocs
4: Private _suppliersAdapter As SuppliersTableAdapter = Nothing
5: Protected ReadOnly Property Adapter() As SuppliersTableAdapter
6: Get
7: If _suppliersAdapter Is Nothing Then
8: _suppliersAdapter = New SuppliersTableAdapter()
9: End If
10: Return _suppliersAdapter
11: End Get
12: End Property
13: <System.ComponentModel.DataObjectMethodAttribute _
14: (System.ComponentModel.DataObjectMethodType.Select, True)> _
15: Public Function GetSuppliers() As NorthwindWithSprocs.SuppliersDataTable
16: Return Adapter.GetSuppliers()
17: End Function
18: <System.ComponentModel.DataObjectMethodAttribute _
19: (System.ComponentModel.DataObjectMethodType.Update, True)> _
20: Public Function UpdateSupplier(companyName As String, contactName As String, _
21: contactTitle As String, supplierID As Integer) As Boolean
22: Dim suppliers As NorthwindWithSprocs.SuppliersDataTable = _
23: Adapter.GetSupplierBySupplierID(supplierID)
24: If suppliers.Count = 0 Then
25: ' no matching record found, return false
26: Return False
27: End If
28: Dim supplier As NorthwindWithSprocs.SuppliersRow = suppliers(0)
29: supplier.CompanyName = companyName
30: If contactName Is Nothing Then
31: supplier.SetContactNameNull()
32: Else
33: supplier.ContactName = contactName
34: End If
35: If contactTitle Is Nothing Then
36: supplier.SetContactTitleNull()
37: Else
38: supplier.ContactTitle = contactTitle
39: End If
40: ' Update the product record
41: Dim rowsAffected As Integer = Adapter.Update(supplier)
42: ' Return true if precisely one row was updated, otherwise false
43: Return rowsAffected = 1
44: End Function
45: End Class
Like the other BLL classes, SuppliersBLLWithSprocs
has a Protected
Adapter
property that returns an instance of the SuppliersTableAdapter
class along with two Public
methods: GetSuppliers
and UpdateSupplier
. The GetSuppliers
method calls and returns the SuppliersDataTable
returned by the corresponding GetSupplier
method in the Data Access Layer. The UpdateSupplier
method retrieves information about the particular supplier being updated via a call to the DAL s GetSupplierBySupplierID(supplierID)
method. It then updates the CategoryName
, ContactName
, and ContactTitle
properties and commits these changes to the database by calling the Data Access Layer s Update
method, passing in the modified SuppliersRow
object.
[!NOTE] Except for
SupplierID
andCompanyName
, all columns in the Suppliers table allowNULL
values. Therefore, if the passed-incontactName
orcontactTitle
parameters areNothing
we need to set the correspondingContactName
andContactTitle
properties to aNULL
database value using theSetContactNameNull
andSetContactTitleNull
methods, respectively.
Step 7: Working with the Computed Column from the Presentation Layer
With the computed column added to the Suppliers
table and the DAL and BLL updated accordingly, we are ready to build an ASP.NET page that works with the FullContactName
computed column. Start by opening the ComputedColumns.aspx
page in the AdvancedDAL
folder and drag a GridView from the Toolbox onto the Designer. Set the GridView s ID
property to Suppliers
and, from its smart tag, bind it to a new ObjectDataSource named SuppliersDataSource
. Configure the ObjectDataSource to use the SuppliersBLLWithSprocs
class we added back in Step 6 and click Next.
Figure 11: Configure the ObjectDataSource to Use the SuppliersBLLWithSprocs
Class (Click to view full-size image)
There are only two methods defined in the SuppliersBLLWithSprocs
class: GetSuppliers
and UpdateSupplier
. Ensure that these two methods are specified in the SELECT and UPDATE tabs, respectively, and click Finish to complete the configuration of the ObjectDataSource.
Upon completion of the Data Source Configuration wizard, Visual Studio will add a BoundField for each of the data fields returned. Remove the SupplierID
BoundField and change the HeaderText
properties of the CompanyName
, ContactName
, ContactTitle
, and FullContactName
BoundFields to Company, Contact Name, Title, and Full Contact Name, respectively. From the smart tag, check the Enable Editing checkbox to turn on the GridView s built-in editing capabilities.
In addition to adding BoundFields to the GridView, completion of the Data Source Wizard also causes Visual Studio to set the ObjectDataSource s OldValuesParameterFormatString
property to original_{0}. Revert this setting back to its default value, {0} .
After making these edits to the GridView and ObjectDataSource, their declarative markup should look similar to the following:
[!code-aspxMain]
1: <asp:GridView ID="Suppliers" runat="server" AutoGenerateColumns="False"
2: DataKeyNames="SupplierID" DataSourceID="SuppliersDataSource">
3: <Columns>
4: <asp:CommandField ShowEditButton="True" />
5: <asp:BoundField DataField="CompanyName"
6: HeaderText="Company"
7: SortExpression="CompanyName" />
8: <asp:BoundField DataField="ContactName"
9: HeaderText="Contact Name"
10: SortExpression="ContactName" />
11: <asp:BoundField DataField="ContactTitle"
12: HeaderText="Title"
13: SortExpression="ContactTitle" />
14: <asp:BoundField DataField="FullContactName"
15: HeaderText="Full Contact Name"
16: SortExpression="FullContactName"
17: ReadOnly="True" />
18: </Columns>
19: </asp:GridView>
20: <asp:ObjectDataSource ID="SuppliersDataSource" runat="server"
21: SelectMethod="GetSuppliers" TypeName="SuppliersBLLWithSprocs"
22: UpdateMethod="UpdateSupplier">
23: <UpdateParameters>
24: <asp:Parameter Name="companyName" Type="String" />
25: <asp:Parameter Name="contactName" Type="String" />
26: <asp:Parameter Name="contactTitle" Type="String" />
27: <asp:Parameter Name="supplierID" Type="Int32" />
28: </UpdateParameters>
29: </asp:ObjectDataSource>
Next, visit this page through a browser. As Figure 12 shows, each supplier is listed in a grid that includes the FullContactName
column, whose value is simply the concatenation of the other three columns formatted as ContactName
(ContactTitle
, CompanyName
) .
Figure 12: Each Supplier is Listed in the Grid (Click to view full-size image)
Clicking the Edit button for a particular supplier causes a postback and has that row rendered in its editing interface (see Figure 13). The first three columns render in their default editing interface - a TextBox control whose Text
property is set to the value of the data field. The FullContactName
column, however, remains as text. When the BoundFields were added to the GridView at the completion of the Data Source Configuration wizard, the FullContactName
BoundField s ReadOnly
property was set to True
because the corresponding FullContactName
column in the SuppliersDataTable
has its ReadOnly
property set to True
. As noted in Step 4, the FullContactName
s ReadOnly
property was set to True
because the TableAdapter detected that the column was a computed column.
Figure 13: The FullContactName
Column is Not Editable (Click to view full-size image)
Go ahead and update the value of one or more of the editable columns and click Update. Note how the FullContactName
s value is automatically updated to reflect the change.
[!NOTE] The GridView currently uses BoundFields for the editable fields, resulting in the default editing interface. Since the
CompanyName
field is required, it should be converted into a TemplateField that includes a RequiredFieldValidator. I leave this as an exercise for the interested reader. Consult the Adding Validation Controls to the Editing and Inserting Interfaces tutorial for step-by-step instructions on converting a BoundField to a TemplateField and adding validation controls.
Summary
When defining the schema for a table, Microsoft SQL Server allows the inclusion of computed columns. These are columns whose values are calculated from an expression that usually references the values from other columns in the same record. Since the values for computed columns are based on an expression, they are read-only and cannot be assigned a value in an INSERT
or UPDATE
statement. This introduces challenges when using a computed column in the main query of a TableAdapter that tries to automatically generate corresponding INSERT
, UPDATE
, and DELETE
statements.
In this tutorial we discussed techniques for circumventing the challenges posed by computed columns. In particular, we used stored procedures in our TableAdapter to overcome the brittleness inherent in TableAdapters that use ad-hoc SQL statements. When having the TableAdapter wizard create new stored procedures, it is important that we have the main query initially omit any computed columns because their presence prevents the data modification stored procedures from being generated. After the TableAdapter has been initially configured, its SelectCommand
stored procedure can be retooled to include any computed columns.
Happy Programming!
About the Author
Scott Mitchell, author of seven ASP/ASP.NET books and founder of 4GuysFromRolla.com, has been working with Microsoft Web technologies since 1998. Scott works as an independent consultant, trainer, and writer. His latest book is Sams Teach Yourself ASP.NET 2.0 in 24 Hours. He can be reached at mitchell@4GuysFromRolla.com. or via his blog, which can be found at http://ScottOnWriting.NET.
Special Thanks To
This tutorial series was reviewed by many helpful reviewers. Lead reviewers for this tutorial were Hilton Geisenow and Teresa Murphy. Interested in reviewing my upcoming MSDN articles? If so, drop me a line at mitchell@4GuysFromRolla.com.
|