• Silverlight + WCF使用Linq to SQL以及ADO.NET Entity Data Model更新数据库子表方法


    前言:日前,Microsfot已发布了Silverlight 4.0 RC2,其所主导的WCF RIA Services也渐渐为更多的开发人员所认识并使用,分布式N层结构数据访问要为WCF RIA Serices所代替了么? 但这不是我想要讨论的问题,这里仅是针对Silverlight + WCF在使用Linq to SQL以及ADO.NET Entity Data Model下的更新数据库子表的方式作比较。

     

    使用Linq to SQLADO.NET Entity Data ModelEntity Framework实体框架,以下简称EF)访问数据库的一般方式是用实体Entity映射数据表,形成一种最直观的封装,对数据表的增、删、更改、查询等操作都可以变成对Entity的操作(这里说的只是“可以”,许多情况下Entity还是不如TSQL或存储过程方便的)。而在关系型数据库中,主子表的结构是很常见的,如下图的TMasterTDetail

     

     

    LINQ to SQL.NET 3.0)、EF 1.0.NET 3.5SP1)、EF 2.0.NET 4.0)中对于这种主子表关系的表示有所不同:

     

    LINQ to SQL

     

     

    l  子实体TDetail中含有MasterKey属性,对应主实体中的MasterKey

    l  在两个实体的设计图中均不显示出导航属性,但是在代码中存在的;

    l  导航属性、在DataContext中的属性均以单数形式显示(即不以TMastersTDetails形式显示),且没有地方能设置;

    l  特别要注意的是DataContext的序列化模式,要想在WCF中传输LINQ to SQL实体,就得要将序列化模式设为“单向Unidirectional”;

    l  为了控制并发共享冲突,要手去设置主键(一般是主键)的“时间戳”属性(对应的AttributeIsVersion),而且对于非int型的字段,这里似乎还有Bug。在默认情况下,各个实体的各个与数据表字段对应的属性的时间戳均false,也就是使用并发控制。


     

    EF 1.0

     

     

    l  子实体TDetail中没有MasterKey属性;

    l  在两个实体的设计图中均显示出了导航属性,在代码中同样也存在的;

    l  LINQ to SQL一样导航属性、在ObjectContext中的属性均以单数形式显示,但前者可以在设计器中设置,后者则没有地方可以更改;

    l  各个属性的“并发模式”默认为None,即不参与并发控制。

     

     

    EF 2.0

     

     

    l  子实体TDetail中有MasterKey属性,改回和LINQ to SQL一样了;

    l  在两个实体的设计图中均显示出了导航属性,在代码中同样也存在的;

    l  导航属性、在ObjectContext中的属性均可选择以单数、复数的形式显示,建议使用复数形式;

    l  各个属性的“并发模式”默认为None,即不参与并发控制;

    l  整个ObjectContext增加了一个Lazy Loading Enabled,且默认为true,这个属性和LINQ to SQL中的延迟加载类似,并且下文会提到要注意的地方。

     

    Silverlight + WCF + Entity更新数据库的方式一般是“调用WCFGetXXX()方法将Entity传送至Silverlight端,在界面上将Entity修改之后,调用WCFUpdateXXX()方法将Entity传回至WCF端进行操作”。在这种模式中,对于Silverlight端而言,Entity完全是离线的,和服务端没有关系,和数据库中的数据更是没有关系(因为要访问数据库就得先通过WCF),对于WCF端而言,Entity是以“Detached”的形态被传送回来的,处于一种未知的状态,只有先将其Attach,管理其状态,然后才有可能将其最新的值更新至数据库。

     

    而对于“Update更新”一词,本来就是应当针对是数据的“新版本”与“旧版本”才有所谓更新的,那么如何产生两个版本?

    l  一次性从WCF端传送两份相同的数据至Silverlight端,一个数据用于被更改,另一个则用于表示“原版本”;

    l  WCF端只传送一份数据至Silverlight端,Silverlight端自行复制一份,用作“原版本”;

    l  WCF端只传送一份数据至Silverlight端,Silverlight端在对该数据更改后传送回WCF端,WCF端再从数据库取出最新版本的数据,用作“原版本”。

    显然第一种方式增加了网络传输量,并且在Silverlight端对实体进行复制一般也不会占用多少CPU资源,第一种方式应当不可取。而针对后二者的情况展开来就成了对并发控制的讨论了,个人偏好最后一种方法,但是第二种情况有时也会用得到,比如要在Silverlight端进行当前版本与原始版本的比较并显示出不同的地方。

     

    前面说了这么多,这里才开始入正题。先看看Silverlight中的界面,很简单,主实体使用几个TextBox来呈现,子实体使用DataForm呈现:

     

     

    并针对两种更新方式,提供了“以当前版本进行更新”的单实体方式、“先复制原始版本,再以两个版本进行更新”的双实体方式。

     

    由于本文仅是演示在更新数据库子表(即子实体)上的代码,为了简单起见,直接在WCF服务中对实体进行操作,但是建议在实际业务应用上,应再增加一层业务class,在业务class中操作实体,WCF服务实例化业务class,并将实体作为参数传递给业务class

     

    更新子实体的关键在于如何让DataContext/ObjectContext知道哪些子实体是新增的、哪些是删除的、哪些是更改的,而这一步,有朋友说在LINQ to SQLDataContext能自动进行判断,不过我试了好久都没有试成功,能自动判断固然好,但是手工进行干预也未尝不是好事。

     

    先看看 LINQ to SQL

    [OperationContract]

    public Models.TMaster GetEntity(string masterKey)

    {

        using (Models.TestDataClassesDataContext db = new Models.TestDataClassesDataContext())

        {

            db.DeferredLoadingEnabled = false;

            System.Data.Linq.DataLoadOptions option = new System.Data.Linq.DataLoadOptions();

            option.LoadWith<Models.TMaster>(m => m.TDetail);

            db.LoadOptions = option;

            return db.TMaster.FirstOrDefault(m => m.MasterKey == masterKey);

        }

    }

     

    [OperationContract]

    public int UpdateWithTwoEntities(Models.TMaster current, Models.TMaster original)

    {

        using (Models.TestDataClassesDataContext db = new Models.TestDataClassesDataContext())

        {

            //db.DeferredLoadingEnabled = false;

            db.TMaster.Attach(current, original);

            this.ApplyCurrentAndOriginalValues(db, current , original);       

            db.SubmitChanges();

            return 1;

        }

    }

    /// <summary>

    /// 将主表标识为已更改,同时判断子表的新增、删除、更改。

    /// CSDN论坛上,有朋友说只需要一步db.TMaster.Attach(current, original);就可以完成

    /// 对子表的更新,可是试了好久好久都没有试出来,不管如何,就算真的可以由DataContext

    /// 自己完成对子表的新增、删除、更改的判断,知道要如何手工的去判断也未尝不是件好事。

    /// </summary>

    /// <param name="db"></param>

    /// <param name="current"></param>

    /// <param name="original"></param>

    void ApplyCurrentAndOriginalValues(Models.TestDataClassesDataContext db,

                                                Models.TMaster current, Models.TMaster original)

    {

        // 已被删除的子表数据

        var deleteds = original.TDetail.Where(ori => !current.TDetail

            .Any(d => d.DetailKey == ori.DetailKey && d.MasterKey == ori.MasterKey));

        db.TDetail.AttachAll(deleteds);

        db.TDetail.DeleteAllOnSubmit(deleteds);

     

        // 新增加的子表数据

        var inserteds = current.TDetail.Where(cur => !original.TDetail

            .Any(d => d.DetailKey == cur.DetailKey && d.MasterKey == cur.MasterKey));

        db.TDetail.InsertAllOnSubmit(inserteds);

     

        // 要更新的子表数据

        var updateds = current.TDetail.Where(cur => original.TDetail

            .Any(d => d.DetailKey == cur.DetailKey && d.MasterKey == cur.MasterKey));

        foreach (Models.TDetail upd in updateds)

        {

            db.TDetail.Attach(upd, original.TDetail.First(d =>

                        d.DetailKey == upd.DetailKey && d.MasterKey == upd.MasterKey));

        }

    }

     

    [OperationContract]

    public int UpdateWithCurrentEntity(Models.TMaster current)

    {

        // LINQ to SQL 中,没有 DataContext.Detach() 方法,要将一个已附加的实体 Detach

        // 就只有序列化后再反序列化(即Clone)得到新的实体。

        // 不过,还有一种方法可以绕道,那就是在另一个不同的 DataContext 中去得到实体

        return this.UpdateWithTwoEntities(current, this.GetEntity(current.MasterKey));

    }

     

    注意:

    l   LINQ to SQL 中,没有 DataContext.Detach() 方法,要将一个已附加的实体 Detach,就只有序列化后再反序列化(即Clone)得到新的实体。不过,还有一种方法可以绕道,那就是在另一个不同的 DataContext 中去得到实体;

    l   Attach(entity,bool) 要求 entity 有时间戳。

     

    在这里,看看默认情况下LINQ to SQL的并发控制所产生的SQL,以对子实体TDetail进行Update为例,打开SQL Server Profile,看到运行的SQL如下:

    exec sp_executesql N'UPDATE [dbo].[TDetail]

    SET [DetailName] = @p6

    WHERE ([DetailKey] = @p0) AND ([MasterKey] = @p1) AND ([DetailName] = @p2) AND ([Fa] = @p3) AND ([Fb] = @p4) AND ([Fc] = @p5)'

    ,N'@p0 varchar(4),@p1 varchar(4),@p2 varchar(4),@p3 int,@p4 varchar(4),@p5 datetime,@p6 varchar(4)'

    ,@p0='D001',@p1='Key1',@p2='DN A',@p3=100,@p4='Fb 1',@p5='2010-08-08 00:00:00:000',@p6='DN B'

    只是Update了一个DetailName字段,就要把全部的字段都作为Where条件,这是开放式并发的SQL

     

    修改子实体TDetail的主键DetailKey的时间戳为True

     

     

    修改后“自动生成的值”会自动变为True,由于是Varchar型,不是Int Identity,所以把它改回False

     

     

    然后再运行测试,得出的SQL为:

    exec sp_executesql N'UPDATE [dbo].[TDetail]

    SET [DetailName] = @p2

    WHERE ([DetailKey] = @p0) AND ([DetailKey] = @p1)'

    ,N'@p0 varchar(4),@p1 varchar(4),@p2 varchar(4)',@p0='D001',@p1='D001',@p2='DN C'

    仅有主键字段使用了开放式并发控制。

     

    不过这里似乎有一个Bug,也可能是我设置得不对,在对TDetail进行插入时,虽然“自动生成的值”已经设为False,可是居然还是尝试去自动生成值,并出现异常

     

     

    接下来看 EF 1.0

    [OperationContract]

    public Models.TMaster GetEntity(string masterKey)

    {

        using (Models.TestEntities db = new Models.TestEntities())

        {

            // .NET 3.5( Entity Framework 1.0)中,没有 db.ContextOptions.LazyLoadingEnabled 可以设置,

            // 而且相当于 db.ContextOptions.LazyLoadingEnabled = false

            Models.TMaster master = db.TMaster.FirstOrDefault(m => m.MasterKey == masterKey);

            master.TDetail.Load();

            return master;

        }

    }

     

    [OperationContract]

    public int UpdateWithTwoEntities(Models.TMaster current, Models.TMaster original)

    {

        using (Models.TestEntities db = new Models.TestEntities())

        {

            // 这里不能写成 db.Attach(current);

            // Attach 会导致新增的子实体的EntityKey != null EntityState = Unchanged

            // 会导致已删除的子实体的 EntityState = Unchanged,原本为 null

            db.Attach(original);

            this.ApplyCurrentAndOriginalValues(db, current, original);

            return db.SaveChanges();

        }

    }

     

    [OperationContract]

    public int UpdateWithCurrentEntity(Models.TMaster current)

    {

        using (Models.TestEntities db = new Models.TestEntities())

        {

            Models.TMaster original = null;

            System.Data.EntityKey key = db.CreateEntityKey(current.EntityKey.EntitySetName, current);

            object oriDataObj;

            if (db.TryGetObjectByKey(key, out oriDataObj))

            {

                original = oriDataObj as Models.TMaster;

                // 只能手工加载(因为相当于 ContextOptions.LazyLoadingEnabled = false

                original.TDetail.Load();

     

                this.ApplyCurrentAndOriginalValues(db, current, original);

            }

     

            return db.SaveChanges();

        }

    }

     

    /// <summary>

    /// 将主表标识为已更改,同时判断子表的新增、删除、更改

    /// </summary>

    /// <param name="db"></param>

    /// <param name="current"></param>

    /// <param name="original"></param>

    void ApplyCurrentAndOriginalValues(Models.TestEntities db, Models.TMaster current, Models.TMaster original)

    {

        // 这个方法在 .NET 4 中已过时,被 ApplyCurrentValues<T>() 代替

        db.ApplyPropertyChanges(current.EntityKey.EntitySetName, current);

     

        // 已被删除的子表数据

        var deleteds = original.TDetail.Where(ori => !current.TDetail.Any(d => d.DetailKey == ori.DetailKey));

        for (int i = deleteds.Count() - 1; i >= 0; i--)

        {

            Models.TDetail d = deleteds.ElementAt(i);

            // 下面这句要不要执行无所谓,不执行的话则可以使用 foreach

            original.TDetail.Remove(d);

            db.DeleteObject(d);

     

        }

        // 新增加的子表数据

        var inserteds = current.TDetail.Where(d => d.EntityKey == null);

        for (int i = inserteds.Count() - 1; i >= 0; i--)

        {

            Models.TDetail d = inserteds.ElementAt(i);

            current.TDetail.Remove(d);

            // 执行下面的语句,会导致 original.TDetail.Add(d); 也会被执行

            d.TMasterReference.Value = original;

            db.AddToTDetail(d);

     

        }

        // 剩下的子表数据就是被更新的、或没有变化的,

        // 但是要注意 original.TDetail 的条数可能比 current.TDetail

        foreach (Models.TDetail d in original.TDetail)

        {

            Models.TDetail curDetail = current.TDetail.FirstOrDefault(cur => cur.DetailKey == d.DetailKey);

            if (curDetail != null)

            {

                db.Attach(d);

                db.ApplyPropertyChanges(d.EntityKey.EntitySetName, curDetail);

            }

        }

    }

     

    注意:

    l   ObjectContext相当于ContextOptions.LazyLoadingEnabled = false

    l   新增的子实体的EntityKey == null

    l   Attach(entity) 应当附加“原始版本”,而不是“当前版本”;

    l   tDetail.TMasterReference.Value = tMaster执行时,会导致 tMaster.TDetail.Add(tDetail) 被执行。

     

    运行测试,得出的更新子表的SQL为:

    exec sp_executesql N'update [dbo].[TDetail]

    set [DetailName] = @0

    where ([DetailKey] = @1)

    ',N'@0 varchar(4),@1 varchar(4)',@0='DN E',@1='D001'

    完全没有使用并发控制。

     

    再看 EF 2.0

     

    [OperationContract]

    public Models.TMaster GetEntity(string masterKey)

    {

        // .NET 4 中,TestEntities() 的构造函数中写了 ContextOptions.LazyLoadingEnabled = true
        //
    下面这样写会在序列化主表时自动查询子表
        //Models.TestEntities db = new Models.TestEntities();
        //return db.TMasters.FirstOrDefault(m => m.MasterKey == masterKey);

        // 但是奇怪的是加上 using 语句,则会导致主表在序列化时出错,因为此时子表尚未被实例化。
        //using (Models.TestEntities db = new Models.TestEntities())
        //{
        //    db.ContextOptions.LazyLoadingEnabled = true;
        //    return db.TMasters.FirstOrDefault(m => m.MasterKey == masterKey);
        //}

     

        // 要使用 using , 只得设置 db.ContextOptions.LazyLoadingEnabled = false

        using (Models.TestEntities db = new Models.TestEntities())

        {

            //事实上 db.ContextOptions.LazyLoadingEnabled = false 往往是更为灵活的,因为并非每个子表都是我们想要加载的

            db.ContextOptions.LazyLoadingEnabled = false;

            Models.TMaster master = db.TMasters.FirstOrDefault(m => m.MasterKey == masterKey);

            master.TDetails.Load();

            return master;

        }

    }

     

    [OperationContract]

    public int UpdateWithTwoEntities(Models.TMaster current , Models.TMaster original)

    {

        using (Models.TestEntities db = new Models.TestEntities())

        {

            // 这里不能写成 db.Attach(current);

            // Attach 会导致新增的子实体的EntityKey != null EntityState = Unchanged

            // 会导致已删除的子实体的 EntityState = Unchanged,原本为 null

            db.Attach(original);

            this.ApplyCurrentAndOriginalValues(db, current, original);

            return db.SaveChanges();

        }

    }

     

    [OperationContract]

    public int UpdateWithCurrentEntity(Models.TMaster current)

    {

        using (Models.TestEntities db = new Models.TestEntities())

        {

            Models.TMaster original = null;

            System.Data.EntityKey key = db.CreateEntityKey(current.EntityKey.EntitySetName, current);

            object oriDataObj;

            if (db.TryGetObjectByKey(key, out oriDataObj))

            {

                original = oriDataObj as Models.TMaster;

                this.ApplyCurrentAndOriginalValues(db, current, original);

            }

            return db.SaveChanges();

        }

    }

     

    /// <summary>

    /// 将主表标识为已更改,同时判断子表的新增、删除、更改

    /// </summary>

    /// <param name="db"></param>

    /// <param name="current"></param>

    /// <param name="original"></param>

    void ApplyCurrentAndOriginalValues(Models.TestEntities db , Models.TMaster current, Models.TMaster original)

    {

        db.TMasters.ApplyOriginalValues(original);

        db.TMasters.ApplyCurrentValues(current);

     

        // 已被删除的子表数据

        var deleteds = original.TDetails.Where(ori => !current.TDetails

            .Any(d => d.DetailKey == ori.DetailKey && d.MasterKey == ori.MasterKey));

        for (int i = deleteds.Count() - 1; i >= 0; i--)

        {

            Models.TDetail d = deleteds.ElementAt(i);

            // 下面这句要不要执行无所谓,不执行的话则可以使用 foreach

            original.TDetails.Remove(d);

            db.DeleteObject(d);

     

        }

        // 新增加的子表数据

        var inserteds = current.TDetails.Where(d => d.EntityKey == null);

        for (int i = inserteds.Count() - 1; i >= 0; i--)

        {

            Models.TDetail d = inserteds.ElementAt(i);

            current.TDetails.Remove(d);

            // 执行下面的语句,会导致 original.TDetails.Add(d); 也会被执行

            d.TMasterReference.Value = original;

            db.AddToTDetails(d);

     

        }

        // 剩下的子表数据就是被更新的、或没有变化的,

        // 但是要注意 original.TDetails 的条数可能比 current.TDetails

        foreach (Models.TDetail d in current.TDetails)

        {

            db.TDetails.ApplyOriginalValues(original.TDetails

                .First(ori=>d.DetailKey == ori.DetailKey && d.MasterKey == ori.MasterKey));

            db.TDetails.ApplyCurrentValues(d);

        }

    }

     

    注意:

    l  GetEntity()中提到的问题,可能是一个Bug,有心的朋友可以通过IL代码查看一下,确定是不是Bug

    l  EF 1.0ApplyPropertyChanges()已过时。

     

    使用 LINQ to SQL还是使用EF,这里不想比较,相信都使用过的朋友自然会进行比较。

  • 相关阅读:
    centos7下安装Jdk8、Tomcat8
    CentOS7.3下使用YUM安装MySQL5.6
    数据库表直接生成Excel
    分页工具类
    ResponseUtil
    图片上传
    UUIDUtils
    html和js
    java
    Eclipse启动发生的错误An internal error occurred during: "Initializing Java Tooling".
  • 原文地址:https://www.cnblogs.com/Sunpire/p/1714956.html
Copyright © 2020-2023  润新知