LINQ to SQL:附加还是不附加?

10

我非常难以确定何时应该附加到对象,何时不应该附加到对象。首先,这是我的(非常简化的)对象模型的小图示。

Schema

在我的数据访问层中,每次进行与数据相关的操作时,我都会创建一个新的DataContext。例如,如果我想保存一个新用户,我会在业务层中创建一个新的用户。

var user = new User();
user.FirstName = "Bob";
user.LastName = "Smith";
user.Username = "bob.smith";
user.Password = StringUtilities.EncodePassword("MyPassword123");
user.Organization = someOrganization; // Assume that someOrganization was loaded and it's data context has been garbage collected.

现在我想去保存这个用户。

var userRepository = new RepositoryFactory.GetRepository<UserRepository>();
userRepository.Save(user);

太棒了!这是我的保存逻辑:

public void Save(User user)
{
 if (!DataContext.Users.Contains(user))
 {
  user.Id = Guid.NewGuid();
  user.CreatedDate = DateTime.Now;
  user.Disabled = false;

  //DataContext.Organizations.Attach(user.Organization);
  DataContext.Users.InsertOnSubmit(user);
 }
 else 
 {
  DataContext.Users.Attach(user);
 }

 DataContext.SubmitChanges();

 // Finished here as well.
 user.Detach();
}

所以,我们来到了这里。你会注意到我注释掉了将DataContext附加到组织的部分。如果我附加到组织,我会得到以下异常:

  

NotSupportedException: 尝试附加或添加不是新的实体, 可能已从另一个DataContext中加载。   这是不支持的。

嗯,那样行不通。让我试试附加(即注释掉关于附加到组织的那行代码)。

  

DuplicateKeyException: 无法添加具有已经使用的密钥的实体。

什么?我只能假设这是在尝试插入一个新的组织,这显然是错误的。

那么,怎么办呢?应该怎么做?正确的方法是什么?看起来L2S使这比它本应该更难...

编辑: 我刚刚注意到,如果我尝试查看待处理的更改集 (dataContext.GetChangeSet()),我会得到我之前描述的相同的NotSupportedException!L2S到底是怎么回事?!


只是好奇,为什么你在数据库中不使用外键来表示对象模型中的关系呢? - Ian P
嗯,我在数据库中...不确定为什么它们没有在图表中显示(即期望有一个灰色的键),但我认为从一个框到另一个框的小箭头表示已经建立了外键关系。 - Brad Heller
你正在使用 L2S,但并不是按照它应该被使用的方式。通常情况下,Attach 几乎从不被调用。你为什么要首先使用 repo 呢?它似乎只会限制你的操作,并没有任何回报。在 L2S 中,DataContext 就是 repository。 - usr
3个回答

8

在幕后,它可能不完全按照这种方式工作,但是这是我构想它的方式:当您从DataContext中召唤对象时,Linq所做的事情之一就是跟踪这个对象随时间的变化,以便知道如果您提交更改,则保存什么。如果您失去了此原始数据上下文,从中召唤的Linq对象将不具有其从DB召唤时发生了什么变化的历史记录。

例如:

DbDataContext db = new DbDataContext();
User u = db.Users.Single( u => u.Id == HARD_CODED_GUID );
u.FirstName = "Foo";
db.SubmitChanges();

在这种情况下,由于用户对象是从数据上下文中召唤出来的,它会跟踪所有对“u”的更改,并知道如何将这些更改提交到数据库。
在您的示例中,您有一个已经持久化在某个地方的用户对象(或者从其他地方传递而不具有其原始数据上下文)。在这种情况下,您必须将其附加到新的数据上下文中。
User u; // User object passed in from somewhere else
DbDataContext db = new DbDataContext();
u.FirstName = "Foo";
DbDataContext.Users.Attach( u );
db.SubmitChanges();

因为用户和组织之间的关系只是在你的数据模型中使用GUID(OrganizationId)表示,所以你只需要附加用户对象即可。

我不确定你的脚手架代码,但可能类似于这样:

    private const Guid DEFAULT_ORG = new Guid("3cbb9255-1083-4fc4-8449-27975cb478a5");
    public void Save(User user)
    {
        if (!DataContext.Users.Contains(user))
        {
            user.Id = Guid.NewGuid();
            user.CreatedDate = DateTime.Now;
            user.Disabled = false;
            user.OrganizationId = DEFAULT_ORG; // make the foreign key connection just
                                               // via a GUID, not by assigning an
                                               // Organization object

            DataContext.Users.InsertOnSubmit(user);
        }
        else
        {
            DataContext.Users.Attach(user);
        }

        DataContext.SubmitChanges();

    }

你需要使用重载Attach(user,true)来告诉Linq2SQL模型已经从原始状态改变。第三个重载允许你传入原始和修改后的对象,它会在后台重新映射。太棒了。我仍然不明白Dave Markle如何使用InsertOnSubmit来更新? - Piotr Kula

1
我使用了一个拥有400多列的大表格。不可能把所有内容都映射和测试!
从数据库获取原始对象,并将其与修改后的对象附加在一起。只需确保返回的对象已完全填充,否则它将用空白覆盖数据库中的内容!
或者您可以将原始GET复制到内存中,并对MOdel进行适当的复制(而不仅仅是引用),然后将原始对象和更改后的对象一起传入,而不是像我在示例中所做的重新获取。这只是说明它如何工作的一个示例。
public void Save(User user)
{

    if (!DataContext.Users.Contains(user))
    {
        user.Id = Guid.NewGuid();
        user.CreatedDate = DateTime.Now;
        user.Disabled = false;
        user.OrganizationId = DEFAULT_ORG; // make the foreign key connection just
                                           // via a GUID, not by assigning an
                                           // Organization object

        DataContext.Users.InsertOnSubmit(user);
    }
    else
    {
        var UserDB = DataContext.Users.FirstOrDefault(db => db.id == user.id); //Costs an extra call but its worth it if oyu have 400 columns!
        DataContext.Users.Attach(user, userDB); //Just maps all the changes on the flu
    }

    DataContext.SubmitChanges();

}

1

因此,“附加”是在您从数据库中获取现有对象,将其分离(例如通过在其他地方进行编组)并希望将其放回数据库时使用的。而不是调用.Attach(),请改为调用.InsertOnSubmit()。从概念上讲,您已经接近了目标,只是使用了错误的方法来实现您想要的结果。


谢谢回复...那么在我现有的父子关系中,如果我要插入一个新的子对象(如上图中的用户),我需要先将其附加到父对象吗? - Brad Heller
1
让我这样说吧。我已经写了大约5个Linq to SQL应用程序,从来没有使用过Attach()方法。如果相关字段上的ID相符,您实际上甚至不需要分配两个对象之间的关系。当您调用SubmitChanges()时,L2SQL将理解您的意思。 - Dave Markle
是的。这样插入了一个带有新ID的新行到数据库中。不确定您如何使用ID更新现有行?我从DBContext获取了对象,更改了一些字段,将对象发送回进行保存。除非删除旧行,否则Attach或InsertOnSubmit()都不起作用。 - Piotr Kula

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接