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

LINQ to Entities 业务对象

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.56/5 (6投票s)

2011年1月19日

CPOL

10分钟阅读

viewsIcon

33673

如何开始设计 LINQ to Entities 业务对象。

引言

在开发了许多使用 LINQ to SQL 技术的应用程序之后,我终于决定仔细研究 LINQ to Entities。

我们知道微软已经停止开发 LINQ to SQL,尽管他们仍在支持它。我阅读了几篇比较这两种技术的文章,评价褒贬不一。作者抱怨 LINQ to Entities 臃肿,不支持 POCO 等……如果你在网上找到这篇文章,我想你已经在网上读过这些内容了。

幸运的是,他们说微软已经在 Visual Studio 2010 中解决了这些问题。

我喜欢 LINQ-TO-SQL;它能够将数据库通信融入到你的编程代码中。

我大力支持分层架构,并且由于 LINQ to SQL 代表直接的数据库通信,我将数据访问层编译为一个独立的类库。该库有三种类型的文件:由 Visual Studio 生成的 LINQ to SQL 类文件、一个 DbHelper 文件——一个包含有用函数和方法的自定义文件,以及一个继承 DbHelper 类并负责处理所有数据库对象的控制器文件。

我的业务层与数据访问层通信,并且通常是另一个包含所有业务对象的类库。这些对象利用数据访问层对象的属性和方法。表示层只与业务层通信,并且对数据访问层是隐藏的。不幸的是,这种方法有时需要大量的重复编码。我经常需要在我的业务对象中重复数据对象的属性和方法。LINQ to Entities 技术在概念上是不同的。它不是直接与数据库通信,而是与概念实体通信。它们通过特殊的映射连接到数据库对象。

理想情况下,一旦完成了映射,你根本就不会关心数据库。LINQ to SQL 直接针对 MS SQL Server,而 LINQ to Entities 则旨在独立于数据库。

所以,我决定尝试这项技术,并找出它能为我节省多少编码时间和精力。另外,我还决定将业务层与数据访问层结合起来,以节省编码时间。

LINQ to Entities 技术为你提供了许多不同的项目处理选项。我选择了 ADO.NET Entity 数据模型从数据库生成代码的选项。它不是一个完美的代码,而是一个概念验证。我没有比较性能。我认为这以后再说。

让我们一起开始吧。

数据库

首先,创建数据库。我使用的是 MS SQL Server 2008。运行以下脚本

CREATE TABLE [dbo].[Customer](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [Name] [varchar](50) NOT NULL,
    [Title] [varchar](50) NOT NULL,
 CONSTRAINT [PK_Customer] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)
)

GO
CREATE TABLE [dbo].[AddressType](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [Type] [varchar](50) NOT NULL,
 CONSTRAINT [PK_AddressType] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)
)
GO

CREATE TABLE [dbo].[CustomerAddress](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [CutomerID] [int] NOT NULL,
    [TypeID] [int] NOT NULL,
    [Address] [varchar](50) NULL,
    [City] [varchar](50) NULL,
    [State] [varchar](50) NULL,
    [PostalCode] [varchar](50) NULL,
    [Country] [varchar](50) NULL,
 CONSTRAINT [PK_CustomerAddress] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)
) 
GO

ALTER TABLE [dbo].[CustomerAddress]  WITH CHECK ADD  CONSTRAINT 
                  [FK_CustomerAddress_AddressType] FOREIGN KEY([TypeID])
REFERENCES [dbo].[AddressType] ([ID])
GO
ALTER TABLE [dbo].[CustomerAddress] CHECK CONSTRAINT 
            [FK_CustomerAddress_AddressType]
GO

ALTER TABLE [dbo].[CustomerAddress]  WITH CHECK ADD  CONSTRAINT 
                  [FK_CustomerAddress_Customer] FOREIGN KEY([CutomerID])
REFERENCES [dbo].[Customer] ([ID])
GO
ALTER TABLE [dbo].[CustomerAddress] CHECK CONSTRAINT 
            [FK_CustomerAddress_Customer]
GO

正如你所见,我创建了三个简单的表

  1. 客户
  2. CustomerAddress
  3. AddressType

Customer 存储客户的主要信息。CustomerAddress 存储客户不同地址的信息。AddressType 指定可能的地址类型(如家庭、办公室等)。

每个表都有一个主键,并且它们通过外键约束相互关联。

Visual Studio 解决方案

我使用的是 Visual Studio 2010。创建一个 Windows 控制台项目,并命名为 TestSqlToEntities。将默认解决方案名称更改为 TestSqlToEntitiesSol。解决方案创建后,向解决方案添加一个新的类库项目,并命名为 EntityModel。删除此解决方案中默认的 Class1.cs 文件。现在,你需要将 ADO.NET Entity 数据模型添加到你的 EntityModel 项目中。

我将尝试引导你完成整个过程;对于那些熟悉它的人,请跳过这些说明。

  1. 点击 添加 > 新项。
  2. 将文件名更改为 ObjectContext.edmx。但这并非必需。

  3. 选择模型内容。
  4. ChoseModelContents.gif

    单击“下一步”。

  5. 选择数据连接。
  6. ChooseDataConnections.gif

    正如你所见,我已经有了到相应数据库的连接。你需要点击“新建连接...”按钮并按照说明进行。它将带你到相同的屏幕。将 App.Config 文件中的默认设置名称更改为 ModelEntities。这正是我在代码中使用过的。

    单击“下一步”。

  7. 选择数据库对象
  8. ChooseDbObjects.gif

    单击“完成”。

结果,你将看到以下 ObjectContext.edmx 文件(设计模式)

edmx.gif

请花时间熟悉该文件;你可以通过右键单击它并选择 打开方式... 选项,使用不同的编辑器打开它。你会发现该文件实际上只是一个 XML 文件。从它的结构中,你可以识别概念是如何与数据存储分离的。但由于本文档有不同的目标,让我们继续。

参考文献

请添加这些引用

  1. TestSqlToEntities 到 EntityModel(项目引用)
  2. TestSqlToEnties 到 System.Data.Entity(程序集引用)
  3. EntityModel 到 System.Configuration(程序集引用)

实体模型

现在让我们看一下 Objectcontext.edmx.cs 文件。这个文件是由 Visual Studio 生成的。

在我们的例子中,创建了四个类。

  1. ModelEntities 类继承自 ObjectContext,并具有三个公共属性,用于返回 CustomerCustomerAddressAddressTypes 的对象集。
  2. Customer 类(EntityObject
  3. CustomerAddress 类(EntityObject
  4. AddressType 类(Entity object)

通过这些类,我们可以与数据库通信,并且因为它们代表了概念层,我们可以将它们视为业务类而不是数据访问类。

问题是我们想在它们中添加一些状态和行为,但我们做不到,因为基文件是由 Visual Studio 生成的。下次我们从数据库修改 edmx 文件时,任何添加的内容都会被擦除。

幸运的是,我们可以添加一些部分类来解决这个问题。

但在这样做之前,让我们创建一个 ModelHelper 文件。

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq;
using System.Text;
using System.Configuration;
using System.Reflection;
using System.Collections;
using System.ComponentModel;

namespace EntityModel
{
    public class ModelHelper
    {
        public static ModelEntities 
               GetObjectContext(ModelEntities context = null)
        {
            if (null != context && context.Connection.State == 
                      ConnectionState.Open) return context;

            return new ModelEntities(
              ConfigurationManager.ConnectionStrings[
              "ModelEntities"].ConnectionString);
        }

        public static object Get<TEntity>(EntityKey key, 
                                ModelEntities context = null)
        {
            try
            {
                return (TEntity)GetObjectContext(context).GetObjectByKey(key);
            }
            catch
            {
                return null;
            }
        }

        public static TEntity Save<TEntity>(TEntity entity, 
                         EntityKey key, ModelEntities context = null)
        {
            EntityObject ent = entity as EntityObject;

            using (ModelEntities updater = GetObjectContext(context))
            {        
                    try
                    {
                        TEntity dbEntity = (TEntity)updater.GetObjectByKey(key);
                        DeepCopy<TEntity>(entity, dbEntity);
                    }
                    catch (Exception ex)
                    {
                        if (ex is System.Data.ObjectNotFoundException)     
                        updater.AddObject(key.EntitySetName, entity);
                    }
                updater.SaveChanges();
                return entity;
            }

        }

        public static void Delete<TEntity>(TEntity entity, 
                             EntityKey key, ModelEntities context = null)
        {
            using (ModelEntities updater = GetObjectContext(context))
            {
                try
                {
                    TEntity dbEntity = (TEntity)updater.GetObjectByKey(key);
                    updater.DeleteObject(dbEntity);
                    updater.SaveChanges();
                }
                catch { }
            }
        }

        #region "Utility functions"

        public static void DeepCopy<T>(T copyFrom, T copyTo)
        {
            //get properties info from source:
            PropertyInfo[] propertiesFrom = copyFrom.GetType().GetProperties();

            //loop through the properties info:
            foreach (PropertyInfo propertyFrom in propertiesFrom)
            {
                //get value from source:
                var valueFrom = propertyFrom.GetValue(copyFrom, null);

                //get property from destination
                var propertyTo = copyTo.GetType().GetProperty(propertyFrom.Name);


                if (propertyTo != null && valueFrom != null)
                //a bit of extra validation
                {
                    //the property is an entity collection:
                    if (valueFrom.GetType().Name.Contains("EntityCollection"))
                    {
                        //get value from destination
                        var valueTo = copyTo.GetType().GetProperty(
                                         propertyFrom.Name).GetValue(copyTo, null);

                        //get collection generic type:
                        Type genericType = propertyTo.PropertyType.GetGenericArguments()[0];


                        //get list source from source:
                        IListSource colFrom = (IListSource)valueFrom;

                        //get list source from destination:
                        IListSource colTo = (IListSource)valueTo;

                        //loop through list source:
                        foreach (dynamic b in colFrom.GetList())
                        {
                            //create instance of the generic type:
                            dynamic c = (dynamic)Activator.CreateInstance(genericType);

                            //copy source into this instance:
                            DeepCopy<dynamic>(b, c);

                            //add the instance into destination entity collection:
                            colTo.GetList().Add(c);
                        }
                    }

                    // do not copy if the property:
                    //is  entity object,
                    //is entity reference,
                    //entity state,
                    //entity key
                    else if (propertyTo.PropertyType.BaseType.Name.Contains("EntityObject")
                        || valueFrom.GetType().Name.Contains("EntityReference")
                        || valueFrom.GetType().Name.Contains("EntityState")
                        || valueFrom.GetType().Name.Contains("EntityKey"))
                    {
                        //do nothing;
                    }
                    else // set the value of the destination property:
                        propertyTo.SetValue(copyTo, valueFrom, null);
                }
            }
        }

        #endregion
    }
}

这需要一些解释

public static ModelEntities GetObjectContext(ModelEntities context = null)
{
    if (null != context && context.Connection.State == ConnectionState.Open)
        return context;

    return new ModelEntities(
      ConfigurationManager.ConnectionStrings[
      "ModelEntities"].ConnectionString);
}

此函数返回 ModeEntities,它继承自 ObjectContext 类。ObjectContext 类提供了将实体数据作为对象进行查询和操作的功能。ObjectContext 类是与实体类型(在概念模型中定义)作为对象进行交互的主要类。ObjectContext 类的实例封装了一个数据库连接,形式为 EntityConnection 对象。GetObjectContext 函数接受一个可选的 ModelEntities 类型参数,默认值为 null。这样做是为了在需要时重用现有的 ObjectContext,而不是打开一个新的。你也可以选择将上下文提供给此函数,如果已打开数据库连接,则会返回该连接。否则,将创建并返回新的 ObjectContext

此函数的连接字符串是通过 ConfigurationManager 检索的。**注意:** 我将 EntityModel 项目中的 App.config 文件剪切并移到了主项目中。

public static object Get<TEntity>(EntityKey key, ModelEntities context = null)
{
    try
    {
        return (TEntity)GetObjectContext(context).GetObjectByKey(key);
    }
    catch
    {
        return null;
    }
}

GetObjectByKey 尝试从 ObjectStateManager 中检索具有指定 EntityKey 的对象。如果对象当前未加载到对象上下文中,则会执行查询以尝试从数据源返回对象。如果对象已加载,它会在不访问数据源的情况下返回对象,这是与 LINQ to SQL 的一个重要区别。

EntityKey 类提供了对实体类型实例的持久引用。每种实体类型都有一个基于实体的一个或多个标量属性的键。键由概念模型中的 Key 元素定义。与关系数据库一样,这些键值用于验证给定实体的唯一性并提高查询性能。通常,键属性映射到底层表中的键列,该列是标识列或其他受约束以保证唯一值的列。我为泛型类型的实体对象编写了这个函数。

public static TEntity Save<TEntity>(TEntity entity, 
              EntityKey key, ModelEntities context = null)
{
    EntityObject ent = entity as EntityObject;

    using (ModelEntities updater = GetObjectContext(context))
    {        
        try
        {
            TEntity dbEntity = (TEntity)updater.GetObjectByKey(key);
            DeepCopy<TEntity>(entity, dbEntity);
        }
        catch (Exception ex)
        {
            if (ex is System.Data.ObjectNotFoundException)     
            updater.AddObject(key.EntitySetName, entity);
        }
        updater.SaveChanges();
        return entity;
    }
}

Save 函数也是泛型的。它接受实体对象和该对象的实体键。即使键指向一个不存在的对象,也必须提供 EntityKeyEntityKey 有助于定义对象的标识。在向 ObjectSet 添加新的泛型对象时,EntityKey.EntitySetName 属性用于定义 EntitySetName。我使用 try... catch... 结构来判断对象是否存在并需要更新,还是需要将新对象添加到集合中。此函数使用我稍后将介绍的 DeepCopy 函数。

public static void Delete<TEntity>(TEntity entity, 
              EntityKey key, ModelEntities context = null)
{
    using (ModelEntities updater = GetObjectContext(context))
    {
        try
        {
            TEntity dbEntity = (TEntity)updater.GetObjectByKey(key);
            updater.DeleteObject(dbEntity);
            updater.SaveChanges();
        }
        catch { }
    }
}

此函数无需详细说明。它只是删除实体对象。

public static void DeepCopy<T>(T copyFrom, T copyTo)
{
    //get properties info from source:
    PropertyInfo[] propertiesFrom = copyFrom.GetType().GetProperties();

    //loop through the properties info:
    foreach (PropertyInfo propertyFrom in propertiesFrom)
    {
        //get value from source:
        var valueFrom = propertyFrom.GetValue(copyFrom, null);

        //get property from destination
        var propertyTo = copyTo.GetType().GetProperty(propertyFrom.Name);


        if (propertyTo != null && valueFrom != null)
        //a bit of extra validation
        {
            //the property is an entity collection:
            if (valueFrom.GetType().Name.Contains("EntityCollection"))
            {
                //get value from destination
                var valueTo = copyTo.GetType().GetProperty(
                               propertyFrom.Name).GetValue(copyTo, null);

                //get collection generic type:
                Type genericType = 
                  propertyTo.PropertyType.GetGenericArguments()[0];


                //get list source from source:
                IListSource colFrom = (IListSource)valueFrom;

                //get list source from destination:
                IListSource colTo = (IListSource)valueTo;

                //loop through list source:
                foreach (dynamic b in colFrom.GetList())
                {
                    //create instance of the generic type:
                    dynamic c = (dynamic)Activator.CreateInstance(genericType);

                    //copy source into this instance:
                    DeepCopy<dynamic>(b, c);

                    //add the instance into destination entity collection:
                    colTo.GetList().Add(c);
                }
            }

            // do not copy if the property:
            //is  entity object,
            //is entity reference,
            //entity state,
            //entity key
            else if (propertyTo.PropertyType.BaseType.Name.Contains("EntityObject")
                || valueFrom.GetType().Name.Contains("EntityReference")
                || valueFrom.GetType().Name.Contains("EntityState")
                || valueFrom.GetType().Name.Contains("EntityKey"))
            {
                //do nothing;
            }
            else // set the value of the destination property:
                propertyTo.SetValue(copyTo, valueFrom, null);
        }
    }
}

此递归方法使用 System.Reflection 将一个实体对象复制到另一个实体对象。它遍历源对象的属性并分析每个属性。如果属性是简单的,则将其值复制到目标对象的相应属性值。如果属性是实体集合,则遍历集合泛型类型并递归地复制到目标对象的相应集合中。它不复制某些属性。我已添加注释;请遵循注释来理解其工作原理。

处理业务

让我们添加新文件。它将是 Customer 的部分类。在这个类中,我们将使用我们的 ModelHelper 类以及 Visual Studio 为我们生成的所有类。

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text;

namespace EntityModel
{
    public partial class Customer
    {
        private EntityKey entityKey = null;

        public Customer() { entityKey = new EntityKey(
                 "ModelEntities.Customers", "ID", 0); }

        public Customer(int id)
        {
            if (id > 0)
                entityKey = new EntityKey("ModelEntities.Customers", "ID", id);
            Customer b = (Customer)ModelHelper.Get<Customer>(entityKey);
            if (null != b)
                ModelHelper.DeepCopy<Customer>(b, this);
        }

        public Customer Save()
        {
            return ModelHelper.Save<Customer>(this, entityKey);
        }

        public void Delete()
        {
            ModelHelper.Delete<Customer>(this, entityKey);
        }

        public static List<Customer> GetAll(ModelEntities context)
        {
            var list = context.Customers.ToList();
            return list;
        }

        public static CustomerAddress CreateAddress(string address, 
               string addressType, string city, string country, 
               string state = null, string postalCode = null, 
               bool saveOnCreation = false)
        {
            CustomerAddress ca = new CustomerAddress();
            EntityKey caEntityKey = new EntityKey(
              "ModelEntities.CustomerAddresses", "ID", 0);
            ca.Address = address;
            ca.Country = country;
            ca.City = city;
            ca.State = state;
            ca.PostalCode = postalCode;

            //process address type:
            AddressType at = Customer.CreateAddressType(addressType, saveOnCreation);
            if (at.ID > 0)
                ca.TypeID = at.ID;
            else
                ca.AddressType = at;

            if (saveOnCreation)
                ModelHelper.Save<CustomerAddress>(ca, caEntityKey);
            return ca;

        }

        public static AddressType CreateAddressType(
                     string type, bool saveOnCreation = false)
        {
            using (ModelEntities db = ModelHelper.GetObjectContext())
            {
                AddressType at = db.AddressTypes.Where(
                   o => o.Type.ToLower() == type.ToLower()).FirstOrDefault();
                EntityKey atEntityKey = new EntityKey(
                   "ModelEntities.AddressTypes", "ID", 0);
                if (null != at)
                    return at;
                else
                {
                    at = new AddressType();
                    at.Type = type;
                }
                if (saveOnCreation)
                    ModelHelper.Save<AddressType>(at, atEntityKey);

                return at;
            }
        }
    }
}

此类有两个构造函数,以及 Save()Delete()GetAll() 实例方法。我们需要处理创建类的 EntityKey,然后使用 ModelHelper 来帮助。此外,该类有几个静态方法来创建 AddressTypeCustomerAddress 对象。

代码很简单,不需要任何解释。

程序

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EntityModel;

namespace TestSqlToEntities
{
    class Program
    {
        static void Main(string[] args)
        {

            Customer c = new Customer();
            c.Name = "Name";
            c.Title = "Title";
            CustomerAddress ca = Customer.CreateAddress("Street address", 
                 "Home", "City", "Country", 
                 "State", "Post Code");

            c.CustomerAddresses.Add(ca);

            c.Save();

            using (ModelEntities db = ModelHelper.GetObjectContext())
            {
                List<Customer> list = Customer.GetAll(db);

                string line = "{0} {1} {2} {3}";

                foreach (Customer cst in list)
                {
                    foreach (CustomerAddress cadr in cst.CustomerAddresses)
                    {
                        Console.WriteLine(line, cst.Title, cst.Name, 
                                 cadr.Address, cadr.AddressType.Type);
                    }


                }

                Console.ReadKey();
            }
        }
    }
}

以下代码将创建对象,将它们保存到数据库,并显示已保存的内容。代码以创建一个空的 Customer 对象开始。然后为其 NameTitle 属性赋值。

然后创建 CustomerAddress 对象。我们将 CustomerAddress 添加到 Customer 中并保存。LINQ to Entities 负责处理数据库。如果 AddressType(在本例中是 Home)存在,它将被使用,否则将创建 AddressType 的新条目。因此,简单的 c.Save() 操作将影响三个或两个表。

然后程序显示客户列表。你会注意到我正在使用

using (ModelEntities db = ModelHelper.GetObjectContext())
{
 List<Customer> list = Customer.GetAll(db);
 ................................................
}

并将数据库传递给 GetAll 过程。原因是,在从 Customer 对象检索数据时,要保持“dbObjectContext 的范围。LINQ to SQL 会进行惰性请求。这意味着,如果底层对象未被引用,则不会请求其数据。当我引用底层对象时(例如,cst.AddressType.Type),此时才会检索数据。这就是为什么我需要“db”对象是就绪且连接的原因。一旦它超出范围(在“using”之后),它就会断开连接。

结论

我没有考虑这个小型项目的性能,而是想找出 LINQ to Entities 与 LINQ to SQL 相比的便捷性。我必须承认,我对这项技术印象深刻。如你所见,我开发的简单的 ModelHelper 类为我节省了大量的编码工作。你可以为 Visual Studio 生成的一些 EntityObject 创建部分类。将这些类与 ModelHelper 结合使用实例和静态函数,你可以轻松创建所需的任何业务功能。

© . All rights reserved.