Three-tier .NET Application Utilizing Three ORM Technologies






4.95/5 (114投票s)
LINQ to SQL, Entity Framework, and NHibernate used in a parallel fashion in a three-tier WinForms application.
1 Introduction
This article presents a three-tier .NET WinForms accounting application, called "LEK", that can be configured at run time to access its data (in a SQL Server database) using either LINQ to SQL, Entity Framework, or NHibernate. The download for the article contains the Visual Studio 2008 solution, including everything you need to build the application. The source code included in the download, and the article itself, are organized so that it is as easy as possible to compare how the same functionality can be achieved with each of the three ORMs. If you are already familiar with one of these three ORMs, and need to learn one of the other three, then I think you will be able to use this article to leverage your knowledge of one to learn the other.
2 Contents at a Glance
- 引言
- Contents at a Glance
- A Few Screenshots
- Detailed Contents
- 背景
- My Ulterior Motive (...well, one of them)
- Data Model
- Business Rules
- Visualization of the Data
- 架构
- ORM Classes and Their Mapping to Database Entities
- How the ORM Classes Fit in the Architecture
- Using the ORM Frameworks for Simple Operations
- Using the ORM Frameworks for a Complex Query
- Using the ORM Frameworks for a Complex Data Write Operation
- A Few Other Helpful Views and the Functions that Feed Them
- Downloading, Setting up the Database, Building, and Running
- Using the Code
- 历史
3 A Few Screenshots
Contents at a Glance Detailed Contents
Here is the form that comes up when you launch LEK
Here is the form for creating and querying for accounting transactions
4 Detailed Contents
- 引言
- Contents at a Glance
- A Few Screenshots
- Detailed Contents
- 背景
- My Ulterior Motive (...well, one of them)
- Data Model
- 7.1 Net Worth Account (NWAccnt)
- 7.2 Participant
- 7.3 Ownership
- 7.4 Delta Allocation Account (DltAllocAccnt)
- 7.5 Post
- 7.6 Transaction
- 7.7 Item
- 7.8 Delta Allocation (DltAlloc)
- 7.9 Net Worth Account Delta (NWAccntDlt)
- 7.10 Wash Delta (WashDlt)
- Business Rules
- Visualization of the Data
- 架构
- 10.1 Simplest Possible
- 10.2 Minimal Sharing
- 10.3 Single App Option
- 10.4 Projects or Namespaces
- 10.5 WCF Configuration
- 10.6 UI Interface
- 10.7 Generic Util Separate from Domain Util
- 10.8 Service Interface
- 10.9 Avoiding Mocking for Unit Tests
- 10.10 Isolating the ORM Classes From the Rest of the App
- 10.11 Isolating the ORM Dependant Code for Each ORM
- 10.12 Dependencies: The Big Picture
- ORM Classes and Their Mapping to Database Entities
- 11.1 Generating a Default Set of ORM Classes in LINQ to SQL and Entity Framework
- 11.1.1 LINQ to SQL
- 11.1.2 Entity Framework
- 11.2 ERDs
- 11.2.1 SQL Server Database
- 11.2.2 LINQ to SQL ORM Classes
- 11.2.3 Entity Framework ORM Classes
- 11.3 Examining an ORM Class for LINQ to SQL, Entity Framework, and NHibernate
- 11.3.1 LINQ to SQL
- 11.3.2 Entity Framework
- 11.3.3 NHibernate
- 11.3.4 Comparing the ORM Classes and Mapping for each ORM
- 11.4 ORM Classes and Mapping Info Specifications for NHibernate
- How the ORM Classes Fit in the Architecture
- 12.1 The Service Classes
- 12.2 The IService Interface
- Using the ORM Frameworks for Simple Operations
- 13.1 Row Version (GlobalType Namespace)
- 13.2 Participant (GlobalType Namespace)
- 13.3 The Service Class
- 13.4 Simple Data Read Operations
- 13.4.1 Data Access Code
- 13.4.2 Generated SQL
- 13.5 Simple Data Write Operations
- 13.5.1 Data Access Code
- 13.5.1.1 Supporting Code Not Specific to Any ORM
- 13.5.1.2 LINQ to SQL Code for a Simple Data Write Operation
- 13.5.1.3 Entity Framework Code for a Simple Data Write Operation
- 13.5.1.4 NHibernate Code for a Simple Data Write Operation
- 13.5.2 Generated SQL for a Simple Data Write Operation
- Using the ORM Frameworks for a Complex Query
- 14.1 Segregation of Criteria Setup Code
- 14.1.1 Segregatable Criteria
- 14.1.2 Traversing an Association
- 14.1.3 A Function to Set Criteria
- 14.1.4 Calling the Criteria Setting Function
- 14.1.5 Calling the Criteria Setting Function When NOT Traversing an Association
- 14.2 Transaction Predicates
- 14.3 Supporting Code for Segregated Criteria Setup
- 14.4 Criteria Setting Function
- 14.5 Five Queries for Building a Collection of Transactions
- 14.6 SQL Generated for One of the Queries for Building a Collection of Transactions
- Using the ORM Frameworks for a Complex Data Write Operation
- 15.1 The Strategy
- 15.2 About the Presentation of the Code in this Section
- 15.3 Function Dependency Tree for SaveTransaction
- 15.4 Stored Procedures
- 15.4.1 GetTransactionTimestamp
- 15.4.2 DeleteTransactionChildRecords
- 15.5 Supporting Functions Unique to Each ORM
- 15.5.1 LINQ to SQL
- 15.5.2 Entity Framework
- 15.5.2.1 CreateEntityKey
- 15.5.2.2 GetFK
- 15.5.2.3 EnsureFK
- 15.5.2.4 SetAllPropsAsModified
- 15.5.3 NHibernate
- 15.5.3.1 EnsureFKWithInteger
- 15.5.3.2 EnsureFKWithEntity
- 15.6 SaveTransaction and the Supporting Functions With an Implementation in Each ORM
- 15.6.1 CreateORMRowVersion
- 15.6.2 SPs: GetTransactionTimestamp and DeleteTransactionChildRecords
- 15.6.3 CreateTransactionReadyToAttach
- 15.6.4 TransactionRowVersionMatches
- 15.6.5 SetDALObjectFromDomainObject
- 15.6.6 LLSaveTransaction
- 15.6.7 Save Transaction
- A Few Other Helpful Views and the Functions that Feed Them
- 16.1 Net Worth Account Deltas for an Account
- 16.2 Items for an Account
- 16.3 Summary
- 16.4 Items
- Downloading, Setting up the Database, Building, and Running
- Using the Code
- 历史
5 Background
Contents at a Glance Detailed Contents
If you have used an ORM (object-relational mapping) framework before, you know that they can eliminate a lot of code that is relatively tedious and error prone. It is relatively easy to play around with Query Analyzer or SQL Server Management Studio to come up with some SQL to move data into or out of a database. But whether you turn this SQL into Stored Procedures or paste it directly into your application source code, you end up with a lot of logic expressed as plain old strings that no compiler is going to complain about if you get wrong. Well, I shouldn't say get wrong. It will be right when you add it to your application. But the schema will change, and when it does, it won't be so easy to find all the places where the SQL is no longer correct.
So, ORMs are great, but which one should you use? To answer that, you need to have a good idea about what your application needs to do and how long it will be around. If you need something that isn't too complicated, you need it fast, and you know the database will always be SQL Server, then LINQ to SQL is probably a good choice. If you need to create a complicated application and expect it to last a long time, then NHibernate would be a good choice. If you really like and believe in Microsoft, and if you can use .NET 4.0 (available yet?), then Entity Framework would probably work well.
I am not going attempt to provide much more guidance than that on selecting an ORM. What I hope to do instead, is to provide an example application that you can use to leverage your knowledge of LINQ to SQL, Entity Framework, or NHibernate to get you started with another one of those three. Perhaps you have used NHibernate on a large application and now find yourself on a team that needs to create something fast with LINQ to SQL. Or maybe you started out with LINQ to SQL or Entity Framework, found that it just wasn't up to the task, and are now switching to NHibernate. Either way, I think you can use this application to see how some basic functionality you know how to use with one of the ORMs can be achieved with another.
If you are just getting started with one of these ORMs and don't know any of the others any better, I think this document and application can still be helpful. The application goes beyond the typical entry level example application for an ORM in that it is architected like a large application with real large application issues, such as concurrency, transactions, composable "where" clauses, and detached entities. If you have already made it through some simple example applications, but are struggling with how to implement functionality in a real-world application, then this application may be helpful.
Besides ORM issues, this application also addresses application architecture in considerable detail. This document will discuss at length breaking up the functionality of an application into namespaces with clearly defined dependencies - which the application does. The model-view-presenter design is used for the user interface and the data transfer objects are used to move data between tiers. Communication between the client and server tiers is handled via WCF (Windows Communication Foundation).
Of course, this application doesn't do everything well, and there are some things for which you should not look to this application for guidance. This is a WinForms application with an uninspired UI. You won't find anything on WPF (Windows Presentation Foundation), AJAX, ASP, or Silverlight in this application. The data access layer (where the ORMs are used) is covered pretty well with unit tests, but not much else is. There is a lot of ORM-related logic in the data access layer and testing it efficiently would require substituting for the real database an in-memory one that can still be operated on by the ORMs. This has not been done. (Can it?). Also, this example application does not properly address error handling, logging, security, deployment, instrumentation, or internationalization.
And certainly this should not be considered a complete example for any of the ORMs. For each, it only scratches the surface.
6 My Ulterior Motive (...well, one of them)
Contents at a Glance Detailed Contents
The ideal type of application for exploring ORM, architecture, and WCF would probably be one that you can easily imagine being used in a corporate environment by hundreds of users, or a public ASP.NET application used by thousands. On this count, I didn't do so well. Instead, I wrote something that I personally would find useful: a rich client accounting application. Imagining this in use in a corporate environment with hundreds of users will be a stretch. I do believe, however, that the architecture, database design, data access techniques, and concurrency measures taken are all proper for a large corporate environment with hundreds of users - even if the functionality itself could just as well have been provided with a much simpler design more appropriate for a single user at home.
7 Data Model
Contents at a Glance Detailed Contents
LEK is a three-tier database application that helps a family manage its money. LEK is architected as if it were a large application with hundreds of concurrent users, even though it is typically used by a single person sitting at his desk in his bedroom (that would be me). LEK has a very simple Windows Forms UI and a server application that can communicate with the database using either LINQ to SQL, Entity Framework, or NHibernate. The client and the server communicate via WCF.
LEK is based on the following family accounting model
- Each individual in the family owns a certain percentage of each of the assets owned by the family.
- Each individual in the family is responsible for a certain percentage of each of the liabilities the family is responsible for.
- Each expense incurred by the family can be allocated among the family members.
- Each income received by the family can be allocated among the family members.
Here are some examples: "Father" and "Mother" (I will use these terms as if they were names) both work, but income earned by each belongs to both equally. (Mother gets 50% of what Father makes and the Father gets 50% of what the Mother makes.) Father and Mother each have their own bank accounts, of which they "own" 100%. If they go on a vacation, they take a "Joint Wallet" in which they each have a 50% stake. When Mother buys groceries the Father owes for half. When Father buys diving gear for himself he is responsible for all of it. "Son" and "Daughter" (using as if they were names) each get an allowance. If Mother buys Son a Star Wars light saber and it isn't Christmas, Son owes Mother 100% for it.
I am a "database first" kind of guy. I know this isn't conventional wisdom. A designer is supposed to figure out the use cases and then create an entity model that can implement the functionality described by the use cases. To me, that is just too shaky a ground to start on. I know vaguely what kinds of operations need to be performed, but I know exactly what data I am trying to store and manage. Perhaps what I actually know is a logical model of the data. When it comes time to implement the model in a real database, things could change. Things could also change during optimization, or, non-ideally, once the database gets filled up with real production data and is being hit by queries from real users. But at least when starting off, my database typically looks just like my conceptual model of the data.
Here is the conceptual data model for LEK
In the above figure, each box represents an entity. The lines represent many-to-one associations between the entities. For example, each Item
is associated with one Transaction
and one DltAllocAccnt
. A Transaction
can have zero, one, or more Item
s. A DltAllocAccnt
can have zero, one or more Item
s. A dashed line indicates that the association doesn't have to exist.
7.1 Net Worth Account (NWAccnt)
Contents at a Glance Detailed Contents
A NWAccnt
(Net Worth Account) is something that figures into the net worth of the family. Examples: Father's checking account, Mother's credit card (negative), the house, the mortgage (negative), Son's piggy bank.
7.2 Participant
Contents at a Glance Detailed Contents
A Participant
is a family member.
7.3 Ownership
Contents at a Glance Detailed Contents
If you made a table on paper with NWAccnt
s down the left and Participant
s across the top and each cell indicating the percentage ownership that each Participant
had in each NWAccnt
, then each cell would be an Ownership
. In database parlance, each Ownership
record would have a foreign key to a NWAccnt
record, a foreign key to a Participant
record, and a numerical value indicating percent ownership. No two records would have the same pair of foreign keys.
7.4 Delta Allocation Account (DltAllocAccnt)
Contents at a Glance Detailed Contents
A DltAllocAccnt
(Delta Allocation Account) is an expense or income account. Examples: groceries, gasoline, entertainment, salary, interest.
7.5 Post
Contents at a Glance Detailed Contents
A Post
is a point in time in which all money related activity is considered to be properly accounted for. I typically collect receipts and record everything each Saturday, so there usually ends up being one post each week.
7.6 Transaction
Contents at a Glance Detailed Contents
A Transaction
is an event that changes who-owes-who, the family's net worth, or where the money is. A Transaction
is typically brought about by a Participant
, and it will eventually get associated with a Post
. Examples: Mother bought groceries, Father got pay check, Daughter allocated allowance, money moved from a savings account to a checking account.
7.7 Item
Contents at a Glance Detailed Contents
An Item
is a portion of a Transaction
that affects the net worth of one or more Participant
s. Each Item
is associated with a Transaction
and a DltAllocAccnt
. Two Item
s can be associated with the same Transaction
and the same DltAllocAccnt
(gallon of milk and cup of cream both purchased at the store and both are considered a "dairy" expense). The division of a Transaction
into Item
s is arbitrary. For example, if the Transaction
is "Bought groceries at HEB" then there could be the four Item
s "gallon milk", "4 ounces cheese", "1 lb pork", "1 lb beef", or there could be the two Item
s "dairy", "meat", or there could be just a single Item
"groceries". If the DltAllocAccnt
s are fine-grained, this will sometimes require fine-grained Item
s. For example, if the accounts in the system include "milk", "cheese", "pork", and "beef" then a Transaction
involving these types of things will need to be broken down into at least enough Item
s to allow each Item
to be associated with a DltAllocAccnt
. If there is just a single "groceries" DltAllocAccnt
, then breaking down the Transaction
to this level of detail would be optional. Some other examples of Item
s include "received pay check" (would be associated with a "salary" or "income" DltAllocAccnt
) and "granted allowance" (could be associated with an "allowance" DltAllocAccnt
).
7.8 Delta Allocation (DltAlloc)
Contents at a Glance Detailed Contents
A DltAlloc
(Delta Allocation) is an allocation of the expense or income for an Item
to a Participant
. Each DltAlloc
is associated with an Item
and a Participant
. No two DltAlloc
s can be associated with the same Item
and Participant
.
7.9 Net Worth Account Delta (NWAccntDlt)
Contents at a Glance Detailed Contents
A NWAccntDlt
(Net Worth Account Delta) is a change in the value of a NWAccnt
. A NWAccntDlt
is associated with a Transaction
and a NWAccnt
. For example, if you withdraw cash from your checking account and put it in your wallet then there would be two NWAccntDlt
s: a negative one associated with a "checking" NWAccnt
and a positive one associated with a "wallet" NWAccnt
. Both of these would be associated with the same Transaction
.
7.10 Wash Delta (WashDlt)
Contents at a Glance Detailed Contents
If Father goes to the store and buys something for Son and Mother then they each owe Father for it. But for the family as a whole the amount owed is "a wash" - Father is owed $5, Mother owes $3, and Son owes $2. As you can imagine, this could get complicated very quickly as the number of Participant
s grows. Rather than keeping track of each possible combination of Participant
s owing each other, a virtual third party is introduced called the "the wash". In the above example, Mother owes "the wash" $3, Son owes "the wash" $2, and Father is owed by "the wash" $5. Each of these would be a WashDlt
. Each WashDlt
is associated with a Transaction
and a Participant
. No two WashDlt
s have the same Participant
and the same Transaction
. All the WashDlt
s associated with the same Transaction
sum to 0.
8 Business Rules
Contents at a Glance Detailed Contents
The most obvious rules for the data center around the Transaction
. The sum of the NWAccntDlt
s in a Transaction
must equal the sum of the DltAlloc
s in the Transaction
. The sum of the WashDlt
s in a Transaction
must be 0. Finally, at the time that a Transaction
is created, the change of the net worth of a Participant
must equal the sum of the DltAlloc
s in the Transaction
for that Participant
. This last rule is what ties in the WashDlt
s. In a Transaction
with one or more NWAccntDlt
s each Participant
is attributed a percentage of the amount in each NWAccntDlt
. For example, let's say Mother goes to the store and buys $20 in groceries which includes a $4 can of sardines for Father. Everyone in the family except Father HATES the smell of the sardines so that family has the policy that even though grocery expenses generally are split between Mother and Father, Father pays for sardines himself. Mother uses the "joint wallet" to pay for groceries. There will be one NWAccntDlt
of -$20, but since Father and Mother each own 50% of "joint wallet", they have each individually seen their net worth as tracked by the NWAccnt
s drop by $10. Since Father is to bear the expense of the sardines himself, however, there are also two WashDlt
s: a -$2 one for the Father and a +$2 one for Mother. For both Father and Mother the change to their net worth is the sum of their portion of the NWAccntDlt
changes (-$10 and -$10 for Father and Mother respectively) plus their WashDlt
s (-$2 and +$2 for Father and Mother respectively). The net result is that the Father's net worth went down by $12, and the Mother's went down by $8.
9 Visualization of the Data
Contents at a Glance Detailed Contents
Here are some screenshots from LEK of a Transaction
slightly more complicated than the one just described. Note that on the forms that receive user input, amounts are shown in cents rather than in dollars. This is merely to save the user from having to enter the decimal point.
Building on the example described in Business Rules (above), let's say Son also goes to the store with Mother and he brings his own money. He somehow pulls mom into the toy aisle, grabs a "light saber", and says "Mom! Mom! Can I get this light saber? I have the money for it!". Mother agrees. At check out, Mother gives the attendant $20 from the joint wallet and $5 from Son.
The NWAccntDlt
s for this Transaction
will appear like this in the UI
The Item
s for this Transaction
will appear like this
The "1" and "0" in the above represent which Participant
bears the expense of the Item
. "Milk" for example, has a "1" under both "Father" and "Mother", which indicates that Father and Mother split the expense for this Item
. If the cells contained "2" and "1" for the "Father" and "Mother" columns, that would indicate that Father is responsible for 2/3 of the expense and Mother is responsible for 1/3 of the expense.
The summary of the whole Transaction
will appear like this
10 Architecture
Contents at a Glance Detailed Contents
LEK can be deployed as either a two-tier application or a three-tier application. In either case there is a database tier consisting of a SQL Server database. In the three-tier configuration, the other two tiers are a client .NET application and a server .NET application communicating via WCF. In the real world, the server application would probably be a Windows Service or would be hosted in IIS. In this example, however, the server application is just an executable. The two-tier configuration consists of the database and a single .NET application. For both configurations, the executables are only small shell applications that rely on numerous DLL assemblies for nearly all application logic. In the three-tier configuration, some of the DLLs are used by the server only, some by the client only, and some are used by both. The application for the two-tier configuration uses almost all of the DLLs.
Dividing the application logic into numerous DLLs serves two purposes. First, it makes it easier to deploy the application as separate client and server applications that are only as large as necessary. Second, it makes it easy to enforce a structured architecture with well-defined dependencies between parts of the application. There is a downside, however: it takes much longer to build an application consisting of many small DLLs than it does to build an application consisting of a few large DLLs. A better solution is to only create as many DLLs as are needed for efficient deployment, and to enforce a structured architecture with a namespace dependency tool like NDepend (http://www.ndepend.com) or Lattix (http://www.lattix.com). Alas - I haven't done that yet. So for now, we have a bunch of DLLs, each generated from code in a separate namespace.
The LEK Visual Studio 2005 solution consists of 3 EXE projects and 19 DLL projects. The code that makes up each project is in a distinct namespace with the same name as the name of the project. Rather than presenting the whole thing at once, I will build up the actual architecture from the simplest possible architecture, explaining the reason behind each step's elaboration.
10.1 Simplest Possible
Contents at a Glance Detailed Contents
The simplest possible architecture for the client and server components of a three-tier rich client WCF application would be to have a single executable for the client and a single executable for the server, neither of which would rely on any DLLs
10.2 Minimal Sharing
Contents at a Glance Detailed Contents
The client and the server will need to share message formats at the very least, so rather than duplicating that specification in each project, this is put into a shared DLL called "Util"
10.3 Single App Option
Contents at a Glance Detailed Contents
Testing and debugging are usually easier when you only have to work with a single application. This will allow you to step though code while debugging from the UI all the way to the data access code. Pulling out all of the domain-oriented code into separate projects ("Service" and "Presenter") makes this code available to a single executable project ("WinApp") that is easier to work with during development. This will also facilitate changing the host for the service to a custom Windows Service or to IIS.
10.4 Projects or Namespaces
Contents at a Glance Detailed Contents
As mentioned earlier, for a large project it is probably best to keep the number of DLLs down and to enforce dependencies between different parts of the code by utilizing numerous namespaces and an application like NDepend (http://www.ndepend.com) or Lattix (http://www.lattix.com) to enforce dependencies between the namespaces. I have not done this with LEK, but each project does have its own namespace with the same name as the project. From this point forward, I will refer to namespaces rather than projects when discussing how the code for the application is divided up and the dependencies between the divisions.
10.5 WCF Configuration
Contents at a Glance Detailed Contents
A WCF application in production is usually easiest to configure if IP address, ports, and other WCF setup information are stored in configuration files. For development, however, it is often easier to compile this into the application. Since the client and the server must agree on these settings, they are put into a shared namespace called ClientServer
, which is only accessible from the WinFormServer
and WinFormClient
namespaces
10.6 UI Interface
Contents at a Glance Detailed Contents
UI code is hard to test and subject to the whims of whoever is setting the aesthetics for your application. As much as possible, the elements that control the appearance and directly interact with the user should be separated from the rest of the code. This will make it easier to write unit and integration tests for the rest of the code. In LEK, every form displayed to the user has two classes: one in the Presenter
namespace called a "presenter" and one in the WinUI
namespace called a "view". The presenter sends commands to the view to show information. The view "paints" the information on to the screen. When the user interacts with one of the painted controls, the view sends a notification to the presenter that the user wants to do something. The presenter takes the appropriate action and then sends a command to the view to update it if necessary.
The presenter only knows that the view it communicates with implements a specific interface. The view only knows that the presenter it interacts with implements a specific interface. These interfaces are defined in the UIInterface
namespace. This allows the Presenter
and WinUI
namespaces to be decoupled. They depend on the UIInterface
namespace but not on each other. The presenter has access to a factory that it asks to create its view. The presenter passes its view an interface reference to itself for the view to pass events back through.
The view factory is defined in the WinUI
namespace, but implements an interface of its own (for use by the presenters) that is defined in the UIInterface
namespace. The view factory itself is created from within the WinFormClient
or WinApp
namespaces, which then initializes the presenters by passing them an interface reference to the view factory. In this sense the shell executables provide an "inversion of control" functionality: they supply factories to subordinate code that is ignorant of the concrete classes that implement the functionality that the subordinate code needs.
I have not created unit tests for the presenters, but this architecture would make it easy to do so. The presenters would be supplied with a view factory that would create automated views that implement the required interfaces. The automated views would expose an additional interface so that they could be programmatically directed by the test harness to simulate a user interaction.
10.7 Generic Util Separate from Domain Util
Contents at a Glance Detailed Contents
If you have worked on more than one project, then you probably have a library of generic routines and types that you carry from project to project. In LEK, the Util
namespace is reserved for this sort of non-domain specific code. At this point in the elaboration of the architecture, all other shared code is reallocated to the GlobalType
, UIType
, and DomainUtil
namespaces. The GlobalType
namespace is for simple data types (little or no logic) that must be available to nearly all namespaces - client and server. The UIType
namespace is for the simple data types that must pass between the presenters and the views (in the Presenter
and WinUI
namespaces), but that aren't needed by the Service
namespace. The DomainUtil
namespace is for all other types and logic that need to be accessed by the Service
namespace and the Presenter
namespace (need to be accessed by both the server and the client tiers).
10.8 Service Interface
Contents at a Glance Detailed Contents
The most significant divisions between different parts of a three-tier WCF WinForms application are the division between the client and server applications and the division between the server application and the database. Data flowing across these divisions (between the tiers) moves across process boundaries, and maybe even machine boundaries. Consequently, the communication is orders of magnitude slower, and since it is across a network it may affect other users, even users in completely different applications.
The next step in the elaboration of the architecture concerns the division between the client and server applications. Presenters (in the Presenter
namespace) make calls via WCF to one or more objects exposed in the Service
namespace. The presenters are part of the client application - of which there may be hundreds of instances running. Updates to the client will not typically coincide with updates to the server, so in addition to concerning yourself with network latency and utilization, you must also be careful to maintain compatibility between the server and clients. Ideally, you will usually want to deploy a new server first that can still communicate with the old clients, but that has additional capability that can be used by new clients.
In LEK, communication between the client and the server is restricted to calls made by the presenters to a service object defined in the Service
namespace. The Presenter
namespace, however, has no dependency to the Service
namespace. Instead, the presenters are provided (by WinFormClient
) with WCF proxy objects that implement an interface defined in the ServiceInterface
namespace. All the types that flow across this interface are very simple types with little or no logic - data transfer objects. The types are defined in Util
(for those not specific to LEK), GlobalType
(those that have to make it all the way to the views in WinUI
), and DTO
(the vast majority). The DomainUtil
namespace knows about the DTO
, GlobalType
, and Util
namespaces so it can create these data transfer objects. It does not, however, know about ServiceInterface
, and ServiceInterface
does not know about it (DomainUtil
). These dependency restrictions will ensure that DomainUtil
is only used for logic that can be tested without having to create mocks of the server object, and that all tests of presenter objects can be tested by creating mocks of the server object. Furthermore, isolating the interface and the data transfer objects into their own namespaces helps the developer (there could be many) focus his or her attention on these parts when necessary. When will it be necessary? Whenever client/server compatibility issues are being contemplated or whenever it is suspected that network latency or bandwidth are limiting the performance of the application.
10.9 Avoiding Mocking for Unit Tests
Contents at a Glance Detailed Contents
A well-factored application with near complete unit test coverage is easier to maintain than one with less extensive unit test coverage. Changes can be made with greater confidence that mistakes that make it past the compiler will be caught when running the unit tests. Integration tests that go through several layers are great at catching mistakes as well, but if they are extensive enough to cover the majority of the code, they will probably pass between two or more tiers and thus take much longer to run (and when problems surface, the tests won't point you to the source of the problem). It only takes a few milliseconds to instantiate an object, call some functions on it, and then verify that return values and final state are as expected. But if that object internally calls functions that cross process boundaries (through WCF or database connections) then they will take orders of magnitude longer to execute. An application with hundreds of tests that make calls across process boundaries will take several minutes to run, and the root cause of failures will be hard to track down. Faced with this prospect, developers will typically write fewer tests, use them less often, and spend more time waiting for tests to complete.
A layered design is a great improvement over designs characterized by "spaghetti code", but simple layering does not do much to make unit testing easier. Since the UI logic depends on the business logic, which in turn depends on the data access logic, testing UI logic often means test execution flow will cross process boundaries and thus exhibit the problems discussed above. One solution to this is mocking the lower layers, so that tests execute quickly and problems can be isolated to the layer under test and not the supporting code. But mocking takes time and adds complexity. Where possible, it is better to decouple code from the layers altogether. For example, instead of having a business object on the client that validates and transforms data and then passes this data to the backend to be persisted in the database, it is better to have a business object that simply validates and transforms the data according to the business rules. This way testing the business object will not require waiting for data to cross tiers and will not require mocking. Mocking may still be required to test the code that is responsible for instantiating the business object, getting the data transfer object from it, and forwarding this on to the backend, but this will be much less code to test.
In the previous step of the elaboration of the architecture, the Service
namespace was deemed to contain all the code for the server, which includes the code that interacts with the database. To reinforce the notion that as much of this code as possible should not depend on the database, a new namespace, called ServerUtil
, is created which will not be allowed to communicate with the database. Your goal as a developer should be to locate as much server side code as possible in the ServerUtil
namespace, where use of the database is forbidden, thus ensuring the most effective unit testing.
Likewise, in the previous step of the elaboration of the architecture, the Presenter
namespace was deemed to contain all the code for the client except for the code responsible for painting the UI and interacting with the user. To reinforce the notion that as much of this code as possible should be independent of the back-end, a new namespace, called ClientUtil
, is created without any dependency on the ServiceInterface
namespace. Your goal as a developer should be to locate as much client side code as possible in the ClientUtil
namespace, where use of the server object or WCF server proxies are prevented, thus ensuring the most effective unit testing.
10.10 Isolating the ORM Classes From the Rest of the App
Contents at a Glance Detailed ContentsAn ORM tool will give you the ability to map classes to tables in the database. It will also provide an API for using objects of these classes to move data into or out of the database. A key architectural decision you will need to make is what namespaces in your application should have access to the ORM classes. This decision will depend, in part, on an even more fundamental decision: how closely should the object model you work with in code match the relational model of your database? If you think that a data model created exclusively for expressing the business and presentation logic of your application would be very similar to a data model created exclusively for moving data into and out of a relational database, then you should probably make your ORM classes available to all the namespaces in your application. This is especially true if the application is relatively simple and has a short estimated life time (months). On the other hand, if you think that a data model created exclusively for expressing the business and presentation logic of your application would be significantly different from a data model created exclusively for moving data into and out of a relational database, then you should probably be more guarded in exposing your ORM classes to the rest of your application. This is especially true if the application is relatively complex and has a long life time (years).
Another factor to consider is how amenable the ORM tool is to generating ORM classes that don't exactly match your database schema. Entity Framework and NHibernate offer a lot of capability to do this. If you are comfortable using some of the more advanced ORM features to map classes to your database that don't exactly match up to your schema, then these classes will be more suitable for use in the different layers of your application. You should also consider how you feel about allowing your classes to carry around functions and attributes only used by the ORM layer. NHibernate lets you map classes to the database with very few artifacts of the mapping: they are "POCO" ("Plain Old Class Objects"). This makes them more suitable for use in different layers of your application. Entity Framework (at least version 3.5) and LINQ to SQL, on the other hand, require mapped classes with many special functions and attributes used by the ORM. Ideally classes used by your presentation and business rules should not expose methods and attributes that are never used by these layers.
Due to the fact that LEK is meant to show how to use different ORM tools, and because I am attempting to model the architecture for a complex and long lived application, I have architected LEK to be very guarded in exposing the ORM classes to the rest of the application. All the ORM classes in LEK are in the Dal
namespace (actually they each have their own, but that is in the next step). Dal
is only accessible to the Service
namespace (actually they each have their own one of these as well, but that is in the next step). The code in the Service
namespace transforms the ORM objects into data transfer objects when reading data from the database and transforms data transfer objects into ORM objects when writing data to the database. Both the Service
namespace and Presenter
namespace make use of business objects from DomainUtil
or ServerUtil
or ClientUtil
that implement the business logic of the application. The classes describing these business objects make up the true data model for the application. Typically, the business objects are initialized with the data transfer objects, and when asked to supply a snapshot of data for serialization to the database, they supply the data as data transfer objects. The Service
namespace contains the (custom, handwritten) code that translates between data transfer objects and ORM objects. The ORM classes very closely match the actual database schema, and are structured such that the mapping to the database is as simple as possible.
10.11 Isolating the ORM Dependant Code for Each ORM
Contents at a Glance Detailed ContentsRather than the Dal
namespace, LEK actually has a separate namespace for each of the three ORM tools being explored. DalEF
contains the ORM classes for Entity Framework, DalNH
contains the ORM classes for NHibernate, and DalLinqToSQL
contains the ORM classes for LINQ to SQL. DalEF
and DalLinqToSQL
each contain only the code generated by Visual Studio when generating the ORM classes from an existing database. For both, the generated code is slightly modified (through the designer UI) from the default to change how the ORM layer deals with concurrency issues. Although not machine-generated, the code in DalNH
provides the same functionality as that in DalEF
and DalLinqToSQL
: the ORM classes and the specifics of how they are mapped to the database are defined.
The Service
namespace is also replaced by three ORM specific namespaces: ServiceEF
, ServiceNH
, and ServiceLinqToSQL
. Each of these are independent of each other and depend on their corresponding Dal*
namespace. There is some logic in these that could be factored out into a common namespace, but since LEK is meant to explore how any one of these could be used in a Visual Studio project for a real application, and not how all three could be used in a Visual Studio project for a real application, this redundancy is not factored out.
ServiceEF
, ServiceNH
, and ServiceLinqToSQL
each provide exactly the same functionality: they provide the API that is published via WCF. This API is exactly described in the combination of the ServiceInterface
namespace and in the supporting namespaces that provide the specification of the data types (DTO
, GlobalType
, and Util
). The class in the ServiceFactory
namespace is responsible for instantiating the correct implementation of the interface, depending on the parameters that it is passed.
10.12 Dependencies: The Big Picture
Contents at a Glance Detailed Contents
Each box above represents both an assembly and a namespace of LEK. Blue text indicates the namespace is used on the server side and a red border indicates the namespace is used on the client side. Boxes with both blue text and a red border represent namespaces used on both the client and the server sides. As previously mentioned, in an actual application with a team of developers and hundreds of users, you would probably use fewer assemblies to improve build time, but still use a similar collection of namespaces and a tool like NDepend (http://www.ndepend.com) or Lattix (http://www.lattix.com) to manage dependencies between them.
Here is the dependency matrix for the LEK namespaces, as generated by NDepend
11 ORM Classes and Their Mapping to Database Entities
Contents at a Glance Detailed Contents
The primary objective of an ORM tool is to make it possible for you to move data into and out of the database using objects rather than SQL. Each of the technologies allows you to specify a set of classes that define these objects as well as a specification of how the fields of the classes map to the fields in the database. Adding data to the database involves instantiating instances of these classes and then passing the instances into an API provided by the ORM tool. Pulling data from the database involves passing a query into the API and getting back instances of these objects. The query may be similar to SQL, but is in terms of the ORM classes.
11.1 Generating a Default Set of ORM Classes in LINQ to SQL and Entity Framework
Contents at a Glance Detailed Contents
Visual Studio makes it very easy to automatically add a complete set of ORM classes for both Entity Framework and LINQ to SQL. You simply bring up the "Add New Item" dialog for a project, select either "ADO.NET Entity Data Model" (for Entity Framework) or "LINQ to SQL Classes" (for LINQ to SQL), and then follow the steps in the wizard that follows. In both cases, you can browse to a database, select tables, and then let the wizard generate a default set of classes.
11.1.1 LINQ to SQL
Contents at a Glance Detailed Contents
Here is the dialog that was used to add the LINQ to SQL ORM classes to LEK
And here is the DalLinqToSQL project after the operation was completed
The DataClasses.designer.cs file contains the ORM classes and the DataClasses.dbml file contains the mapping information.
11.1.2 Entity Framework
Contents at a Glance Detailed Contents
Here is the dialog that was used to add the Entity Framework ORM classes to LEK
And here is the DalEF project after the operation was completed
The Model.Designer.cs file contains the ORM classes and the Model.edmx file contains the mapping information.
11.2 ERDs
11.2.1 SQL Server Database
Contents at a Glance Detailed Contents
For both LINQ to SQL and Entity Framework, using the wizard to add the classes requires selecting the database that the classes would map to. Here is the SQL Server diagram of the database that was chosen in both cases
In both cases, the wizard creates a set of ORM classes that map one for one to the tables in the database. After the wizard completes, the ERD will be shown for the generated classes.
11.2.2 LINQ to SQL ORM Classes
Contents at a Glance Detailed Contents
Here is the ERD for the ORM classes generated by the wizard for LINQ to SQL
11.2.3 Entity Framework ORM Classes
Contents at a Glance Detailed Contents
Here is the ERD for the ORM classes generated by the wizard for Entity Framework
11.3 Examining an ORM Class for LINQ to SQL, Entity Framework, and NHibernate
Contents at a Glance Detailed Contents
The primary means of creating and modifying ORM classes and associated mapping information for LINQ to SQL and Entity Framework is through the Visual Studio IDE. For NHibernate, you must create and maintain the ORM classes and mapping information manually. However, the ORM classes and mapping information for NHibernate are much simpler than the corresponding code for LINQ to SQL and Entity Framework. In this section, I will show the ORM class and associated mapping information for the simplest entity in LEK: Post
. My intention is simply to show the kind of information in each of these files, and to allow you to compare the relative complexity of each. You probably shouldn't spend too much time at this point trying to interpret the code.
11.3.1 LINQ to SQL
Contents at a Glance Detailed Contents
11.3.1.1 The Post ORM Class for LINQ to SQL
Please see Code Window 001 in the version of this article on periodnet.blogspot.com.
11.3.1.2 The Post ORM Class Mapping for LINQ to SQL
<?xml version="1.0" encoding="utf-8"?>
<Database Name="xLekDev" Class="DataClassesDataContext"
xmlns="http://schemas.microsoft.com/linqtosql/dbml/2007">
<Connection Mode="AppSettings" ConnectionString
="Data Source=xxMACHINENAMExx;Initial Catalog=xLekDev;Integrated Security=True"
SettingsObjectName="DalLinqToSql.Properties.Settings"
SettingsPropertyName="LekDevConnectionString" Provider="System.Data.SqlClient" />
<!--text removed -->
<Table Name="dbo.Post" Member="Posts">
<Type Name="Post">
<Column Name="ID" Type="System.Int32" DbType="Int NOT NULL IDENTITY"
IsPrimaryKey="true" IsDbGenerated="true" CanBeNull="false" />
<Column Name="Instant" Type="System.DateTime" DbType="DateTime NOT NULL"
CanBeNull="false" />
<Association Name="Post_Transaction" Member="Transactions" ThisKey="ID"
OtherKey="PostID" Type="Transaction" />
</Type>
</Table>
<!--text removed -->
</Database>
11.3.2 Entity Framework
Contents at a Glance Detailed Contents
11.3.2.1 The Post ORM Class for Entity Framework
Please see the Code Window 003 in the version of this article on periodnet.blogspot.com.
11.3.2.2 The Post ORM Class Mapping for Entity Framework
Please see Code Window 004 in the version of this article on periodnet.blogspot.com.
11.3.3 NHibernate
Contents at a Glance Detailed Contents
11.3.3.1 The Post ORM Class for NHibernate
public class Post
{
int _iID;
public virtual int ID
{
get{return _iID;}
set{_iID =value;}
}
DateTime _dtInstant;
public virtual DateTime Instant
{
get{return _dtInstant;}
set{_dtInstant =value;}
}
}
11.3.3.2 The Post ORM Class Mapping for NHibernate
<?xml version="1.0"?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2"
namespace="DalNH"
assembly="DalNH">
<!-- Mappings for class 'Post' -->
<class name="Post" table="Post">
<!-- Identity mapping -->
<id name="ID">
<column name="ID" />
<generator class="native" />
</id>
<!-- Simple mappings -->
<property name="Instant" />
</class>
</hibernate-mapping>
11.3.4 Comparing the ORM Classes and Mapping for each ORM
Contents at a Glance Detailed Contents
The mapping and ORM classes for each ORM are the same in the following respects
- There is an ORM class for each table.
- For each field in the database there is a corresponding public property in the ORM class.
- Each foreign key field in the database is mapped to an entity property in the ORM class. The entity property in the ORM class corresponds to the record in the parent table that the foreign key points to (this is not shown in the
Post
class above).
Some of the ways they differ are
- In the LINQ to SQL and Entity Framework classes and mappings, the inverse of the foreign keys are also mapped. That is, if there is a foreign key in the database from a child entity to a parent entity, then the ORM class for the parent entity will have a public property for a collection of child entities (that map to the child entities in the database). This sort of mapping is optional in NHibernate. For example, the NHibernate version of the
Post
ORM class could have a property for a collection ofTransaction
s, but I chose not to create one. - In the LINQ to SQL mappings, foreign keys in the database are also mapped to integer members of the ORM classes (this is not shown in the
Post
class above).
11.4 ORM Classes and Mapping Info Specifications for NHibernate
Contents at a Glance Detailed Contents
Since the ORM classes and mappings were created manually for NHibernate, I will discuss them in detail. Here is a snapshot of the solution project for the NHibernate ORM classes and mappings
I chose to collect all the ORM classes into a single file: DalNH.cs, and to have a separate mapping file (the *.hbm.xml file) for each entity.
11.4.1 NHibernate ORM Classes
Contents at a Glance Detailed Contents
The code window below shows all the contents of DalNH.cs. The format is somewhat compressed so that common elements line up vertically
Please see Code Window 007 in the version of this article on periodnet.blogspot.com.
NHibernate allows ORM classes to be "POCO": Plain Old CLR Objects. They don't have to have any of the attributes or special functions that the ORM classes for LINQ to SQL and Entity Framework do. This makes them more suitable for use in many layers of your application. An NHibernate ORM class does have to have public setters for all the properties that map to fields in the database, and they must be virtual. These two requirements are enough to dissuade me from using them outside of the data access layer in LEK. (NHibernate ORM classes are, however, much more suitable for use outside of the data access layer than are LINQ to SQL or Entity Framework 3.5 ORM classes.)
As with the other ORM classes, all foreign keys are mapped to properties of an entity type. None are mapped to simple integer properties as they are in the LINQ to SQL ORM classes. Unlike the ORM classes for the other ORMs, only some of the possible child collections are mapped. These include WashDlt
s within a Transaction
, NWAccntDlts
within a Transaction
, Item
s within a Transaction
, and DltAlloc
s within an Item
. Recall that this is by choice: I manually created the ORM classes for NHibernate, whereas the Visual Studio IDE created the ORM classes for LINQ to SQL and Entity Framework.
The child records for a Transaction
and the child records for an Item
("grandchildren" of a Transaction
) are mapped so that a Transaction
and all its details can be treated as one object in code when updating. When a Transaction
is saved in LEK, the Item
s in the Transaction
and the allocations for each Item
are also saved. When a DltAllocAccnt
entity is saved on the other hand, only the DltAllocAccnt
database record is touched. The allocations (DltAlloc
records, each of which does refer to a DltAllocAccnt
) are not touched.
11.4.2 NHibernate ORM Class Mapping Specification
Contents at a Glance Detailed Contents
The DalNH project contains a separate XML file for the mapping specification of each entity. In the below code window, I have combined all of these into a single XML specification and compressed the format somewhat so that it is easier to see the correlation between the XML markup and the entity properties
Please see Code Window 008 in the version of this article on periodnet.blogspot.com.
Although it must be created and maintained manually, it is much less code than is required for the mappings in LINQ to SQL and Entity Framework.
11.4.2.1 Arranged and Organized
If the lines of the specification are rearranged somewhat (for demonstration only, this is invalid XML), it becomes easier to grasp what is being specified and how it is being specified
Please see Code Window 009 in the version of this article on periodnet.blogspot.com.
Each section in the code window above is described below
11.4.2.2 Classes
Class
elements are used to map ORM classes to database tables.
11.4.2.3 Primary Keys
Id
elements are used to map ORM class properties to primary keys in the database. The contained generator
element in each id
element indicates to NHibernate that values for these fields are automatically generated by the database (they are each an identity column in SQL Server).
11.4.2.4 Row Versions
Version
elements are used to map ORM class properties to SQL Server timestamp fields in the database. (A timestamp field for a record in SQL Server is automatically updated whenever any of the fields of the record are updated. Timestamps are used for optimistic concurrency.)
11.4.2.5 FKs That Can be Null
Many-to-one
elements without a not-null
attribute are used to map ORM class properties (that are themselves entities) to nullable foreign keys in the database.
11.4.2.6 FKs to Parents that Don't Track Entity as a Child
Many-to-one
elements with a not-null
attribute are used to map ORM class properties (that are themselves entities) to non-nullable foreign keys in the database.
11.4.2.7 FKs to Parents that Track Entity as a Child
Many-to-one
elements with a cascade='save-update'
attribute are used to map ORM class properties (that are themselves entities) to foreign keys in the database when the entity corresponding to the foreign key tracks the entity containing the property as a child object.
11.4.2.8 Children
Set
elements are used to map ORM class properties (that are a collection of child entities) to a collection of child records in the database.
11.4.2.9 Simple Fields
Property
elements are used to map ORM class properties to fields in the database that are not keys, timestamps, or child records.
12 How the ORM Classes Fit in the Architecture
Contents at a Glance Detailed Contents
Each ORM has a namespace for the ORM classes and a namespace for the data access code that uses the ORM classes. Here is the dependency diagram that was shown earlier:
LINQ to SQL 的 ORM 类位于 DalLinqToSQL
命名空间中,Entity Framework 的 ORM 类位于 DalEF
命名空间中,NHibernate 的 ORM 类位于 DalNH
命名空间中。我将把这些命名空间称为“Dal*
”命名空间。
LINQ to SQL ORM 类仅在 ServiceLinqToSQL
命名空间中使用,Entity Framework ORM 类仅在 ServiceEF
命名空间中使用,NHibernate ORM 类仅在 ServiceNH
命名空间中使用。我将把这些命名空间称为“Service*
”命名空间。
12.1 服务类
Contents at a Glance Detailed Contents
每个 Service*
命名空间都包含一个 Service
类,该类定义了一个单例对象,能够提供客户端所需的所有数据访问服务。运行时,将实例化一个 Service
对象(针对其中一种 ORM)。它将能够访问相应 ORM 的 API。要将数据持久化到数据库,客户端会将数据传输对象发送到服务单例,服务单例会将其转换为 ORM 类对象,并使用 ORM 的 API 将其保存到数据库。从数据库读取数据基本上是反向过程:在收到数据请求后,服务单例使用 ORM 的 API 生成包含数据库数据的 ORM 类对象,然后将其转换为数据传输对象并发送回客户端。
12.2 IService 接口
Contents at a Glance Detailed Contents
每种 ORM 的 Service 类都实现了一个名为 IService
的接口。客户端只知道它用于数据访问的对象实现了 IService
接口。(我在这里松散地使用“客户端”一词,实际上是指 Presenter
命名空间中的类(参见上图)。客户端本身是由用户运行的托管应用程序创建的(并在 WinFormClient
或 WinApp
命名空间中定义)。如果 LEK 在三层配置中运行,那么 WCF 服务器(WinFormServer
命名空间,中间层)将通过工厂实例化一个 Service 类,并通过 WCF 公开它。每个托管应用程序(WinFormClient
命名空间)将实例化一个到服务器端单例的 WCF 代理,并将该代理传递给其包含的客户端(Presenter
命名空间)。代理也实现了 IService
。如果 LEK 在两层配置中运行,那么托管应用程序(WinApp
命名空间)将直接实例化一个 Service 类,并将其传递给其包含的客户端。无论哪种情况,客户端只知道它有一个实现了 IService
的对象。它不知道也不关心该对象是服务器上单例的代理还是其自身进程中单例的代理。它也不知道也不关心该对象使用的是 LINQ to SQL、Entity Framework 还是 NHibernate。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 010。
13 使用 ORM 框架进行简单操作
Contents at a Glance Detailed Contents
在 LEK 中,Participant
、NWAccnt
和 DltAllocAccnt
实体的操作非常相似。对于每个实体,都有一个简单的窗体,其中包含一个数据网格,显示数据库中的所有实体。对于每个实体,数据网格允许编辑任何 Item
,并允许添加新的 Item
。更改以批处理方式保存:用户编辑或添加了几个实体后,用户一次性将所有更改提交到数据库。UI 利用网格行与实体对象之间的双向绑定,这使得 UI 代码极其简单。绑定到网格的实体对象的类不同于 ORM 类。前者定义在 GlobalType
命名空间中,不依赖于任何 ORM 框架。后者定义在 dal*
命名空间中,每个命名空间都与一个 ORM 框架紧密关联。在 LEK 源代码中,上面提到的三个 GlobalType
实体的实际类派生自 VerySimpleTwoWayDTO
,它也位于 GlobalType
命名空间中。本节将通过关注 Participant
GlobalType
和 ORM 类来描述框架的简单用法。然而,对于 GlobalType
Participant
类,本讨论将使用一个没有基类的版本。这应该有助于更轻松地关注本节的概念,而无需深入研究 Participant
GlobalType
的继承结构。
13.1 行版本(GlobalType 命名空间)
Contents at a Glance Detailed Contents
Participant
GlobalType
依赖于 RowVersion
,它也位于 GlobalType
命名空间中。RowVersion
是 LEK 中用于数据库时间戳字段的 ORM 独立类型。SQL Server 中的时间戳字段实际上是版本戳。对于具有时间戳字段的表,每次记录中的任何字段发生更改时,SQL Server 都会自动更新记录的时间戳字段。LEK 使用时间戳字段来实现乐观并发。
[ser.DataContract]
public class RowVersion
{
[ser.DataMember]byte[] _data;
public RowVersion(byte[] data)
{
_data = data;
}
public byte[] GetCopyOfData()
{
if(_data==null)
return null;
return (byte[])_data.Clone();
}
public static bool ValueEquals(RowVersion l, RowVersion r)
{
if(l == r) return true;
if(l == null) return false;
if(r == null) return false;
if(l._data == r._data) return true;
if(l._data == null) return false;
if(r._data == null) return false;
if(l._data.Length != r._data.Length) return false;
int C = l._data.Length;
for(int i=0; i<C; i++)
{
if(l._data[i] != r._data[i])
return false;
}
return true;
}
}
13.2 Participant (GlobalType 命名空间)
Contents at a Glance Detailed Contents
除了为数据库中的每个字段都有一个成员外,Participant
GlobalType
还具有可以跟踪实体编辑状态的成员。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 012。
13.3 Service 类
Contents at a Glance Detailed Contents
在 LEK 的运行实例中,Service
对象负责接收客户端的请求并生成响应。为每个 ORM 框架都定义了一个 Service 类,但在运行时只实例化一个。Service 类位于 Service*
命名空间中。
13.4 简单数据读取操作
Contents at a Glance Detailed Contents
每个 Service 类都有一个名为 Get2WayParticpants()
的公共函数,该函数负责以 GlobalType
.Participant
对象数组的形式返回所有 Participant
的集合,并按 Participant
名称排序。
13.4.1 数据访问代码
Contents at a Glance Detailed Contents
13.4.1.1 LINQ to SQL
下面是 LINQ to SQL 的 Service 类中 Get2WayParticipants
函数的版本
public gt.Participant[] Get2WayParticipants()
{
using (var ctx = new dal.DataClassesDataContext(CONN))
{
var strm =
from e in ctx.Participants
orderby e.Name
select new gt.Participant(e.ID, new gt.RowVersion(e.timestamp.ToArray()),
e.Name);
return strm.ToArray();
}
}
首先创建一个 LINQ to SQL 的上下文对象。接下来,指定一个 LINQ 查询,该查询从数据库检索 ORM 实体对象,并将它们转换为 GlobalType
.Participant
对象。最后,通过调用 strm.ToArray()
,执行查询并将结果返回给调用者。
13.4.1.2 Entity Framework
Entity Framework 版本与 LINQ to SQL 版本不同之处在于 GlobalType
.Participant
对象的创建发生在框架之外而不是内部。随 .NET 3.5 SP1 附带的 Entity Framework 版本无法通过调用带参数的构造函数来投影结果到对象。因此,结果被投影到一个匿名类型,然后通过 LINQ to Objects 表达式将它们转换为 GlobalType
.Participant
。
public gt.Participant[] Get2WayParticipants()
{
using (var ctx = new dal.LekEntities(CONN))
{
var strm =
from e in ctx.Participant
orderby e.Name
select new {e.ID, e.timestamp, e.Name};
//Entity Framework can't handle constructors with parameters
//so the gt.Participants will be created outside of EF
var lst = strm.ToList();
IEnumerable<gt.Participant> strmParticipants = lst.Select(
e=>new gt.Participant(e.ID, new gt.RowVersion(e.timestamp), e.Name)
);
return strmParticipants.ToArray();
}
}
13.4.1.3 NHibernate
NHibernate 版本函数使用一种完全不同的范式从数据库中读取实体。它不创建 LINQ 查询,而是创建一个 NHibernate Criteria
对象,并在此基础上添加过滤、投影和排序信息。NHibernate Lambda 扩展(可在 http://code.google.com/p/nhlambdaextensions/ 获取)的使用使设置 Criteria 变得更加容易。没有这个库,创建 Criteria
对象需要将 ORM 实体名称和字段作为字符串提供。
与 Entity Framework 版本一样,NHibernate 版本函数通过 LINQ to Objects 表达式将结果转换为 GlobalType
.Participant
对象集合。
public gt.Participant[] Get2WayParticipants()
{
using(var ctx = dal.SessionFactory.OpenSession(CONN))
{
nh.ICriteria aCriterea = ctx.CreateCriteria(typeof(dal.Participant));
aCriterea.AddOrder<dal.Participant>(e=>e.Name, nh.Criterion.Order.Asc);
IList<dal.Participant> lst = aCriterea.List<dal.Participant>();
IEnumerable<gt.Participant> strmParticipants= lst.Select(
e=>new gt.Participant(e.ID, new gt.RowVersion(e.timestamp), e.Name)
);
return strmParticipants.ToArray();
}
}
13.4.2 生成的 SQL
Contents at a Glance Detailed Contents
Microsoft 的 SQL Server Profiler 对于检查 ORM 生成的 SQL 非常有用。如果您正在编写与之前创建的不同的 LINQ 语句或 NHibernate Criteria
对象,您可能需要检查生成的 SQL 是否符合预期。
13.4.2.1 LINQ to SQL
为 Get2WayParticipants()
的 LINQ to SQL 版本生成的 SQL 是直接的。
SELECT
[t0].[ID] AS [iID],
[t0].[timestamp],
[t0].[Name] AS [strName]
FROM
[dbo].[Participant] AS [t0]
ORDER BY
[t0].[Name]
13.4.2.2 Entity Framework
Get2WayParticipants()
的 Entity Framework 版本生成的 SQL 稍微复杂一些,但结果相同。
SELECT
[Project1].[C1] AS [C1],
[Project1].[ID] AS [ID],
[Project1].[timestamp] AS [timestamp],
[Project1].[Name] AS [Name]
FROM (
SELECT
[Extent1].[ID] AS [ID],
[Extent1].[Name] AS [Name],
[Extent1].[timestamp] AS [timestamp],
1 AS [C1]
FROM [dbo].[Participant] AS [Extent1]
) AS [Project1]
ORDER BY
[Project1].[Name] ASC
13.4.2.3 NHibernate
为 Get2WayParticipants()
的 NHibernate 版本生成的 SQL 与为 LINQ to SQL 创建的 SQL 几乎相同。
SELECT
this_.ID as ID9_0_,
this_.timestamp as timestamp9_0_,
this_.Name as Name9_0_
FROM
Participant this_
ORDER BY
this_.Name asc
13.5 简单数据写入操作
Contents at a Glance Detailed Contents
每个 Service 类都有一个名为 UpdateParticipants()
的公共函数,该函数负责从传入的 GlobalType
.Participant
对象集合更新数据库。传入的集合可以包含未修改的 Participant
、已修改的 Participant
和新的 Participant
。该函数将忽略未更改的 Participant
,更新已修改的 Participant
,并添加新的 Participant
。如果已修改的 GlobalType
.Participant
对象的 RowVersion
字段的值与数据库中相应的时间戳字段的值不同,则 LEK 将假定记录自快照拍摄以来已更改(在调用 Get2WayParticipants()
时),并抛出一个专门设计的异常。
如前所述,Participant
、NWAccnt
和 DltAllocAccnt
实体非常相似。这些实体的 GlobalType
版本实际上都派生自一个共同的基类 VerySimpleTwoWayDTO
。然而,本文档将 GlobalType
.Participant
呈现为一个单一的、非派生的类。同样,在 LEK 的源代码中,UpdateParticpants()
、UpdateNWAccnts()
和 UpdateDltAllocAccnts()
都是辅助函数,它们会将参数以及几个委托传递给一个名为 UpdateVerySimpleDTO()
的私有函数,该函数执行所有工作。然而,本文档将 UpdateParticipants()
呈现为代表这三个函数,并将其呈现为几乎独立完成所有工作,而无需通用实现函数。
13.5.1 数据访问代码
13.5.1.1 与任何 ORM 无关的支持代码
Contents at a Glance Detailed Contents
为并发错误抛出的专门设计的异常包含并发问题的详细信息,该信息存储在一个自定义类型 ConcurrencyFault
的容器对象中,该类型定义在 DTO
命名空间中。
[ser.DataContract]
public class ConcurrencyFault
{
[ser.DataMember] public string Description{get; private set;}
public ConcurrencyFault(string strDescription)
{
Description = strDescription;
}
}
该异常的实际类型是基于 .NET Framework 提供的泛型类型 System.ServiceModel.FaultException
和上述 ConcurrencyFault
类型构建的类型。System.ServiceModel.FaultException
专为与 SOAP 和 WCF 配合使用而设计。
在 ServerUtil
命名空间中定义了一个工厂方法,用于创建并发异常作为 System.ServiceModel.FaultException<DTO.ConcurrencyFault>
对象。
public static s.ServiceModel.FaultException<DTO.ConcurrencyFault>
CreateConcurrencyFaultException(string strDescrip)
{
DTO.ConcurrencyFault fault = new DTO.ConcurrencyFault(strDescrip);
return new s.ServiceModel.FaultException<DTO.ConcurrencyFault>(
fault,
"ConcurrencyFault: " + fault.Description);
}
13.5.1.2 用于简单数据写入操作的 LINQ to SQL 代码
Contents at a Glance Detailed Contents
UpdateParticipants()
的 LINQ to SQL 版本还依赖于一个辅助函数,用于将 ORM 独立的的“时间戳”类型(GlobalType.RowVersion
)转换为 LINQ to SQL 框架用来表示时间戳的类型(System.Data.Linq.Binary
)。
public static s.Data.Linq.Binary CreateORMRowVersion(gt.RowVersion aRowVersion)
{
if(aRowVersion==null)
return null;
return new System.Data.Linq.Binary(aRowVersion.GetCopyOfData());
}
因此,在涵盖了 UpdateParticipants()
所依赖的所有内容之后,终于展示了 UpdateParticipants()
的 LINQ to SQL 版本。
public void UpdateParticipants(gt.Participant[] arr)
{
using (var ctx = new dal.DataClassesDataContext(CONN))
{
foreach (gt.Participant o in arr)
{
dal.Participant e = new dal.Participant();
if (o.New)
{
e.Name = o.Name;
ctx.Participants.InsertOnSubmit(e);
}
else if (o.Modified)
{
e.ID = o.ID.Value;
e.timestamp=CreateORMRowVersion(o.GetRowVersion());
e.Name = o.Name;
ctx.Participants.Attach(e, /*asModified=*/true);
}
}
try
{
ctx.SubmitChanges();
}
catch(s.Data.Linq.ChangeConflictException)
{
throw su.ServerUtil.CreateConcurrencyFaultException(
typeof(dal.Participant).Name);
}
}
}
该函数遍历所有传入的 GlobalType
.Participant
,添加新的,用修改过的更新数据库,忽略其余的。如果 LINQ to SQL 框架发现正在更新的记录的时间戳字段与用于更新记录的对象的时间戳字段不同,则框架将抛出 System.Data.Linq.ChangeConflictException
。此异常将被捕获,转换为(非 ORM 特定)的 System.ServiceModel.FaultException<DTO.ConcurrencyFault>
,然后重新抛出。
13.5.1.3 用于简单数据写入操作的 Entity Framework 代码
Contents at a Glance Detailed Contents
UpdateParticipants()
的 Entity Framework 版本在结构上非常相似,但要做到这一点,使用随 .NET Framework 3.5 SP1 提供的 Entity Framework 版本需要几个辅助函数。如果你愿意查询 ORM 对象、更新它,然后提交更改,那么这个版本的 Entity Framework 就很容易使用。但如果你不愿意查询你已有的数据(因为你不想比必要时更频繁地访问数据库),那么你必须经历很多麻烦。
使用 LINQ to SQL,手动设置 ORM 实体的“主键”就像将整数键分配给映射到数据库中主键的 ORM 实体字段一样简单(例如:e.ID = iID.Value
)。使用 Entity Framework,键以 System.Data.EntityKeys
的形式存储在 ORM 对象中,它可以容纳非整数复合键。然而,这种额外的灵活性是有代价的:创建这种类型的键比分配一个简单的整数要困难得多。下面是创建一个 System.Data.EntityKey
的辅助函数,当它只是一个整数时。
public static s.Data.EntityKey CreateEntityKey(int iID,
string strQualifiedEntitySetName)
{
IEnumerable<KeyValuePair<string, object>> entityKeyValues =
new KeyValuePair<string, object>[] {
new KeyValuePair<string, object>("ID", iID) };
return new s.Data.EntityKey(strQualifiedEntitySetName, entityKeyValues);
}
与 LINQ to SQL 一样,UpdateParticipant()
的 Entity Framework 版本也依赖于一个辅助函数,用于将 ORM 独立的“时间戳”类型(GlobalType
.RowVersion
)转换为 Entity Framework 用于表示时间戳的类型(Byte[]
)。
public static Byte[] CreateORMRowVersion(gt.RowVersion aRowVersion)
{
if(aRowVersion==null)
return null;
return aRowVersion.GetCopyOfData();
}
更新您刚刚查询过的 Entity Framework ORM 对象很简单:只需更新字段,然后对上下文调用 SaveChanges()
。与 LINQ to SQL 一样,这仅在您使用与保存对象相同的上下文查询该对象时才有效(可能意味着您刚刚查询过它)。如果相反,您将使用您自己创建的 ORM 实体(或使用之前使用其他上下文查询的实体)来更新数据库,那么您必须将其“附加”到上下文。使用 LINQ to SQL,附加包含已更新数据的 ORM 对象需要调用上下文的 Attach()
函数,并将 AsModified
参数设置为 true。Entity Framework 上下文(在 .NET 3.5 SP1 中)也有一个“Attach”函数,但它没有 AsModified
参数可以设置。相反,您必须遍历 ORM 对象的元数据,专门指示哪些字段包含已更新的数据。如果您的 ORM 对象有几十个字段,只有少数几个字段会更新,那么这是好事,因为您无论如何都想这样做,这样生成的 SQL 就不会尝试更新未更改的字段。但对于您只想更新几乎所有字段的简单情况,没有简单的方法。
UpdateParticipants()
的 Entity Framework 版本依赖于 SetAllPropsAsModified()
辅助函数,以指示 Entity Framework 最近附加的实体中几乎所有字段都应用于更新关联数据库记录中的相应字段。
public static void SetAllPropsAsModified(dal.LekEntities ctx, object entity)
{
s.Data.Objects.ObjectStateEntry aObjectStateEntry
= ctx.ObjectStateManager.GetObjectStateEntry(entity);
s.Collections.ObjectModel.ReadOnlyCollection<s.Data.Common.FieldMetadata>
collFieldMetadatas
= aObjectStateEntry.CurrentValues.DataRecordInfo.FieldMetadata;
foreach(var propertyName in collFieldMetadatas.Select(o => o.FieldType.Name))
{
if(propertyName == "ID")
continue;
if(propertyName == "timestamp")
continue;
aObjectStateEntry.SetModifiedProperty(propertyName);
}
}
通过依赖上述辅助函数,UpdateParticipants()
的 Entity Framework 版本可以采用与 LINQ to SQL 版本非常相似的形式。
public void UpdateParticipants(gt.Participant[] arr)
{
string strQualifiedEntitySetName = typeof(dal.LekEntities).Name + "."
+ typeof(dal.Participant).Name;
using (var ctx = new dal.LekEntities(CONN))
{
foreach (gt.Participant o in arr)
{
dal.Participant e = new dal.Participant();
if (o.New)
{
e.Name = o.Name;
ctx.AddToParticipant(e);
}
else if (o.Modified)
{
e.ID = o.ID.Value;
e.timestamp = CreateORMRowVersion(o.GetRowVersion());
e.EntityKey = CreateEntityKey(e.ID, strQualifiedEntitySetName);
e.Name = o.Name;;
ctx.Attach(e);
SetAllPropsAsModified(ctx, e);
}
}
try
{
ctx.SaveChanges();
}
catch(s.Data.OptimisticConcurrencyException)
{
throw su.ServerUtil.CreateConcurrencyFaultException(
typeof(dal.Participant).Name);
}
}
}
与 LINQ to SQL 版本一样,该函数遍历所有传入的 GlobalType
.Participant
,添加新的,用修改过的更新数据库,忽略其余的。如果 Entity Framework 发现正在更新的记录的时间戳字段与用于更新记录的对象的时间戳字段不同,那么 Entity Framework 将抛出 System.Data.OptimisticConcurrencyException
。此异常将被捕获,转换为(非 ORM 特定)的 s.ServiceModel.FaultException<DTO.ConcurrencyFault>
,然后重新抛出。
13.5.1.4 用于简单数据写入操作的 NHibernate 代码
Contents at a Glance Detailed Contents
UpdateParticipants()
函数的 NHibernate 版本与 LINQ to SQL 和 Entity Framework 版本相似。
用于创建时间戳的 ORM 特定类型的辅助函数与 Entity Framework 中的相同。
public static Byte[] CreateORMRowVersion(gt.RowVersion aRowVersion)
{
if(aRowVersion==null)
return null;
return aRowVersion.GetCopyOfData();
}
与 LINQ to SQL 一样,主键为整数的整数可以直接设置,您无需遍历对象的字段来指示字段包含已更新数据。
public void UpdateParticipants(gt.Participant[] arr)
{
using(var ctx = dal.SessionFactory.OpenSession(CONN))
{
foreach (gt.Participant o in arr)
{
dal.Participant e = new dal.Participant();
if (o.New)
{
e.Name = o.Name;
ctx.Save(e);
}
else if (o.Modified)
{
e.ID = o.ID.Value;
e.RowVersion = CreateORMRowVersion(o.GetRowVersion());
e.Name = o.Name;
ctx.Update(e);
}
}
try
{
ctx.Flush();
}
catch(nh.StaleObjectStateException)
{
throw su.ServerUtil.CreateConcurrencyFaultException(
typeof(dal.Participant).Name);
}
}
}
与其他版本一样,该函数遍历所有传入的 GlobalType
.Participant
,添加新的,用修改过的更新数据库,忽略其余的。如果 NHibernate 发现正在更新的记录的时间戳字段与用于更新记录的对象的时间戳字段不同,那么 NHibernate 将抛出 NHibernate.StaleObjectStateException
。此异常将被捕获,转换为(非 ORM 特定)的 s.ServiceModel.FaultException<DTO.ConcurrencyFault>
,然后重新抛出。
13.5.2 用于简单数据写入操作的生成 SQL
Contents at a Glance Detailed Contents
下面的代码窗口展示了当 UpdateParticipants()
接收到一个已更新的 Participant
和一个新 Participant
时,每个 ORM 生成的 SQL。我从 SQL Server Profiler 跟踪中获取了 SQL。我重新格式化了代码并添加了注释。
13.5.2.1 LINQ to SQL
-- INSERT TSQL
INSERT INTO [dbo].[Participant]([Name])
VALUES (@p0)
SELECT [t0].[ID], [t0].[timestamp]
FROM [dbo].[Participant] AS [t0]
WHERE [t0].[ID] = (SCOPE_IDENTITY())
-- INSERT PARAMS
-- varchar(5) @p0='Misty'
-- UPDATE TSQL
UPDATE [dbo].[Participant]
SET [Name] = @p2
WHERE ([ID] = @p0) AND ([timestamp] = @p1)
SELECT [t1].[timestamp]
FROM [dbo].[Participant] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[ID] = @p3)
-- UPDATE PARAMS
-- int @p0=2
-- timestamp @p1=0x000000000001334F
-- varchar(5) @p2='Debra'
-- int @p3=2
13.5.2.2 Entity Framework
-- UPDATE TSQL
update [dbo].[Participant]
set [Name] = @0
where (([ID] = @1) and ([timestamp] = @2))
select [timestamp]
from [dbo].[Participant]
where @@ROWCOUNT > 0 and [ID] = @1
-- UPDATE PARAMS
-- varchar(5) @0='Debra',
-- int @1=2,
-- binary(8) @2=0x0000000000013371
-- INSERT TSQL
insert [dbo].[Participant]([Name])
values (@0)
select [ID], [timestamp]
from [dbo].[Participant]
where @@ROWCOUNT > 0 and [ID] = scope_identity()
-- INSERT PARAMS
-- varchar(5) @0='Misty'
13.5.2.3 NHibernate
-- INSERT TSQL, PART 1
INSERT INTO Participant (Name) VALUES (@p0);
select SCOPE_IDENTITY()
-- INSERT PARAMS, PART 1
-- nvarchar(5) @p0=N'Misty'
-- INSERT TSQL, PART 2
SELECT participan_.timestamp as timestamp9_
FROM Participant participan_
WHERE participan_.ID=@p0
-- INSERT PARAMS, PART 2
-- int @p0=5
-- UPDATE TSQL, PART 1
UPDATE Participant
SET Name = @p0
WHERE ID = @p1 AND timestamp = @p2
-- UPDATE PARAMS, PART 1
-- nvarchar(5) @p0=N'Debra',
-- int @p1=2,
-- varbinary(8) @p2=0x0000000000013383
-- UPDATE TSQL, PART 2
SELECT participan_.timestamp as timestamp9_
FROM Participant participan_
WHERE participan_.ID=@p0
-- UPDATE PARAMS, PART 2
-- int @p0=2
14 使用 ORM 框架进行复杂查询
Contents at a Glance Detailed Contents
考虑创建一个符合某些标准(如日期范围或过账状态)的 Transaction
实体集合所需的工作。您应该能够使用 ORM 特定语法指定 Transaction
的标准,将其提交给 ORM,并返回一个包含所有其子实体的 Transaction
实体集合。Transaction
的子实体包括 NWAccntDlt
s、WashDlt
s 和 Item
s。Transaction
的孙子实体是每个(子)Item
的 DltAlloc
s。为 LINQ to SQL 和 Entity Framework 创建的默认 ORM 集,以及为 NHibernate 已描述的自定义 ORM,都包含这些子实体成员。然而,在每个 ORM 上发出典型的查询以返回 Transaction
实体集合不会自动返回所有子实体都已初始化的实体集。默认情况下,ORM 提供“延迟加载”,即关联实体在您明确请求之前不会加载。因此,在发出简单的 Transaction
查询后,我们可以遍历 Transaction
实体列表并访问每个子实体以从数据库加载它们。然而,这可能会导致对数据库发出大量查询。如果 Transaction
平均有两个 NWAccntDlt
s、四个 WashDlt
s、四个 Item
s,以及每个 Item
有四个 DltAlloc
s,并且只返回 10 个 Transaction
,那么总共将发出 261 个单独的数据库查询(1 个查询用于 Transaction
列表,20 个查询用于 NWAccntDlt
s,40 个查询用于 WashDlt
s,40 个查询用于 Item
s,以及 160 个查询用于 DltAlloc
s)。显然,我们需要避免这种情况。
我相信,对于每种 ORM,都可以指定 ORM 类映射,以便框架“急切”加载指定的子实体。很容易想象这在一个简单的父子关系中是如何工作的:ORM 框架将连接涉及的表以子实体的结果集形式返回,并将父字段附加到每个子实体。为了将此结果集转换为实体,框架将遍历结果集,为每个结果创建一个子实体,并为具有唯一父字段集值的每个结果创建一个父实体。您可能会想到,对于我们拥有的比 Transaction
更复杂的多个子实体关系的 SQL 会相当复杂。此外,返回的数据会更加冗余,因为每个子实体可能都需要携带父实体字段。
ORM 可以采用的另一种策略是首先使用简单查询获取顶级对象的列表,然后,对于每个顶级对象,发出对每种类子实体的查询。在上面描述的示例场景中,这将总共产生 41 个查询——好多了,但仍然是大量的数据库命中。
更好的策略是,如果 ORM 为每种实体类型执行一个单独的查询,然后将结果组合成实体。在示例场景中,这将只产生 4 个查询,并且与返回的 Transaction
数量无关(即使 100 个 Transaction
满足条件而不是只有 10 个,仍然只有 4 个查询)。
我不知道这三个 ORM 是否都可以配置为利用我刚才描述的最后一种策略。如果可以,这将是 ORM 的高级用法,而我还没有足够了解来描述它。但是我可以自己轻松地实现这种策略,从而让 ORM 保持其基础的、易于访问的配置。
总结一下,目标是生成一个符合某些标准并且包含所有子数据(descendent data)的 Transaction
实体列表。为了实现这一点,将执行五个单独的查询:
- 一个查询,用于返回匹配标准的
Transaction
。 - 一个查询,用于返回匹配
Transaction
的NWAccntDlt
s。 - 一个查询,用于返回匹配
Transaction
的WashDlt
s。 - 一个查询,用于返回匹配
Transaction
的Item
s。 - 一个查询,用于返回匹配
Transaction
的Item
s 的DltAlloc
s。
每个查询都将按 Transaction
日期和 ID 对结果进行排序。将执行一个例程,该例程以协调的方式处理这 5 个结果流,通过组合 Transaction
实体对象及其子实体对象来创建包含完整初始化的 Transaction
实体的集合。
这项工作面临的最大挑战,以及对您自身工作影响最大的挑战,是如何在存在许多可能的 Transaction
标准的情况下,最大限度地减少重复代码。标准可能性包括 Transaction
:
- 发生在指定日期之前。
- 发生在指定日期或之后。
- 发生在指定日期范围内。
- 具有指定的 ID。
- ID 在指定范围内。
- 具有指定的
Post
ID。 Post
ID 在指定范围内。- 小于或等于指定值的
Post
ID。 - 大于或等于指定值(或
Post
ID 为 NULL)的Post
ID。 - 已过账(具有非 NULL
Post
ID)。 - 未过账(具有 NULL
Post
ID)。
14.1 标准设置代码的隔离
Contents at a Glance Detailed Contents
我们希望避免为需要执行的 5 个查询中的每一个重复设置标准的代码。设置标准的代码需要与其余逻辑隔离。每种 ORM 都需要不同的方法,有些方法会更好。
考虑以下两个实体类,它们与 LEK 或任何 ORM 框架无关。
public class State
{
public string Name{get;set;}
public int Area{get;set;}
public int Perimiter{get;set;}
}
public class City
{
public string Name{get;set;}
public int Population{get;set;}
public State TheState{get;set;}
}
14.1.1 可隔离的标准
Contents at a Glance Detailed Contents
14.1.1.1 在 LINQ 中
在 LINQ to SQL 和 Entity Framework 中,过滤通常使用 LINQ 语句执行:
public static List<State> GetStatesSimple(IEnumerable<State> states)
{
IEnumerable<State> qry =
from o in states where o.Area > 4 where o.Perimiter > 30
select o;
return qry.ToList();
}
public static List<State> GetStatesSegregatable(IEnumerable<State> states)
{
IEnumerable<State> qry = from o in states select o;
qry = from o in qry where o.Area > 4 select o;
qry = from o in qry where o.Perimiter > 30 select o;
return qry.ToList();
}
上面的第一个函数显示了一个典型的 LINQ 查询,第二个函数显示了一个已分解的查询,以便可以将标准部分隔离出来。在这两种情况下,查询直到调用 ToList()
时才实际执行。调用 ToList()
时执行的逻辑在这两种情况下是相同的。只有表达该逻辑的方式在两个函数中是不同的。
14.1.1.2 在 NHibernate 中
在 NHibernate 中,可隔离的过滤是通过 NHibernate Criteria
对象执行的。
public static IList<State> GetStatesSegretable(nh.ISession ctx)
{
nh.ICriteria aCriteria = ctx.CreateCriteria<State>();
aCriteria.Add<State>(o=>o.Area > 4);
aCriteria.Add<State>(o=>o.Perimiter > 30);
return aCriteria.List<State>();
}
请注意,Criteria
对象具有与之关联的实体类型,但编译器不知道此类型。如果您基于 City
创建了一个 ICriteria
,编译器不会阻止您调用 Add<State>(..)
。在这方面,LINQ 比 NHibernate 更安全。
14.1.2 遍历关联
Contents at a Glance Detailed Contents
14.1.2.1 在 LINQ 中
在 LINQ 中,如果您尝试根据父对象的属性过滤对象,可能很简单,如下所示:
public static List<City> GetCitiesSegregateable(IEnumerable<City> cities)
{
IEnumerable<City> qry = from o in cities select o;
qry = from o in qry where o.TheState.Area > 4 select o;
qry = from o in qry where o.TheState.Perimiter > 30 select o;
return qry.ToList();
}
请注意,实际的 ORM 提供程序(LINQ to SQL 或 Entity Framework)可能需要您指定一个 join,但至少 LINQ 语法有可能非常清晰。
14.1.2.2 在 NHibernate 中
在 NHibernate 中,根据父对象的属性过滤对象有点棘手,部分原因是 API 语法的限制。下面的三个函数都将编译,但只有第三个会执行。
public static IList<City> X1_GetCitiesSegregateable(nh.ISession ctx)
{
nh.ICriteria aCriteria = ctx.CreateCriteria<City>();
//won't work, Criteria is intrinsically of Cities
aCriteria.Add<State>(o=>o.Area > 4);
aCriteria.Add<State>(o=>o.Perimiter > 30);
return aCriteria.List<City>();
}
public static IList<City> X2_GetCitiesSegregateable(nh.ISession ctx)
{
nh.ICriteria aCriteria = ctx.CreateCriteria<City>();
//won't work, must prepare for traversing relationship
aCriteria.Add<City>(o=>o.TheState.Area > 4);
aCriteria.Add<City>(o=>o.TheState.Perimiter > 30);
return aCriteria.List<City>();
}
public static IList<City> GetCitiesSegregateable(nh.ISession ctx)
{
nh.ICriteria aCriteria = ctx.CreateCriteria<City>();
State oStateAlias = null;
aCriteria.CreateAlias<City>(o=>o.TheState, ()=>oStateAlias);
aCriteria.Add(()=>oStateAlias.Area > 4);
aCriteria.Add(()=>oStateAlias.Perimiter > 30);
return aCriteria.List<City>();
}
第一个函数版本说明了一个由于编译器不知道 aCriteria
背后的类型而容易陷入的陷阱。第二个版本会失败,因为它没有(但必须)明确指出将遍历基于外键的关系。顺便说一句,LINQ to SQL 中也会出现类似的情况。第三个版本正确执行,但是它引入了另一个容易陷入的陷阱。aCriteria.CreateAlias...
行中的 oStateAlias
变量必须与 aCriteria.Add...
行中的 oStateAlias
变量具有相同的名称(但不一定是同一个变量)。编译器不知道这一点,因此当 aCriteria.Add...
行被隔离到它们自己的函数时,很容易出错。
14.1.3 用于设置标准的函数
Contents at a Glance Detailed Contents
14.1.3.1 在 LINQ 中
将标准逻辑隔离(提取)到 LINQ 中的自己的函数需要标准设置函数中要操作的类型适用于该函数所必需的所有情况。任何需要使用标准设置函数的函数都需要将它正在处理的类型转换为可接受的类型。考虑以下:
public interface IStateReferer
{
State TheState{get;}
}
public class CityState : IStateReferer
{
public City TheCity{get;set;}
public State TheState{get;set;}
}
public class StateWrapper : IStateReferer
{
public State TheState{get;set;}
}
static IEnumerable<T> GetWithStateFilter<T>(IEnumerable<T> qry) where T : IStateReferer
{
qry = from o in qry where o.TheState.Area > 4 select o;
qry = from o in qry where o.TheState.Perimiter > 30 select o;
return qry;
}
在上面的代码窗口中,IStateReferer
是表达基于 State
的标准设置函数所需的接口。CityState
实现该接口,并在查询 State
符合某些标准时查询 City
时非常方便。StateWrapper
实现该接口,并在查询符合某些标准的 State
时非常方便。GetWithStateFilter
是基于 IStateReferer
接口的标准设置函数。
14.1.3.2 在 NHibernate 中
将标准逻辑隔离(提取)到 NHibernate 中的自己的函数非常简单。
public static void AddStateFilterBasedOn_oStateAlias(nh.ICriteria aCriteria)
{
State oStateAlias = null;//must match alias setup in caller
aCriteria.Add(()=>oStateAlias.Area > 4);
aCriteria.Add(()=>oStateAlias.Perimiter > 30);
}
尽管它很简单,但仍然很容易出错。如果我们向此函数传递一个未设置 State
别名的 ICriteria
对象,该函数将编译但不会执行。同样,如果我们向此函数传递一个设置了正确类型别名的 ICriteria
对象,但该设置是通过不同的局部变量名实现的,那么该函数将编译但不会执行。
14.1.4 调用标准设置函数
Contents at a Glance Detailed Contents
14.1.4.1 在 LINQ 中
调用 LINQ 的标准设置函数需要将枚举的类型“投影”(转换)为实现 IStateReferer
接口的类型,并在返回实际结果时将转换后的类型转换回来。
public static List<City> GetCities(IEnumerable<City> cities)
{
IEnumerable<CityState> qry = from o in cities select
new CityState{TheCity=o, TheState=o.TheState};
qry = GetWithStateFilter<CityState>(qry);
return qry.Select(o=>o.TheCity).ToList();
}
14.1.4.2 在 NHibernate 中
调用 NHibernate 的标准设置函数需要使用一个具有与标准设置函数中相应局部变量相同类型和名称的局部变量来设置别名。
public static IList<City> GetCities(nh.ISession ctx)
{
nh.ICriteria aCriteria = ctx.CreateCriteria<City>();
//must match alias setup in AddStateFilterBasedOn_oStateAlias
State oStateAlias = null;
aCriteria.CreateAlias<City>(o=>o.TheState, ()=>oStateAlias);
AddStateFilterBasedOn_oStateAlias(aCriteria);
return aCriteria.List<City>();
}
14.1.5 在不遍历关联的情况下调用标准设置函数
Contents at a Glance Detailed Contents
14.1.5.1 在 LINQ 中
在不遍历子到父关联的情况下调用 LINQ 的标准设置函数,基本上与调用遍历子到父关联的标准设置函数相同。唯一的区别是我们正在转换为 StateWrapper
而不是 CityState
,并且从它们转换回来。
public static List<State> GetStates(IEnumerable<State> states)
{
IEnumerable<StateWrapper> qry = from o in states
select new StateWrapper{TheState = o};
qry = GetWithStateFilter<StateWrapper>(qry);
return qry.Select(o=>o.TheState).ToList();
}
14.1.5.2 在 NHibernate 中
使用 NHibernate 调用标准设置函数在不遍历子到父关联的情况下有点棘手。下面的两个函数都将编译,但第一个会产生错误。第二个能正确工作。
public static IList<State> X_GetStates(nh.ISession ctx)
{
nh.ICriteria aCriteria = ctx.CreateCriteria<State>();
//must match alias setup in AddStateFilterBasedOn_oStateAlias
State oStateAlias = null;
aCriteria.CreateAlias<State>(o=>o, ()=>oStateAlias);//won't work
AddStateFilterBasedOn_oStateAlias(aCriteria);
return aCriteria.List<State>();
}
public static IList<State> GetStates(nh.ISession ctx)
{
//must match alias setup in AddStateFilterBasedOn_oStateAlias
State oStateAlias = null;
nh.Criterion.DetachedCriteria aDetCriteria
= nhl.DetachedCriteria<State>.Create(()=>oStateAlias);
nh.ICriteria aCriteria = aDetCriteria.GetExecutableCriteria(ctx);
AddStateFilterBasedOn_oStateAlias(aCriteria);
return aCriteria.List<State>();
}
14.2 Transaction 标准
Contents at a Glance Detailed Contents
在 LEK 中,Transaction
标准在 TransPred
对象中指定。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 043。
TransPred
对象定义了返回一组 Transaction
的标准。抽象基类具有一组 System.Runtime.Serialization.KnownType
属性,这些属性指定了该类型的对象可以拥有的所有具体类型。这使得 WCF 能够反序列化传输这些类型对象的的消息,即使接口只指定了基类。
以下是实际返回符合指定标准的 Transaction
实体列表的函数的签名。
[sm.OperationContract]DTO.Transaction[]
GetTransactions(DTO.TransPred aTransPred, int? iParticipantID);
14.3 分离标准设置的支撑代码
Contents at a Glance Detailed Contents
回想一下,GetTransactions()
将通过单独查询 WashDlt
s、NWAccntDlt
s、Item
s、DltAlloc
s 和 Transaction
(“五次查询”)来实现。对 Transaction
的查询将确保 Transaction
符合标准,而对每个子实体的查询将确保每个子对象的祖先 Transaction
对象符合标准。五个查询流都将首先按 Transaction
日期排序,然后按 Transaction
ID 排序。最后,将使用一个拼接算法以协调的方式遍历这些流,通过组合 Transaction
实体对象及其子实体对象来创建完整初始化的 Transaction
对象。
14.3.1 ..在 LINQ to SQL 和 Entity Framework 中
LINQ to SQL 和 Entity Framework 的标准设置函数(或函数)理想情况下能够处理一种类型,该类型可以在每个查询的 LINQ 语句中创建。这种类型必须提供对底层 Transaction
对象的访问,以便标准设置函数可以施加其限制。对于 LINQ to SQL 和 Entity Framework,该类型是接口 ITransComposit
。五个具体类型(每个类型都针对其中一个查询进行了优化)实现了该接口。
interface ITransComposit
{
dal.Transaction eTransaction {get;}
}
class WashDltJoinedToTrans : ITransComposit
{
public dal.WashDlt eWashDlt {get; set;}
public dal.Transaction eTransaction {get; set;}
}
class NWAccntDltJoinedToTrans : ITransComposit
{
public dal.NWAccntDlt eNWAccntDlt {get; set;}
public dal.Transaction eTransaction {get; set;}
}
class ItemJoinedToTrans : ITransComposit
{
public dal.Item eItem {get; set;}
public dal.Transaction eTransaction {get; set;}
}
class DltAllocJoinedToItemJoinedToTrans : ITransComposit
{
public dal.DltAlloc eDltAlloc {get; set;}
public dal.Item eItem {get; set;}
public dal.Transaction eTransaction {get; set;}
}
class TransactionWrapper : ITransComposit
{
public dal.Transaction eTransaction {get; set;}
}
14.3.2 ..在 NHibernate 中
在 NHibernate 中,不需要支持代码即可使五个查询使用通用的标准设置函数。
14.4 标准设置函数
Contents at a Glance Detailed Contents
14.4.1 ..在 LINQ to SQL 中
这是 LINQ to SQL 的标准设置函数。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 046。
请注意,其签名和结构与之前为示例 State
和 City
实体所示的标准设置函数非常相似。
14.4.2 ..在 Entity Framework 中
这个非常相似的 Entity Framework 函数可以编译。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 047。
但不幸的是,执行会产生以下错误:
"无法将类型“ServiceEF.WashDltJoinedToTrans”强制转换为类型“ServiceEF.ITransComposit”。LINQ to Entities 仅支持将实体数据模型基本类型强制转换。"
太糟糕了。我们被迫回退到为每个查询单独实现非通用版本。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 048。
14.4.3 ..在 NHibernate 中
这是 NHibernate 的标准设置函数。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 049。
请注意,其签名和结构与之前为示例 State
和 City
实体所示的 NHibernate 标准设置函数非常相似。
14.5 用于构建 Transaction 集合的五次查询
Contents at a Glance Detailed Contents
14.5.1 ..在 LINQ to SQL 中
这是 LINQ to SQL 的五次查询。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 050。
请注意,其结构与之前为 LINQ 显示的 GetStates()
和 GetCities()
示例函数相似。但是,有一个区别是,对于 LINQ to SQL 提供程序,4 个子实体 LINQ 语句必须指定获取 Transaction
实体的 join。
14.5.2 ..在 Entity Framework 中
这是 Entity Framework 的五次查询。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 051。
请注意,其结构与之前为 LINQ 显示的 GetStates()
和 GetCities()
示例函数相似。与 LINQ to SQL 版本不同,不需要 join 子句。可以指定它们,但在使用随 .NET 3.5 SP1 附带的 Entity Framework 版本时,这样做会导致生成效率较低的 SQL。
14.5.3 ..在 NHibernate 中
这是 NHibernate 的五次查询。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 052。
请注意,返回子对象的查询结构与之前为 NHibernate 显示的 GetCities()
查询相似。请注意,GetTransactionDAOsForTransactions()
的结构与之前为 NHibernate 显示的 GetStates()
查询非常相似。
14.6 用于构建 Transaction 集合的查询之一生成的 SQL
Contents at a Glance Detailed Contents
在这五个查询中,GetDltAllocDAOsForTransaactions()
最为复杂,因为它必须遍历最多的子到父关联。本节将展示每个 ORM 为此查询生成的 SQL。
14.6.1 ..在 LINQ to SQL 中
LINQ to SQL 生成的 SQL 不能再简单了。
SELECT
[t0].[ID],
[t0].[ItemID],
[t0].[ParticipantID],
[t0].[Amount]
FROM
[dbo].[DltAlloc] AS [t0]
INNER JOIN [dbo].[Item] AS [t1] ON [t0].[ItemID] = [t1].[ID]
INNER JOIN [dbo].[Transaction] AS [t2] ON [t1].[TransactionID] = [t2].[ID]
WHERE
(NOT ([t2].[PostID] IS NOT NULL)) OR ([t2].[PostID] >= @p0)
ORDER BY
[t2].[Instant] DESC,
[t2].[ID] DESC,
[t1].[ID],
[t0].[ParticipantID]
14.6.2 ..在 Entity Framework 中
Entity Framework 生成的 SQL 更复杂。
SELECT
[Project1].[C1] AS [C1],
[Project1].[ID] AS [ID],
[Project1].[Amount] AS [Amount],
[Project1].[ItemID] AS [ItemID],
[Project1].[ParticipantID] AS [ParticipantID]
FROM ( SELECT
[Extent1].[ID] AS [ID],
[Extent1].[ItemID] AS [ItemID],
[Extent1].[ParticipantID] AS [ParticipantID],
[Extent1].[Amount] AS [Amount],
[Extent2].[ID] AS [ID1],
[Extent3].[ID] AS [ID2],
[Extent3].[Instant] AS [Instant],
1 AS [C1]
FROM
[dbo].[DltAlloc] AS [Extent1]
LEFT OUTER JOIN [dbo].[Item]
AS [Extent2] ON [Extent1].[ItemID] = [Extent2].[ID]
LEFT OUTER JOIN [dbo].[Transaction]
AS [Extent3] ON [Extent2].[TransactionID] = [Extent3].[ID]
LEFT OUTER JOIN [dbo].[Post]
AS [Extent4] ON [Extent3].[PostID] = [Extent4].[ID]
WHERE
([Extent4].[ID] IS NULL) OR ([Extent3].[PostID] >= @p__linq__9)
) AS [Project1]
ORDER BY
[Project1].[Instant] DESC,
[Project1].[ID2] DESC,
[Project1].[ID1] ASC,
[Project1].[ParticipantID] ASC
我不确定为什么 Entity Framework 使用左外连接,而内连接也可以。当在包含几千条记录的数据库上执行时,此查询比 LINQ to SQL 版本慢得多(是的,我确实使用这个系统)。如果 LINQ 语句像 LINQ to SQL 版本一样包含 join,那么生成的 SQL 会更复杂,速度也会更慢。我怀疑生成的 SQL 的复杂性与尝试创建适用于各种数据库服务器的 SQL 有关。或者,复杂查询可能更好地处理某些错误条件。我真的说不准。我希望随 .NET 4.0 发布的 Entity Framework 版本能够为此类查询生成更简单、更快的 SQL。
14.6.3 ..在 NHibernate 中
NHibernate 生成的 SQL 与 LINQ to SQL 生成的 SQL 几乎一样简单。
SELECT
this_.ID as ID1_2_,
this_.ItemID as ItemID1_2_,
this_.ParticipantID as Particip3_1_2_,
this_.Amount as Amount1_2_,
oitemalias1_.ID as ID7_0_,
oitemalias1_.TransactionID as Transact2_7_0_,
oitemalias1_.DltAllocAccntID as DltAlloc3_7_0_,
oitemalias1_.Descrip as Descrip7_0_,
otransacti2_.ID as ID8_1_,
otransacti2_.timestamp as timestamp8_1_,
otransacti2_.ParticipantID as Particip3_8_1_,
otransacti2_.PostID as PostID8_1_,
otransacti2_.Descrip as Descrip8_1_,
otransacti2_.Instant as Instant8_1_
FROM
DltAlloc this_
inner join Item oitemalias1_ on this_.ItemID=oitemalias1_.ID
inner join [Transaction] otransacti2_ on oitemalias1_.TransactionID=otransacti2_.ID
WHERE
(otransacti2_.PostID is null or otransacti2_.PostID >= @p0)
ORDER BY
otransacti2_.Instant desc,
otransacti2_.ID desc,
this_.ItemID asc,
this_.ParticipantID asc
然而,它不必要地包含了 Item
和 Transaction
实体的字段,而这些字段在 NHibernate Criteria 查询中并未被请求。
15 使用 ORM 框架进行复杂数据写入操作
Contents at a Glance Detailed Contents
要保存新的 Transaction
或修改现有的 Transaction
,LEK 客户端会调用在 ServiceInterface.IService
接口上声明的 SaveTransaction()
函数。
[sm.OperationContract]
[sm.FaultContract(typeof(DTO.ConcurrencyFault))]
int SaveTransaction(DTO.Transaction aTransaction, bool bAssumeInBalance);
此函数由三个 Service 类(每个 ORM 一个)实现。对于每种 ORM,该函数都处理三种不同的实体类型:
DTO.
Transaction
:用于在层之间传输数据。DominUtil.
Transaction
:用于强制执行Transaction
的业务规则。Dal*.
Transaction
(ORM 类):用于将Transaction
数据打包发送到数据库,或从数据库读取Transaction
数据。
DomainUtil
.Transaction
位于客户端和服务器都可以访问的命名空间中,用于在客户端和服务器上验证 Transaction
数据。客户端从 UI 收集的数据创建 DomainUtil
.Transaction
对象。然后客户端将其转换为 DTO
.Transaction
,然后再发送到服务器。DTO
.Transaction
对象针对数据传输进行了优化。它们不包含任何逻辑,只包含描述 Transaction
到服务器的基本数据(包含账户 ID 但不包含账户名称)。一旦服务器收到 DTO
.Transaction
,它会将其转换回 DomainUtil
.Transaction
,可能重新验证数据,然后将其转换为 Dal*.Transaction
(ORM 实体对象)以保存到数据库。
将新的 Transaction
保存到数据库,或修改带有已修改子记录的现有 Transaction
,需要的不仅仅是一个 ORM 实体对象。还必须保存 NWAccntDlt
s、WashDlt
s、Item
s 和 DltAlloc
s。如果 Transaction
是新的,这很简单:所有子实体都需要添加。如果 Transaction
是对数据库中已存在的 Transaction
的编辑,那么保存可能变得复杂得多。在这种情况下,子实体可能是新的、已修改的或未更改的。同样,在这种情况下,Transaction
实体需要指示曾经是它的一部分但现在已不再是(已删除实体)的子实体。
每种 ORM 的 Transaction
ORM 类都包含子实体对象的成员。这些子对象与 ORM 上下文协调,跟踪它们是新的、已修改的、未更改的还是已删除的。保存包含任何这些更改状态的子实体集合的 ORM Transaction
对象很容易,前提是所有添加、编辑和删除都发生在现有 ORM 上下文的生命周期内,并且该 ORM 上下文用于在进行更改之前检索数据。在这种情况下,您只需告诉 ORM 上下文保存实体,它就会处理所有细节。然而,上下文生命周期应该很短,并且在 n 层应用程序中,实体对象通常在客户端获取更改,而客户端可能在任何时间段内(等待用户)。此外,当我们在公共接口上不特别喜欢 ORM 类公开的接口时,我们可能不希望客户端甚至拥有 ORM 实体对象。
在 LEK 中,客户端处理专门为客户端设计的实体对象——不依赖于 ORM(类位于 DomainUtil
命名空间)。此外,当它将数据发送到服务器时,它将其作为专门为传输数据设计的对象发送(类位于 DTO
命名空间)。为了让服务器正确处理实体数据,它必须在服务器上模拟客户端上发生的情况。一种方法是让服务器从数据库检索 Transaction
数据的全部新副本(作为 ORM 实体),然后“重放”客户端上发生的事情。当然,要做到这一点,服务器必须接收包含状态跟踪信息(什么新、什么已更改、什么已删除)的数据传输对象。此外,还需要使用时间戳或行版本,以便服务器能够判断数据库数据自快照形成并发送到客户端以来是否已更改。有了所有这些信息,服务器将遍历从数据库中最新获取的 ORM 实体和从客户端接收的实体对象图。它将以协调的方式遍历这两个对象图,根据从客户端接收的实体更新 ORM 实体。然后,它会将修改后的 Transaction
ORM 实体(带有子实体)提交给用于检索它们的 ORM 上下文,该上下文将正确处理所有添加、更新和删除。
未采用此方法的主要原因是 LEK 不希望服务器必须查询每个对象来“重放”更改。这会消耗时间和服务器资源,如果我能获得同样功能且更有效率的东西(我认为我做到了),那将是更可取的。
那么,如果我们已经拥有客户端提供的所有必要数据(包括新的和已修改的),为什么还需要从数据库中查询呢?简短的回答是,您不需要,或者至少不应该。在每种 ORM 中,从创建的对象而不是最近查询的对象更新数据库都需要将创建的对象“附加”到上下文。这在 NHibernate 中很简单,在 LINQ to SQL 中有点困难(我认为每个子对象都必须单独附加),但在随 .NET 3.5 SP1 提供的 Entity Framework 版本中几乎不可能。在 Entity Framework 中这样做的问题与实体被认为有效的状态转换有关。将子实体从“未更改”(附加时获得的状态)更改为“已添加”是不允许的。(据称 .NET 4.0 中的 Entity Framework 不会有这个问题。)
一些 ORM 在附加方面存在的问题主要是附加已修改的父实体的新的和已修改的子实体混合体。在任何 ORM 中,添加一个图中所有内容都是新的对象图都没有问题。在 NHibernate 和 LINQ to SQL 中,附加一个根实体已修改且所有子对象都是新的对象图很容易,但我无法在 Entity Framework 中使其工作。(接受建议!)
15.1 策略
Contents at a Glance Detailed Contents
对于新的 Transaction
,LEK 仅创建完整的 ORM 实体对象(包含所有子实体集合),然后将其交给 ORM 进行插入。
我在 LEK 中为更新已修改的 Transaction
所决定的策略是:
- 检查
Transaction
的行版本,以确保自快照发送到客户端以来它未发生更改。 - 使用(高效的)存储过程删除所有子实体。
- 在 LINQ to SQL 和 NHibernate 中创建并附加一个
Transaction
实体。在 Entity Framework 中查询然后更新一个Transaction
实体。 - 将所有子实体添加为新对象。
- 将整个生成的对象图交给 ORM 以保存到数据库。
如果大多数修改的 Transaction
只有少量 Item
s、DltAlloc
s、NWAccntDlt
s 和 WashDlt
s,那么此策略将很好地工作。如果一个 Transaction
有数十个子实体,而 Transaction
的更新只是更改其中之一,那么这将导致大量额外的处理。然而,代码简化的回报是巨大的。我认为这是一种合理的方法,前提是大多数 Transaction
在添加后不需要编辑。如果不是这种情况,那么专门为更新创建一个新过程可能会更值得。此过程将首先遍历数据传输实体对象图,从底部(子项)到顶部(父项),删除已删除的对象,然后从顶部向下添加新对象并更新已修改的对象。在每次删除、添加和更新时,将从数据传输对象创建一个 ORM 实体对象,然后作为删除、添加或更新传递给 ORM 上下文。更新和删除将首先需要将对象附加到 ORM 上下文。
15.2 关于本节代码的呈现
Contents at a Glance Detailed Contents
如果您阅读了上一节,那么您应该对 LEK 如何处理保存新的和已更新的 Transaction
有了相当清楚的了解。希望这意味着我可以从下往上呈现保存和更新 Transaction
的代码,而不是从上往下。我认为从上往下的方法更典型:首先描述顶级函数,然后第二次描述它调用的函数,然后第三次描述顶级函数调用的低级函数。这种方法一直让我很困扰,因为在知道某个东西是什么之前,我讨厌看到它被使用。接下来的部分将描述 LEK 中用于保存和更新 Transaction
实体及其子实体的服务器端代码。将首先描述最低级别的函数,然后描述调用这些函数的函数,然后描述调用这些函数的函数,依此类推,直到我们到达 SaveTransaction()
函数本身。我没有显示所有依赖代码。您需要下载源代码才能看到 ORM 类(Dal*
命名空间)、数据传输类(DTO
命名空间)和业务对象类(DomainUtil
命名空间)。
15.3 SaveTransaction 的函数依赖树
Contents at a Glance Detailed Contents
这是将要描述的所有函数的依赖树:(请注意,“底部”——即所有其他函数依赖的代码——实际上在顶部。)
15.4 存储过程
Contents at a Glance Detailed Contents
除了典型的 ORM 操作外,LEK 在更新 Transaction
时还使用两个存储过程。两者都与处理并发的方式有关。LEK 不为构成 Transaction
的每个实体使用时间戳或行版本,而是仅在 Transaction
实体上使用时间戳。任何时候对 Transaction
进行任何更改,即使 Transaction
实体本身未更改,只有一个子实体被修改(例如 DltAlloc
),LEK 也会更新数据库中的 Transaction
实体。在 Transaction
表中设置了一个时间戳字段,因此数据库将更新此时间戳字段。Transaction
时间戳字段将作为整个 Transaction
的版本的占位符。
15.4.1 GetTransactionTimestamp
Contents at a Glance Detailed Contents
LEK 在不先检索要更新的 Transaction
对象的情况下更新 Transaction
。但是,它至少需要检索正在更新的 Transaction
的时间戳字段。这是通过调用 GetTransactionTimestamp
存储过程来实现的。
CREATE PROCEDURE [dbo].[GetTransactionTimestamp]
@ID int,
@Timestamp timestamp OUTPUT
AS
SET @Timestamp = (
SELECT timestamp FROM dbo.[Transaction]
WHERE ID = @ID
)
15.4.2 DeleteTransactionChildRecords
Contents at a Glance Detailed Contents
使用 ORM 删除子对象的典型方法可能是查询 ORM 以获取要删除对象的完整对象图,从图中删除子对象,然后将其提交回 ORM 上下文。然后,ORM 将删除图表中已删除的相应 ORM 实体的每个数据库实体,前提是定义的并发相关字段不表明 ORM 具有过时的对象版本。然而,在 LEK 中,由于 Transaction
实体中的时间戳字段将用于跟踪整个 Transaction
的版本,因此无需对每个子实体执行并发检查。我们可以改为一次性删除它们——前提是我们不介意重新创建那些本来不必删除的。
DeleteTransactionChildRecords
存储过程完成了这项工作。
CREATE PROCEDURE [dbo].[DeleteTransactionChildRecords]
@TransactionID int
AS
BEGIN
-- SET NOCOUNT ON;
DELETE DltAlloc
FROM DltAlloc INNER JOIN Item ON DltAlloc.ItemID = Item.ID
WHERE Item.TransactionID = @TransactionID
DELETE
FROM WashDlt
WHERE WashDlt.TransactionID = @TransactionID
DELETE
FROM NWAccntDlt
WHERE NWAccntDlt.TransactionID = @TransactionID
DELETE
FROM Item
WHERE Item.TransactionID = @TransactionID
END
15.5 每种 ORM 特有的支持函数
在某种意义上,本节是关于弥补 ORM 缺陷的代码。稍后将介绍的顶级函数对于每种 ORM 都非常相似。它们之所以能如此,是因为大部分差异都已在本文描述的函数中得到解决。
15.5.1 LINQ to SQL
Contents at a Glance Detailed Contents
LINQ to SQL 是 Microsoft 对生产级 ORM 的首次尝试。他们没有深入研究任何硬问题,也没有尝试实现可以在所有数据库上运行的功能。但是,对于做简单的事情,它非常容易使用。在更新数据库方面,其他 ORM 的方式并不比 LINQ to SQL 简单,所以本节没有 LINQ to SQL 的代码可以展示。
15.5.2 Entity Framework
Contents at a Glance Detailed Contents
Entity Framework 比 LINQ to SQL 功能更强大,但对于简单的事情,它更难使用。随 .NET 4.0 发布的 Entity Framework 版本据说将使 Entity Framework 像 LINQ to SQL 一样易于使用,同时保持其所有功能。
15.5.2.1 CreateEntityKey
Contents at a Glance Detailed Contents
在 Entity Framework 中,一个键可能比一个简单的整数复杂得多。它可以是不同类型的基元集合。CreateEntityKey()
函数用于创建 LEK 中唯一使用的键类型:基于单个整数的非复合键。
public static s.Data.EntityKey CreateEntityKey(int iID,
string strQualifiedEntitySetName)
{
IEnumerable<KeyValuePair<string, object>> entityKeyValues =
new KeyValuePair<string, object>[] {
new KeyValuePair<string, object>("ID", iID) };
return new s.Data.EntityKey(strQualifiedEntitySetName, entityKeyValues);
}
15.5.2.2 GetFK
Contents at a Glance Detailed Contents
Entity Framework 没有实体的简单公共成员来表示外键。就 Entity Framework 而言,基于整数的外键是“关系数据库思维”,而不是真正的“对象思维”。值得庆幸的是,微软的人们已经重新考虑了这一点,.NET 4.0 版的 Entity Framework 将提供简单的外键。在此之前,我们必须这样做才能从 Entity Framework 实体中获取基于整数的外键。
public static int? GetFK<T>(
/*this */s.Data.Objects.DataClasses.EntityReference<T> refr)
where T : class, s.Data.Objects.DataClasses.IEntityWithRelationships
{
if(refr == null)
throw new s.Exception("ServiceEF:21");
if(refr.EntityKey == null)
{
if(refr.Value!=null)
throw new s.Exception("ServiceEF:27");
return null;
}
// can be null if not yet loaded even if FK is not null
// if(refr.Value == null)
// throw new s.Exception("ServiceEF:31");
if(refr.EntityKey.EntityKeyValues ==null)
throw new s.Exception("ServiceEF:33");
if(refr.EntityKey.EntityKeyValues.Length !=1)
throw new s.Exception("ServiceEF:35");
return (int)refr.EntityKey.EntityKeyValues.First().Value;
}
15.5.2.3 EnsureFK
Contents at a Glance Detailed Contents
如果获取基于整数的外键很困难,那么您就知道设置它也很困难。我迫不及待地想等到 .NET 4.0 版本。在此之前,以下似乎有效。
public static void EnsureFK<T>(
/*this */s.Data.Objects.DataClasses.EntityReference<T> refr,
s.Data.Objects.ObjectContext ctx, int? iID)
where T: class, s.Data.Objects.DataClasses.IEntityWithRelationships
{
int? iCurrentID = Helper.GetFK(refr);
if(iCurrentID == iID)
return;
if(iCurrentID != null)
refr.Value = null;
else
{
if(refr.Value != null)
throw new s.Exception();
}
if(iID != null)
refr.EntityKey = new System.Data.EntityKey(ctx.GetType().Name
+ "." + typeof(T).Name, /*typeof(T).Name +*/ "ID", iID.Value);
}
15.5.2.4 SetAllPropsAsModified
Contents at a Glance Detailed Contents
在 LINQ to SQL 中,用于将新创建的 ORM 实体附加到上下文的函数有一个参数,用于指示对象是否应被视为已修改。NHibernate 提供 Update()
函数来执行相同的操作。使用 Entity Framework,我们必须指示每个字段包含已更新的数据。如果实体对象有 30 个字段,而只有一个将被更新,那么这很有用。但是,对于我们希望更新所有字段的简单情况,Entity Framework 的方式比它需要的要复杂。SetAllPropsAsModified()
提供了简单的典型操作,其中我们只是希望 Entity Framework 认为几乎所有字段都包含已修改的数据。
public static void SetAllPropsAsModified(dal.LekEntities ctx, object entity)
{
s.Data.Objects.ObjectStateEntry aObjectStateEntry
= ctx.ObjectStateManager.GetObjectStateEntry(entity);
s.Collections.ObjectModel.ReadOnlyCollection<s.Data.Common.FieldMetadata>
collFieldMetadatas = aObjectStateEntry.CurrentValues.DataRecordInfo.FieldMetadata;
foreach(var propertyName in collFieldMetadatas.Select(o => o.FieldType.Name))
{
if(propertyName == "ID")
continue;
if(propertyName == "timestamp")
continue;
aObjectStateEntry.SetModifiedProperty(propertyName);
}
}
虽然采用了此函数用于 SaveTransaction()
的策略但并未奏效,因此它实际上并未被使用。请参阅下面 LLSaveTransaction
函数的 Entity Framework 版本,其中对此函数的调用被注释掉了。
15.5.3 NHibernate
NHibernate 比 Entity Framework 更强大,并且几乎像 LINQ to SQL 一样易于使用。有两个支持函数是 NHibernate 特有的:EnsureFKWithInteger()
和 EnsureFKWithEntity()
。
15.5.3.1 EnsureFKWithInteger
Contents at a Glance Detailed Contents
与 Entity Framework 一样,NHibernate 通常不使用外键直接公开的 ORM 类。然而,设置外键比在 Entity Framework 中容易得多。下面是执行此操作的函数。
public static void EnsureFKWithInteger<T>(
s.Action<T> delSet, s.Action<T,int> delSetID, s.Action<T,byte[]>
delSetRowVersion, int? iID)
where T: new()
{
//Example of what this function does:
// if(!iID.HasValue)
// return;
// oDal.TheParticipant = new DalNH.Participant();
// oDal.TheParticipant.ID = iID.Value;
//if ID is set, then this must be as well, even though it won't be used
// oDal.TheParticipant.RowVersion = new byte[]{0};
if(!iID.HasValue)
return;
T parent = new T();
delSet(parent);
delSetID(parent, iID.Value);
delSetRowVersion(parent, new byte[]{0});
}
15.5.3.2 EnsureFKWithEntity
Contents at a Glance Detailed Contents
在 Entity Framework 和 LINQ to SQL 中,设置一个映射为父实体子实体的新实体,只需要将新子实体添加到父实体中的子集合中。不必在子实体中设置指向父对象的外键。而在 NHibernate 中,却需要这样做。不过,该函数极其简单。我只将其创建为一个函数,以便更容易地与 EnsureFKWithInteger()
进行对比。以下是 EnsureFKWithEntity()
。
public static void EnsureFKWithEntity<T>(s.Action<T> delSet, T parent)
{
//Example of what this function does:
// oDal.TheParticipant = parent;
delSet(parent);
}
15.6 SaveTransaction 和每种 ORM 的支持函数实现
Contents at a Glance Detailed Contents
本节中显示的所有函数至少有一个 ORM 具有独特实现。每个函数的实现显示在连续的代码窗口中,并带有行号。行号不是源代码中的实际行号,而是每个代码窗口的范围内的行号(它们在每个代码窗口中重新开始)。每个函数的行垂直间距,使得在多个版本中执行相同操作的行在每个代码窗口中具有相同的行号。这样您就可以逐行比较函数的不同版本。如果函数的代码窗口显示滚动条,您可能希望滚动每个代码窗口,使其显示相同的起始行。
15.6.1 CreateORMRowVersion
Contents at a Glance Detailed Contents
LINQ to SQL 使用 System.Data.Linq.Binary
类型来表示 SQL Server 时间戳字段。Entity Framework 和 NHibernate 使用 Byte[]
(字节数组)。LEK 使用自定义创建的类型 GlobalType
.RowVersion
来在客户端和客户端-服务器接口之间传输时间戳字段数据。CreateORMRowVersion()
函数从 GlobalType
.RowVersion
类型创建 ORM 特定的类型。
15.6.1.1 LINQ to SQL
请参阅 periodnet.blogspot.com 上本文的 代码窗口 065。
15.6.1.2 Entity Framework 和 NHibernate
请参阅 periodnet.blogspot.com 上本文的 代码窗口 066。
15.6.2 存储过程:GetTransactionTimestamp 和 DeleteTransactionChildRecords
Contents at a Glance Detailed Contents
GetTransactionTimestamp()
和 DeleteTransactionChildRecords()
都是对同名存储过程的简单封装。(请参阅上面的存储过程部分,以了解这两个函数的需求。)
15.6.2.1 LINQ to SQL
生成 LINQ to SQL 默认 ORM 类集的向导也可以为存储过程创建包装器。下面的两个函数实际上是包装器的包装器。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 067。
15.6.2.2 Entity Framework
为 Entity Framework 手动创建了存储过程的包装函数。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 068A。
请注意,获取用于创建命令对象的连接对象需要一些“挖掘”(第 4、5、27、28 行)。
15.6.2.3 NHibernate
也为 NHibernate 手动创建了存储过程的包装函数,它几乎与 Entity Framework 的相同。
请参阅 periodnet.blogspot.com 上本文的 代码窗口 068B。
请注意,在第 6 行创建命令对象比在 Entity Framework 中更容易。还请注意,与 Entity Framework 不同,可以将命令显式包含到事务中(第 7 行和第 29 行)。
15.6.3 CreateTransactionReadyToAttach
Contents at a Glance Detailed Contents
要将更改提交到您未通过现有的 ORM 上下文查询的数据库实体,您必须将其“附加”到 ORM 上下文。如果您从头开始创建 ORM 实体(未查询过),则必须确保在附加之前正确初始化字段。CreateTransactionReadyToAttach()
函数执行此操作。请注意,LINQ to SQL 和 NHibernate 的函数版本除了 ORM 类型外是相同的。Entity Framework 的版本需要使用先前描述的 CreateEntityKey()
辅助函数来正确设置主键(第 5 行)。
15.6.3.1 LINQ to SQL
请参阅 periodnet.blogspot.com 上本文的 代码窗口 069。
15.6.3.2 Entity Framework
请参阅 periodnet.blogspot.com 上本文的 代码窗口 070。
15.6.3.3 NHibernate
请参阅 periodnet.blogspot.com 上本文的 代码窗口 071。
15.6.4 TransactionRowVersionMatches
Contents at a Glance Detailed Contents
TransactionRowVersionMatches()
函数检索数据库中时间戳字段的数据,并将其与先前从数据库检索到的提供值进行比较。每个 ORM 的代码几乎相同。
15.6.4.1 LINQ to SQL
请参阅 periodnet.blogspot.com 上本文的 代码窗口 072。
15.6.4.2 Entity Framework
请参阅 periodnet.blogspot.com 上本文的 代码窗口 073。
15.6.4.3 NHibernate
请参阅 periodnet.blogspot.com 上本文的 代码窗口 074。
15.6.5 SetDALObjectFromDomainObject
Contents at a Glance Detailed Contents
SetDALObjectFromDomainObject()
从业务对象 Transaction
实体(DomainUtil
命名空间)设置 ORM Transaction
实体的所有字段(包括子实体集合)。
15.6.5.1 LINQ to SQL
请参阅 periodnet.blogspot.com 上本文的 代码窗口 075。
请注意,在第 5、10、20、30 和 42 行,通过简单地设置映射的外键整数字段来设置外键。
15.6.5.2 Entity Framework
请参阅 periodnet.blogspot.com 上本文的 代码窗口 076。
请注意,在第 5、10、20、30 和 42 行,设置外键需要使用前面讨论过的自定义 EnsureFK()
函数。
15.6.5.3 NHibernate
请参阅 periodnet.blogspot.com 上本文的 代码窗口 077。
请注意,在第 5、10、20、30 和 42 行,设置外键需要使用前面讨论过的自定义 EnsureFKWithInteger()
函数。还请注意,与另外两个 ORM 不同,子实体中指向父实体的外键(这些父实体将子实体视为子实体)也必须设置。这在第 11、21、31 和 43 行通过调用 EnsureFKWithEntity()
来实现,它除了将实体成员设置为父实体之外,什么也不做。
15.6.6 LLSaveTransaction
Contents at a Glance Detailed Contents
LLSaveTransaction()
(“低级别保存事务”)首先执行一个可选检查,以确认提供的业务对象 Transaction
实体是否有效。然后,如果 Transaction
是对现有 Transaction
的修改,则进入一个块;如果是新的 Transaction
,则进入另一个块。
对于修改过的 Transaction
,该块(第 9-40 行)将首先检查数据库中的记录自客户端快照创建以来是否已被更新。如果此检查失败,则抛出异常。接下来,数据库中的所有子记录将被删除(第 13 行)。最后,将创建或查询 ORM Transaction
实体,然后用来自传入的业务对象 Transaction
实体的数据进行设置(第 14-40 行)。即使唯一的更改是子实体对象中的更改,Transaction
实体仍被设置为包含修改过的数据。这将确保 Transaction
记录中的时间戳字段可以用作整个 Transaction
的版本。
对于新的 Transaction
,没有要检查并发性的数据库数据,也没有要删除的子实体。Transaction
实体只需创建和设置,然后 ORM 上下文会收到新实体的通知(第 43-45 行)。
版本之间最显著的区别在于如何设置修改过的 Transaction
以及如何将其“附加”到上下文。在 LINQ to SQL 版本中,ORM 实体的字段在将其附加到上下文后进行设置。在 NHibernate 版本中,字段在附加之前进行设置。我无法让 Entity Framework 版本的函数在这两种顺序下都能正常工作,而是回退到查询对象来正确设置它。
15.6.6.1 LINQ to SQL(函数)
请参阅 periodnet.blogspot.com 上本文的 代码窗口 078。
15.6.6.2 Entity Framework(函数)
请参阅 periodnet.blogspot.com 上本文的 代码窗口 079。
15.6.6.3 NHibernate(函数)
请参阅 periodnet.blogspot.com 上本文的 代码窗口 080。
15.6.6.4 LINQ to SQL(描述)
为了设置上下文以保存修改过的 Transaction
,LINQ to SQL 函数版本首先创建 ORM 实体(第 32 行),然后将其附加到上下文(第 33 行),最后设置 ORM 实体中的所有字段(第 34 行)。(请注意,顺序与 NHibernate 不同。)实体创建和字段设置通过前面讨论的辅助函数进行。实体附加到上下文是通过调用向导生成的“Attach”函数来实现的。
为了设置上下文以保存新的 Transaction
,LINQ to SQL 函数版本首先创建一个新的 ORM 实体(第 43 行),然后设置 ORM 实体中的所有字段(第 44 行),最后通知上下文关于新实体(第 45 行)。字段设置通过前面讨论的辅助函数进行。通知上下文关于新对象是通过调用向导生成的 InsertOnSubmit()
函数来实现的。除了用于通知上下文关于新对象的函数名称外,该块与 Entity Framework 和 NHibernate 版本相同。
15.6.6.5 Entity Framework(描述)
为了设置上下文以保存修改过的 Transaction
,Entity Framework 函数版本首先查询对象(第 38 行),然后通过前面讨论的辅助函数设置返回对象中的所有字段(第 39 行)。简单,但不高效。我更希望不查询对象,而是创建一个“自制”对象并附加它。出于各种原因(请参阅第 16-22 行和 29-31 行的注释),我无法让它工作。(有什么想法?)这在 Entity Framework 的 .NET 4.0 版本中可能会更容易。
为了设置上下文以保存新的 Transaction
,Entity Framework 函数版本首先创建一个新的 ORM 实体(第 43 行),然后设置 ORM 实体中的所有字段(第 44 行),最后通知上下文关于新实体(第 45 行)。字段设置通过前面讨论的辅助函数进行。通知上下文关于新对象是通过调用上下文的(向导生成的)AddToTransaction()
函数来实现的。除了用于通知上下文关于新对象的函数名称外,该块与 LINQ to SQL 和 NHibernate 版本相同。
15.6.6.6 NHibernate(描述)
为了设置上下文以保存修改过的 Transaction
,NHibernate 函数版本首先创建 ORM 实体(第 23 行),然后设置 ORM 实体中的所有字段(第 24 行),最后将其附加到上下文(第 25 行)。(请注意,顺序与 LINQ to SQL 不同。)实体创建和字段设置通过前面讨论的辅助函数进行。实体附加到上下文是通过调用上下文的“Update”函数来实现的。
为了设置上下文以保存新的 Transaction
,NHibernate 函数版本首先创建一个新的 ORM 实体(第 43 行),然后设置 ORM 实体中的所有字段(第 44 行),最后通知上下文关于新实体(第 45 行)。字段设置通过前面讨论的辅助函数进行。通知上下文关于新对象是通过调用上下文的 Save()
函数来实现的。除了用于通知上下文关于新对象的函数名称外,该块与 LINQ to SQL 和 Entity Framework 版本相同。
15.6.7 SaveTransaction
Contents at a Glance Detailed Contents
最后,我们来到保存新事务或已更新事务的顶级函数。SaveTransaction()
将较低级别的函数(LLSaveTransaction()
)封装在 try-catch 块中,以便任何 ORM 特定的并发异常都可以被替换为为与 WCF 兼容而创建的自定义异常。try-catch 块又被封装在一个 using
语句中,该语句建立一个事务(在常规意义上),如果发生任何问题,该事务将被回滚。事务作用域块又被封装在一个 using
语句中,该语句创建 ORM 上下文。这三个版本几乎相同,只是 Entity Framework 版本显式打开了与数据库的连接(第 11-13 行)。
15.6.7.1 LINQ to SQL
请参阅 periodnet.blogspot.com 上本文的 代码窗口 081。
15.6.7.2 Entity Framework
请参阅 periodnet.blogspot.com 上本文的 代码窗口 082。
15.6.7.3 NHibernate
请参阅 periodnet.blogspot.com 上本文的 代码窗口 083。
15.6.7.4 对 SaveTransaction 三个版本的对比
上下文的创建(第 5 行)通过构造 DataClassesDataContext
(对于 LINQ to SQL)、通过构造(向导生成的)LekEntitites
(对于 Entity Framework)以及通过调用 OpenSession()
(对于 NHibernate)来完成。
与 LINQ to SQL 和 NHibernate 的版本相比,Entity Framework 的 SaveTransaction()
版本显式打开了数据库连接(第 13 行)。如果没有任何存储过程调用,这是不必要的。LLSaveTransactions()
执行的第一个数据库操作是调用 GetTransactionTimestamp()
存储过程。对于 LINQ to SQL,存储过程的执行是通过向导生成的包装函数进行的,该函数与其他 ORM 相关任务一样,让我们不必担心打开数据库连接。对于 NHibernate,我们确实需要编写大部分代码来调用存储过程,但在调用存储过程时,数据库连接(从中获取存储过程命令)已经打开(我不确定 NHibernate 何时打开它)。对于 Entity Framework,存储过程的执行也通过显式设置存储过程调用的代码进行。然而,与 NHibernate 不同的是,从上下文中获取的连接不一定已打开。我猜测 NHibernate 在创建会话时打开连接,而 Entity Framework 只在发生需要它的 ORM 操作时才打开它。由于在调用存储过程之前没有执行 ORM 操作,除非我们的代码显式打开它,否则连接不会打开。
实际写入数据库(第 16 行)由 LINQ to SQL 上下文的 SubmitChanges()
函数、Entity Framework 上下文的 SaveChanges()
函数以及 NHibernate 上下文的 Flush()
函数触发。
如果发生并发问题(第 15 行),LINQ to SQL 会抛出 System.Data.Linq.ChangeConflictExcepion
,Entity Framework 会抛出 System.Data.OptimisticConcurrencyException
,NHibernate 会抛出 NHibernate.StaleObjectStateException
。三个版本中的每个版本都为相应的异常类型提供了捕获(第 20 行),然后将其转换为 WCF 友好的 ConcurrencyFaultException
(第 26 行)。
16 其他有用的视图及其支持函数
Contents at a Glance Detailed Contents
在构成 IService
接口的 33 个函数中,我只讨论了四个:Get2WayParticipants()
、UpdateParticipants()
、GetTransactions()
和 SaveTransactions()
。然而,这四个是应用程序的核心函数,其中的逻辑代表了其他函数中大部分的逻辑。在本节中,我将讨论另外四个不太重要的函数,但它们在 LEK 中扮演着重要角色。我不会展示这些函数的源代码,而只会描述 LEK 使其成为可能的功能。
16.1 账户的净值账户差额
Contents at a Glance Detailed Contents
当我通过银行的网站查看我的实际支票账户时,余额几乎从不与 LEK 显示的余额相符。不可避免地,我会丢失一张收据或忘记自动扣款。因此,将账户与“官方”对账单进行核对至关重要。在 LEK 中,此活动由“账户的净值账户差额”视图促进。此视图显示了特定 NWAccnt
在一段时间内或在记账事件之间发生的所有更改。我通常会(通过网络)从我的银行提取最新的对账单,将其转换为位图,并排显示对账单和同一时期的视图,然后从对账单中划掉有视图中条目的交易。那些没有被标记的交易是我丢失收据的交易,或者是什么,所以我将它们输入 LEK。GetNWAccntDltsOfTransations()
函数提供“账户的净值账户差额”视图所需的数据。
16.2 账户的项目
Contents at a Glance Detailed Contents
要查看特定类型的支出或收入流(如电费账单)的历史记录,LEK 提供了“账户的项目”视图。此视图与“账户的净值账户差额”视图非常相似,您指定一个时间段或边界记账事件,然后得到一个账户更改列表。视图中的每一行实际上对应一个分配给选定 DltAllocAccnt
的 Item
。每一行都有一个用于每个可能 Participant
的单元格,值为非零的单元格对应实际的 DltAlloc
。GetDltAllocsOfTransactions()
函数提供“账户的项目”视图所需的数据。
16.3 摘要
Contents at a Glance Detailed Contents
LEK 提供了“摘要”视图来显示一段时间内所有账户更改的总金额。可以从该视图获得的信息示例包括:某个 Participant
在过去一年中的食品杂货支出金额、某个 Participant
的储蓄账户在过去一年中的总存款金额、某个 Participant
在过去一年中的净值变化、某个 Participant
欠“大锅”(套现)的金额在过去一年中的变化。这些值可以显示在任何时间段(不只是年份)。如果系统中最初的几个 Transaction
确立了所有 NWAccnt
的当前值,并且时间段设置为在第一个 Transaction
之前开始,那么摘要视图将显示每个 Participant
的所有 NWAccnt
的实际值。“摘要”视图显示每个 Participant
和整个家庭的所有金额。GetSummaryOfTransactions()
函数提供“摘要”视图所需的数据。
16.4 项目
Contents at a Glance Detailed Contents
在 Participant
s 和账户之间分配费用通常是相当主观的。通常,在我输入我妻子购买但我不太清楚有什么必要的东西的 Transaction
时,我会以一种她不会同意的方式分配费用。稍后,在准备记账时查看已输入的 Transaction
s,我会醒悟过来,修改分配,以维持家庭和睦。这两个活动,即查看已输入的 Transaction
s 和修改分配,可以从“Transaction
s”和“Transaction
”视图中进行,但从“项目”视图中可以更有效地进行。此视图显示特定时间段或记账事件的所有 Item
s,而不考虑它们所属的 Transaction
s。Item
s 可以按 Participant
s 之间的分配百分比和按账户排序。这样可以很容易地一目了然地看到在一系列 Transaction
s 中谁在支付什么。该视图还提供了一种直接在视图中进行更改的方法,而无需打开单独的 Transaction
s。GetItemsInTransactions()
函数提供“项目”视图所需的数据。ResetItemsInTransactions()
函数保存“项目”视图中所做的更改。
17 下载、设置数据库、构建和运行
Contents at a Glance Detailed Contents
所有源文件均可作为 zip 文件 在此处 获取。
zip 文件包含使用 Visual Studio 2008 构建应用程序所需的所有内容。除了 zip 文件中的文件外,您还需要 Microsoft Visual Studio 2008 和 .NET Framework 3.5 SP1。要运行该应用程序,您还需要 SQL Server 2005 数据库。
以下是下载、构建和运行应用程序的所有步骤
- 下载 zip 文件。
- 将 zip 文件中的所有文件和目录解压缩到硬盘上的一个目录中。我将把这个目录称为“INSTALL”。
- 在 Visual Studio 2008 中打开 INSTALL\Lek.sln。
- 确保 SQL Server 2005 正在运行。
- 打开 SQL Server Management Studio,创建一个名为 Lek(主数据库)的数据库和一个名为 LekDev(测试数据库)的数据库。
- 在 SQL Server Management Studio 中,为每个数据库运行 Solution Items\SchemaAdd.sql 中的 DDL SQL。
- 在 Visual Studio 中,打开 ServiceFactory\LEKSettings.config,将
ProdDBConnectionString
设置的值更改为您刚刚创建的 Lek 数据库的连接字符串,并将DevDBConnectionString
设置的值更改为您刚刚创建的 LekDev 数据库的连接字符串。您可能只需要将“ADIT80”替换为您自己的计算机名即可。 - 构建解决方案。
- 从菜单中选择“测试 | 运行 | 解决方案中的所有测试”,并确保所有 94 个测试都通过。
- 运行“WinApp”项目,或者,如果您想将应用程序作为真正的三层应用程序运行,请运行“WinFormServer”项目。您将看到“选择 ORM 和数据库”对话框。
- 在“选择 ORM 和数据库”对话框中,选中“重置数据”复选框,然后单击“测试数据库”列中的其中一个按钮。
- 您将看到 LEK 对话框。
18 使用代码
Contents at a Glance Detailed Contents
LEK 不会*试图*变得聪明。它不会尝试猜测您接下来要做什么或尝试猜测您想要什么。所有无模式对话框都不会与其他无模式对话框通信。因此,例如,当您保存一个新的 Transaction
时,它不会自动出现在“Transaction
s”窗体中。您需要单击“重新加载”才能看到它。当然,只有当您在“Transaction
s”窗体中的查询设置为使新 Transaction
匹配查询条件时,它才会显示。默认情况下,条件是“未记账”,并且新 Transaction
s 始终是未记账的,因此,默认情况下,单击“重新加载”时它会显示。模态对话框包括用于指定条件的对话框、“事务的净值账户差额”对话框(用于设置 Transaction
)以及“事务项目”对话框(也用于设置 Transaction
)。一旦关闭模态(子)对话框,您输入到这些对话框中的信息将立即反映在启动它们的父对话框中。
19 历史
Contents at a Glance Detailed Contents
- 2009 年 12 月 20 日:首次发布。
- 2009 年 12 月 22 日
- 本页:添加了代码窗口标签。
- 本页:将 zip 文件位置更改为标准位置。
- 本页:修复了代码窗口 002 中的行换行错误。
- 本页:修复了第 13.2 节中的一个小格式错误。
- 本页:为所有图像添加了“alt”标签。
- 下载:改进了 SQL Server 不可用情况下的错误处理。
- 2009 年 12 月 25 日
- 本页:在 periodnet.blogspot.com 上的版本中使代码窗口变宽。
- 本页:对一些语法和拼写进行了几处更正。
- 本页:对一些地方的措辞进行了微调。
- 下载:移除了几个额外的文件。
- 2010 年 1 月 30 日
- 通过添加
[ser.DataContract]
属性,使DTO.Ownership
可在 WCF 上序列化。 - 在 WinFormClient 中添加了 App.Config,其中包含一个注释掉的键值对,用于指定互联网上服务的地址。
- 使 WinFormClient 覆盖 WinFormClient.exe.config 中找到的任何值的服务地址。
- 为
ServiceLinqToSql.Service
提供了一个默认构造函数,该构造函数将从 Web.config 文件(用于 IIS)或从 WinFormServer.exe.config 读取连接字符串。