65.9K
CodeProject 正在变化。 阅读更多。
Home

Three-tier .NET Application Utilizing Three ORM Technologies

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (114投票s)

2009年12月21日

CPOL

109分钟阅读

viewsIcon

175345

downloadIcon

4372

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

  1. 引言
  2. Contents at a Glance
  3. A Few Screenshots
  4. Detailed Contents
  5. 背景
  6. My Ulterior Motive (...well, one of them)
  7. Data Model
  8. Business Rules
  9. Visualization of the Data
  10. 架构
  11. ORM Classes and Their Mapping to Database Entities
  12. How the ORM Classes Fit in the Architecture
  13. Using the ORM Frameworks for Simple Operations
  14. Using the ORM Frameworks for a Complex Query
  15. Using the ORM Frameworks for a Complex Data Write Operation
  16. A Few Other Helpful Views and the Functions that Feed Them
  17. Downloading, Setting up the Database, Building, and Running
  18. Using the Code
  19. 历史

3 A Few Screenshots

Contents at a Glance   Detailed Contents

Here is the form that comes up when you launch LEK

001Launch.gif

Here is the form for creating and querying for accounting transactions

002TransactionsSHRUNK.gif

4 Detailed Contents

  1. 引言
  2. Contents at a Glance
  3. A Few Screenshots
  4. Detailed Contents
  5. 背景
  6. My Ulterior Motive (...well, one of them)
  7. Data Model
  8. Business Rules
  9. Visualization of the Data
  10. 架构
  11. ORM Classes and Their Mapping to Database Entities
  12. How the ORM Classes Fit in the Architecture
  13. Using the ORM Frameworks for Simple Operations
  14. Using the ORM Frameworks for a Complex Query
  15. Using the ORM Frameworks for a Complex Data Write Operation
  16. A Few Other Helpful Views and the Functions that Feed Them
  17. Downloading, Setting up the Database, Building, and Running
  18. Using the Code
  19. 历史

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

  1. Each individual in the family owns a certain percentage of each of the assets owned by the family. 
  2. Each individual in the family is responsible for a certain percentage of each of the liabilities the family is responsible for. 
  3. Each expense incurred by the family can be allocated among the family members. 
  4. 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

004ERD.gif

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 Items. A DltAllocAccnt can have zero, one or more Items. 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 NWAccnts down the left and Participants 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 Participants. Each Item is associated with a Transaction and a DltAllocAccnt. Two Items 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 Items is arbitrary. For example, if the Transaction is "Bought groceries at HEB" then there could be the four Items "gallon milk", "4 ounces cheese", "1 lb pork", "1 lb beef", or there could be the two Items "dairy", "meat", or there could be just a single Item "groceries". If the DltAllocAccnts are fine-grained, this will sometimes require fine-grained Items. 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 Items 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 Items 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 DltAllocs 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 NWAccntDlts: 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 Participants grows. Rather than keeping track of each possible combination of Participants 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 WashDlts have the same Participant and the same Transaction. All the WashDlts 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 NWAccntDlts in a Transaction must equal the sum of the DltAllocs in the Transaction. The sum of the WashDlts 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 DltAllocs in the Transaction for that Participant. This last rule is what ties in the WashDlts. In a Transaction with one or more NWAccntDlts 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 NWAccnts drop by $10. Since Father is to bear the expense of the sardines himself, however, there are also two WashDlts: 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 WashDlts (-$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 NWAccntDlts for this Transaction will appear like this in the UI

005NWAccntDlts.gif

The Items for this Transaction will appear like this

003Items.gif

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

006Transaction.gif

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

007Arch_Base.gif

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"

008Arch_Util.gif

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.

009Arch_WinApp.gif

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

010Arch_ClientServer.gif

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.

011Arch_UIInterface.gif

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). 

012Arch_GlobalType.gif

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.

013Arch_DTO.gif

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.

014Arch_TierUtils.gif

10.10 Isolating the ORM Classes From the Rest of the App

Contents at a Glance   Detailed Contents

An 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.

015Arch_Dal.gif

10.11 Isolating the ORM Dependant Code for Each ORM

Contents at a Glance   Detailed Contents

Rather 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.

016Arch_ORM.gif

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

DependencyMatrix.GIF

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

AddORMClasses_LtS.GIF

And here is the DalLinqToSQL project after the operation was completed

DalLinqToSqlProject.GIF

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

AddORMClasses_EF.GIF

And here is the DalEF project after the operation was completed

DalEFProject.GIF

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

017ERDSQL.gif

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

019ERDLinqToSQLSHRUNK.gif

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

018ERDEFSHRUNK.gif

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
Code window 001

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
Code window 002
<?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
Code window 003

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
Code window 004

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
Code window 005
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
Code window 006
<?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 of Transactions, 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

DalNHlProject.GIF

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

Code window 007

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 WashDlts within a TransactionNWAccntDlts within a Transaction, Items within a Transaction, and DltAllocs 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 Items 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

Code window 008

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

Code window 009

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: 

016Arch_ORM.gif

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 命名空间中的类(参见上图)。客户端本身是由用户运行的托管应用程序创建的(并在 WinFormClientWinApp 命名空间中定义)。如果 LEK 在三层配置中运行,那么 WCF 服务器(WinFormServer 命名空间,中间层)将通过工厂实例化一个 Service 类,并通过 WCF 公开它。每个托管应用程序(WinFormClient 命名空间)将实例化一个到服务器端单例的 WCF 代理,并将该代理传递给其包含的客户端(Presenter 命名空间)。代理也实现了 IService。如果 LEK 在两层配置中运行,那么托管应用程序(WinApp 命名空间)将直接实例化一个 Service 类,并将其传递给其包含的客户端。无论哪种情况,客户端只知道它有一个实现了 IService 的对象。它不知道也不关心该对象是服务器上单例的代理还是其自身进程中单例的代理。它也不知道也不关心该对象使用的是 LINQ to SQL、Entity Framework 还是 NHibernate。

代码窗口 010

请参阅 periodnet.blogspot.com 上本文的 代码窗口 010

13 使用 ORM 框架进行简单操作

Contents at a Glance   Detailed Contents

在 LEK 中,ParticipantNWAccntDltAllocAccnt 实体的操作非常相似。对于每个实体,都有一个简单的窗体,其中包含一个数据网格,显示数据库中的所有实体。对于每个实体,数据网格允许编辑任何 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 使用时间戳字段来实现乐观并发。

代码窗口 011
[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 还具有可以跟踪实体编辑状态的成员。

代码窗口 012

请参阅 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 函数的版本

代码窗口 013
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

代码窗口 014
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 对象集合。

代码窗口 015
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 是直接的。

代码窗口 016
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 稍微复杂一些,但结果相同。

代码窗口 017
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 几乎相同。

代码窗口 018
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() 时),并抛出一个专门设计的异常。

如前所述,ParticipantNWAccntDltAllocAccnt 实体非常相似。这些实体的 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 命名空间中。

代码窗口 019
[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> 对象。

代码窗口 020
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)。

代码窗口 021
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 版本。

代码窗口 022
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 的辅助函数,当它只是一个整数时。

代码窗口 023
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[])。

代码窗口 024
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 最近附加的实体中几乎所有字段都应用于更新关联数据库记录中的相应字段。

代码窗口 025
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 版本非常相似的形式。

代码窗口 026
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 中的相同。

代码窗口 027
public static Byte[] CreateORMRowVersion(gt.RowVersion aRowVersion)
{
    if(aRowVersion==null)
        return null;
    return aRowVersion.GetCopyOfData();
}

与 LINQ to SQL 一样,主键为整数的整数可以直接设置,您无需遍历对象的字段来指示字段包含已更新数据。

代码窗口 028
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
代码窗口 029
-- 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
代码窗口 030
-- 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
代码窗口 031
-- 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 的子实体包括 NWAccntDlts、WashDlts 和 Items。Transaction 的孙子实体是每个(子)ItemDltAllocs。为 LINQ to SQL 和 Entity Framework 创建的默认 ORM 集,以及为 NHibernate 已描述的自定义 ORM,都包含这些子实体成员。然而,在每个 ORM 上发出典型的查询以返回 Transaction 实体集合不会自动返回所有子实体都已初始化的实体集。默认情况下,ORM 提供“延迟加载”,即关联实体在您明确请求之前不会加载。因此,在发出简单的 Transaction 查询后,我们可以遍历 Transaction 实体列表并访问每个子实体以从数据库加载它们。然而,这可能会导致对数据库发出大量查询。如果 Transaction 平均有两个 NWAccntDlts、四个 WashDlts、四个 Items,以及每个 Item 有四个 DltAllocs,并且只返回 10 个 Transaction,那么总共将发出 261 个单独的数据库查询(1 个查询用于 Transaction 列表,20 个查询用于 NWAccntDlts,40 个查询用于 WashDlts,40 个查询用于 Items,以及 160 个查询用于 DltAllocs)。显然,我们需要避免这种情况。

我相信,对于每种 ORM,都可以指定 ORM 类映射,以便框架“急切”加载指定的子实体。很容易想象这在一个简单的父子关系中是如何工作的:ORM 框架将连接涉及的表以子实体的结果集形式返回,并将父字段附加到每个子实体。为了将此结果集转换为实体,框架将遍历结果集,为每个结果创建一个子实体,并为具有唯一父字段集值的每个结果创建一个父实体。您可能会想到,对于我们拥有的比 Transaction 更复杂的多个子实体关系的 SQL 会相当复杂。此外,返回的数据会更加冗余,因为每个子实体可能都需要携带父实体字段。

ORM 可以采用的另一种策略是首先使用简单查询获取顶级对象的列表,然后,对于每个顶级对象,发出对每种类子实体的查询。在上面描述的示例场景中,这将总共产生 41 个查询——好多了,但仍然是大量的数据库命中。

更好的策略是,如果 ORM 为每种实体类型执行一个单独的查询,然后将结果组合成实体。在示例场景中,这将只产生 4 个查询,并且与返回的 Transaction 数量无关(即使 100 个 Transaction 满足条件而不是只有 10 个,仍然只有 4 个查询)。

我不知道这三个 ORM 是否都可以配置为利用我刚才描述的最后一种策略。如果可以,这将是 ORM 的高级用法,而我还没有足够了解来描述它。但是我可以自己轻松地实现这种策略,从而让 ORM 保持其基础的、易于访问的配置。

总结一下,目标是生成一个符合某些标准并且包含所有子数据(descendent data)的 Transaction 实体列表。为了实现这一点,将执行五个单独的查询:

  • 一个查询,用于返回匹配标准的 Transaction
  • 一个查询,用于返回匹配 TransactionNWAccntDlts。
  • 一个查询,用于返回匹配 TransactionWashDlts。
  • 一个查询,用于返回匹配 TransactionItems。
  • 一个查询,用于返回匹配 TransactionItems 的 DltAllocs。

每个查询都将按 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 框架无关。

代码窗口 032
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 语句执行:

代码窗口 033
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 对象执行的。

代码窗口 034
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 中,如果您尝试根据父对象的属性过滤对象,可能很简单,如下所示:

代码窗口 035
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 语法的限制。下面的三个函数都将编译,但只有第三个会执行。

代码窗口 036
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 中的自己的函数需要标准设置函数中要操作的类型适用于该函数所必需的所有情况。任何需要使用标准设置函数的函数都需要将它正在处理的类型转换为可接受的类型。考虑以下:

代码窗口 037
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 中的自己的函数非常简单。

代码窗口 038
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 接口的类型,并在返回实际结果时将转换后的类型转换回来。

代码窗口 039
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 的标准设置函数需要使用一个具有与标准设置函数中相应局部变量相同类型和名称的局部变量来设置别名。

代码窗口 040
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,并且从它们转换回来。

代码窗口 041
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 调用标准设置函数在不遍历子到父关联的情况下有点棘手。下面的两个函数都将编译,但第一个会产生错误。第二个能正确工作。

代码窗口 042
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 对象中指定。

代码窗口 043

请参阅 periodnet.blogspot.com 上本文的 代码窗口 043

TransPred 对象定义了返回一组 Transaction 的标准。抽象基类具有一组 System.Runtime.Serialization.KnownType 属性,这些属性指定了该类型的对象可以拥有的所有具体类型。这使得 WCF 能够反序列化传输这些类型对象的的消息,即使接口只指定了基类。

以下是实际返回符合指定标准的 Transaction 实体列表的函数的签名。

代码窗口 044
[sm.OperationContract]DTO.Transaction[]    
GetTransactions(DTO.TransPred aTransPred, int? iParticipantID);

14.3 分离标准设置的支撑代码

Contents at a Glance   Detailed Contents

回想一下,GetTransactions() 将通过单独查询 WashDlts、NWAccntDlts、Items、DltAllocs 和 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。五个具体类型(每个类型都针对其中一个查询进行了优化)实现了该接口。

代码窗口 045
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 的标准设置函数。

代码窗口 046

请参阅 periodnet.blogspot.com 上本文的 代码窗口 046

请注意,其签名和结构与之前为示例 StateCity 实体所示的标准设置函数非常相似。

14.4.2 ..在 Entity Framework 中

这个非常相似的 Entity Framework 函数可以编译。

代码窗口 047

请参阅 periodnet.blogspot.com 上本文的 代码窗口 047

但不幸的是,执行会产生以下错误:

"无法将类型“ServiceEF.WashDltJoinedToTrans”强制转换为类型“ServiceEF.ITransComposit”。LINQ to Entities 仅支持将实体数据模型基本类型强制转换。"

太糟糕了。我们被迫回退到为每个查询单独实现非通用版本。

代码窗口 048

请参阅 periodnet.blogspot.com 上本文的 代码窗口 048

14.4.3 ..在 NHibernate 中

这是 NHibernate 的标准设置函数。

代码窗口 049

请参阅 periodnet.blogspot.com 上本文的 代码窗口 049

请注意,其签名和结构与之前为示例 StateCity 实体所示的 NHibernate 标准设置函数非常相似。

14.5 用于构建 Transaction 集合的五次查询

Contents at a Glance   Detailed Contents

14.5.1 ..在 LINQ to SQL 中

这是 LINQ to SQL 的五次查询。

代码窗口 050

请参阅 periodnet.blogspot.com 上本文的 代码窗口 050

请注意,其结构与之前为 LINQ 显示的 GetStates()GetCities() 示例函数相似。但是,有一个区别是,对于 LINQ to SQL 提供程序,4 个子实体 LINQ 语句必须指定获取 Transaction 实体的 join。

14.5.2 ..在 Entity Framework 中

这是 Entity Framework 的五次查询。

代码窗口 051

请参阅 periodnet.blogspot.com 上本文的 代码窗口 051

请注意,其结构与之前为 LINQ 显示的 GetStates()GetCities() 示例函数相似。与 LINQ to SQL 版本不同,不需要 join 子句。可以指定它们,但在使用随 .NET 3.5 SP1 附带的 Entity Framework 版本时,这样做会导致生成效率较低的 SQL。

14.5.3 ..在 NHibernate 中

这是 NHibernate 的五次查询。

代码窗口 052

请参阅 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 不能再简单了。

代码窗口 053
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 更复杂。

代码窗口 054
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 几乎一样简单。

代码窗口 055
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

然而,它不必要地包含了 ItemTransaction 实体的字段,而这些字段在 NHibernate Criteria 查询中并未被请求。

15 使用 ORM 框架进行复杂数据写入操作

Contents at a Glance   Detailed Contents

要保存新的 Transaction 或修改现有的 Transaction,LEK 客户端会调用在 ServiceInterface.IService 接口上声明的 SaveTransaction() 函数。

代码窗口 056
[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 实体对象。还必须保存 NWAccntDlts、WashDlts、Items 和 DltAllocs。如果 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 所决定的策略是:

  1. 检查 Transaction 的行版本,以确保自快照发送到客户端以来它未发生更改。
  2. 使用(高效的)存储过程删除所有子实体。
  3. 在 LINQ to SQL 和 NHibernate 中创建并附加一个 Transaction 实体。在 Entity Framework 中查询然后更新一个 Transaction 实体。
  4. 将所有子实体添加为新对象。
  5. 将整个生成的对象图交给 ORM 以保存到数据库。

如果大多数修改的 Transaction 只有少量 Items、DltAllocs、NWAccntDlts 和 WashDlts,那么此策略将很好地工作。如果一个 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

这是将要描述的所有函数的依赖树:(请注意,“底部”——即所有其他函数依赖的代码——实际上在顶部。)

SaveTransFunctions.GIF

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 存储过程来实现的。

代码窗口 057
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 存储过程完成了这项工作。

代码窗口 058
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 中唯一使用的键类型:基于单个整数的非复合键。

代码窗口 059
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 实体中获取基于整数的外键。

代码窗口 060
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 版本。在此之前,以下似乎有效。

代码窗口 061
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 认为几乎所有字段都包含已修改的数据。

代码窗口 062
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 中容易得多。下面是执行此操作的函数。

代码窗口 063
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()

代码窗口 064
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
代码窗口 065

请参阅 periodnet.blogspot.com 上本文的 代码窗口 065

15.6.1.2 Entity Framework 和 NHibernate
代码窗口 066

请参阅 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 类集的向导也可以为存储过程创建包装器。下面的两个函数实际上是包装器的包装器。

代码窗口 067

请参阅 periodnet.blogspot.com 上本文的 代码窗口 067

15.6.2.2 Entity Framework

为 Entity Framework 手动创建了存储过程的包装函数。

代码窗口 068A

请参阅 periodnet.blogspot.com 上本文的 代码窗口 068A

请注意,获取用于创建命令对象的连接对象需要一些“挖掘”(第 4、5、27、28 行)。

15.6.2.3 NHibernate

也为 NHibernate 手动创建了存储过程的包装函数,它几乎与 Entity Framework 的相同。

代码窗口 068B

请参阅 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
代码窗口 069

请参阅 periodnet.blogspot.com 上本文的 代码窗口 069

15.6.3.2 Entity Framework
代码窗口 070

请参阅 periodnet.blogspot.com 上本文的 代码窗口 070

15.6.3.3 NHibernate
代码窗口 071

请参阅 periodnet.blogspot.com 上本文的 代码窗口 071

15.6.4 TransactionRowVersionMatches

Contents at a Glance   Detailed Contents

TransactionRowVersionMatches() 函数检索数据库中时间戳字段的数据,并将其与先前从数据库检索到的提供值进行比较。每个 ORM 的代码几乎相同。

15.6.4.1 LINQ to SQL
代码窗口 072

请参阅 periodnet.blogspot.com 上本文的 代码窗口 072

15.6.4.2 Entity Framework
代码窗口 073

请参阅 periodnet.blogspot.com 上本文的 代码窗口 073

15.6.4.3 NHibernate
代码窗口 074

请参阅 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
代码窗口 075

请参阅 periodnet.blogspot.com 上本文的 代码窗口 075

请注意,在第 5、10、20、30 和 42 行,通过简单地设置映射的外键整数字段来设置外键。

15.6.5.2 Entity Framework
代码窗口 076

请参阅 periodnet.blogspot.com 上本文的 代码窗口 076

请注意,在第 5、10、20、30 和 42 行,设置外键需要使用前面讨论过的自定义 EnsureFK() 函数。

15.6.5.3 NHibernate
代码窗口 077

请参阅 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(函数)
代码窗口 078

请参阅 periodnet.blogspot.com 上本文的 代码窗口 078

15.6.6.2 Entity Framework(函数)
代码窗口 079

请参阅 periodnet.blogspot.com 上本文的 代码窗口 079

15.6.6.3 NHibernate(函数)
代码窗口 080

请参阅 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
代码窗口 081

请参阅 periodnet.blogspot.com 上本文的 代码窗口 081

15.6.7.2 Entity Framework
代码窗口 082

请参阅 periodnet.blogspot.com 上本文的 代码窗口 082

15.6.7.3 NHibernate
代码窗口 083

请参阅 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() 函数提供“账户的净值账户差额”视图所需的数据。

NetWorthAccountDeltasForAnAccount.GIF

16.2 账户的项目

Contents at a Glance   Detailed Contents

要查看特定类型的支出或收入流(如电费账单)的历史记录,LEK 提供了“账户的项目”视图。此视图与“账户的净值账户差额”视图非常相似,您指定一个时间段或边界记账事件,然后得到一个账户更改列表。视图中的每一行实际上对应一个分配给选定 DltAllocAccntItem。每一行都有一个用于每个可能 Participant 的单元格,值为非零的单元格对应实际的 DltAllocGetDltAllocsOfTransactions() 函数提供“账户的项目”视图所需的数据。

ItemsForAnAccountSHRUNK.gif

16.3 摘要

Contents at a Glance   Detailed Contents

LEK 提供了“摘要”视图来显示一段时间内所有账户更改的总金额。可以从该视图获得的信息示例包括:某个 Participant 在过去一年中的食品杂货支出金额、某个 Participant 的储蓄账户在过去一年中的总存款金额、某个 Participant 在过去一年中的净值变化、某个 Participant 欠“大锅”(套现)的金额在过去一年中的变化。这些值可以显示在任何时间段(不只是年份)。如果系统中最初的几个 Transaction 确立了所有 NWAccnt 的当前值,并且时间段设置为在第一个 Transaction 之前开始,那么摘要视图将显示每个 Participant 的所有 NWAccnt 的实际值。“摘要”视图显示每个 Participant 和整个家庭的所有金额。GetSummaryOfTransactions() 函数提供“摘要”视图所需的数据。

Summary.GIF

16.4 项目

Contents at a Glance   Detailed Contents

Participants 和账户之间分配费用通常是相当主观的。通常,在我输入我妻子购买但我不太清楚有什么必要的东西的 Transaction 时,我会以一种她不会同意的方式分配费用。稍后,在准备记账时查看已输入的 Transactions,我会醒悟过来,修改分配,以维持家庭和睦。这两个活动,即查看已输入的 Transactions 和修改分配,可以从“Transactions”和“Transaction”视图中进行,但从“项目”视图中可以更有效地进行。此视图显示特定时间段或记账事件的所有 Items,而不考虑它们所属的 Transactions。Items 可以按 Participants 之间的分配百分比和按账户排序。这样可以很容易地一目了然地看到在一系列 Transactions 中谁在支付什么。该视图还提供了一种直接在视图中进行更改的方法,而无需打开单独的 Transactions。GetItemsInTransactions() 函数提供“项目”视图所需的数据。ResetItemsInTransactions() 函数保存“项目”视图中所做的更改。

ItemsSHRUNK.GIF

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 数据库。

以下是下载、构建和运行应用程序的所有步骤

  1. 下载 zip 文件。
  2. 将 zip 文件中的所有文件和目录解压缩到硬盘上的一个目录中。我将把这个目录称为“INSTALL”。
  3. 在 Visual Studio 2008 中打开 INSTALL\Lek.sln
  4. 确保 SQL Server 2005 正在运行。
  5. 打开 SQL Server Management Studio,创建一个名为 Lek(主数据库)的数据库和一个名为 LekDev(测试数据库)的数据库。
  6. 在 SQL Server Management Studio 中,为每个数据库运行 Solution Items\SchemaAdd.sql 中的 DDL SQL。
  7. 在 Visual Studio 中,打开 ServiceFactory\LEKSettings.config,将 ProdDBConnectionString 设置的值更改为您刚刚创建的 Lek 数据库的连接字符串,并将 DevDBConnectionString 设置的值更改为您刚刚创建的 LekDev 数据库的连接字符串。您可能只需要将“ADIT80”替换为您自己的计算机名即可。
  8. 构建解决方案。
  9. 从菜单中选择“测试 | 运行 | 解决方案中的所有测试”,并确保所有 94 个测试都通过。
  10. 运行“WinApp”项目,或者,如果您想将应用程序作为真正的三层应用程序运行,请运行“WinFormServer”项目。您将看到“选择 ORM 和数据库”对话框。
  11. 在“选择 ORM 和数据库”对话框中,选中“重置数据”复选框,然后单击“测试数据库”列中的其中一个按钮。
  12. 您将看到 LEK 对话框。

Lek.GIF

18 使用代码

Contents at a Glance   Detailed Contents

LEK 不会*试图*变得聪明。它不会尝试猜测您接下来要做什么或尝试猜测您想要什么。所有无模式对话框都不会与其他无模式对话框通信。因此,例如,当您保存一个新的 Transaction 时,它不会自动出现在“Transactions”窗体中。您需要单击“重新加载”才能看到它。当然,只有当您在“Transactions”窗体中的查询设置为使新 Transaction 匹配查询条件时,它才会显示。默认情况下,条件是“未记账”,并且新 Transactions 始终是未记账的,因此,默认情况下,单击“重新加载”时它会显示。模态对话框包括用于指定条件的对话框、“事务的净值账户差额”对话框(用于设置 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 读取连接字符串。
© . All rights reserved.