【问题标题】:EF 4.3 saving One to Many causing errors or duplicate rowsEF 4.3 保存一对多导致错误或重复行
【发布时间】:2012-03-14 18:55:40
【问题描述】:

真的希望有人能提供帮助,因为过去两个晚上我一直在梳理头发,而且速度不快!并且拼命地试图让一个家庭项目进行下去,而我这样做是为了不赚钱,并且在我试图用它来学习一些新技术时,它迅速占用了我的时间。

这似乎是一个常见问题,因为在 stackoverflow 等上发现了一些类似的 ish 帖子,但现在似乎已经引导我找到了一个可行的解决方案。

我得到错误或重复的行进入数据库,我的简化模型是一对多的关系。但我试图将 WebsiteUserSession 对象存储在用户会话中,以保存每次页面加载时的数据库命中。

型号:

public class WebsitePageTracking
{
    [Key]
    public long Id { get; set; }
    public virtual WebsiteUserSession WebsiteUserSession { get; set; }
    public DateTime DateTime { get; set; }
    public string TrackUrl { get; set; }
}

public class WebsiteUserSession
{
    [Key]
    public long Id { get; set; }
    public Guid Guid { get; set; }
    public string IpAddress { get; set; }
    public DateTime DateTime_Created { get; set; }
    public DateTime DateTime_LastSessionStart { get; set; }

    public virtual ICollection<WebsitePageTracking> WebsitePageTracking { get; set; }
}

然后我的代码如下,稍微复杂一点,但希望能明白。请注意,数据上下文存储在 HttpContext.Items 中,这是一个建议。

/// <summary>
/// public method for fetching and persisting website user session
/// </summary>
/// <returns></returns>
public static WebsiteUserSession GetWebsiteUserSession()
{
    // set initial variables
    Guid? websiteUserGuid = null;
    string websiteUserIpAddress = HttpContext.Current.Request.UserHostAddress;
    WebsiteUserSession websiteUserSession = null;            

    // first try and get from the session
    if (HttpContext.Current.Session["WebsiteUserSession"] != null)
    {
        // load the website user session, and fetch out the guid
        websiteUserSession = HttpContext.Current.Session["WebsiteUserSession"] as WebsiteUserSession;
        websiteUserGuid = websiteUserSession.Guid;

        // check to see if ip has changed, if so drop website user session
        if (websiteUserSession.IpAddress != websiteUserIpAddress)
            websiteUserSession = null;
    }
    else
    {
        // nothing so create brand new guid
        websiteUserGuid = Guid.NewGuid();
    }

    // if we don't have one which came from the session then get the website user session using guid/ip
    if (websiteUserSession == null)
        websiteUserSession = GetWebsiteUserSession((Guid)websiteUserGuid, websiteUserIpAddress);

    // if not stored in the session add that now
    if (HttpContext.Current.Session["WebsiteUserSession"] == null)
        HttpContext.Current.Session.Add("WebsiteUserSession", websiteUserSession);

    // return back website user session object
    return websiteUserSession;
}

/// <summary>
/// gets or creates a website user session record in the database
/// </summary>
/// <param name="guid">guid generated for the website user</param>
/// <param name="ipAddress">public ip address of website user</param>
/// <returns></returns>
private static WebsiteUserSession GetWebsiteUserSession(Guid guid, string ipAddress)
{
    DataContext dataContext = HttpContext.Current.Items["DataContext"] as DataContext;

    // try and fetch a user session from database
    WebsiteUserSession websiteUserSession = dataContext
        .WebsiteUserSessions
        .Where(x => x.Guid == guid && x.IpAddress == ipAddress)
        .SingleOrDefault();

    if (websiteUserSession != null)
    {
        // if found update last session time
        websiteUserSession.DateTime_LastSessionStart = DateTime.Now;
    }
    else
    {
        // create a new website user session
        websiteUserSession = new WebsiteUserSession();
        websiteUserSession.Guid = guid;
        websiteUserSession.IpAddress = ipAddress;
        websiteUserSession.DateTime_Created = websiteUserSession.DateTime_LastSessionStart = DateTime.Now;
        dataContext.WebsiteUserSessions.Add(websiteUserSession);
    }

    // persist changes
    dataContext.SaveChanges();                

    return websiteUserSession;
}

/// <summary>
/// log a stock page view to database
/// </summary>
/// <param name="stockId">id of stock being viewed</param>
public static void LogStockPageView(int stockId)
{
    DataContext dataContext = HttpContext.Current.Items["DataContext"] as DataContext;

    WebsiteUserSession websiteUserSession = GetWebsiteUserSession();

    //dataContext.WebsiteUserSessions.Attach(websiteUserSession);
    //dataContext.Entry(websiteUserSession).State = EntityState.Unchanged;

    WebsitePageTracking pageTrack = new WebsitePageTracking();
    pageTrack.WebsiteUserSession = websiteUserSession;
    pageTrack.DateTime = DateTime.Now;
    pageTrack.TrackUrl = HttpContext.Current.Request.Url.ToString();

    dataContext.Entry(client).State = EntityState.Unchanged;
    dataContext.Entry(website).State = EntityState.Unchanged;
    dataContext.Entry(websiteUserSession).State = EntityState.Unchanged;

    //dataContext.Entry(websiteUserSession.WebsitePageTracking).State = EntityState.Detached;

    //dataContext.WebsiteUserSessions.Attach(pageTrack.WebsiteUserSession);    

    dataContext.WebsitePageTracking.Add(pageTrack);

    //dataContext.Entry(pageTrack.WebsiteUserSession).State = EntityState.Unchanged;

    dataContext.SaveChanges();
}

基本上应该发生的情况是有人浏览了一个页面,调用 LogStockPageView() 进而从会话、数据库中获取一个 WebsiteUserSession 实体或创建一个新实体。然后将 WebsitePageTracking 实体添加到与 WebsiteUserSession 关联的数据库中。

你可以看到所有试图让它工作的注释行,但我要么得到错误,要么插入了很多重复的行:(

这样的错误之一是“ObjectStateManager 中已存在具有相同键的对象。ObjectStateManager 无法跟踪具有相同键的多个对象。”。

请有人看看我哪里出错了,甚至可以提出更好的解决方案吗?真的很想与 EF 合作,但很挣扎:(

谢谢,卡尔

【问题讨论】:

    标签: c# entity-framework entity-framework-4 ado.net


    【解决方案1】:

    由于 EF 的工作方式,这个简单的代码存在很多问题。第一个问题是使用会话。 Session 将为您节省数据库查询,但您必须向您的应用程序添加许多特殊代码才能使其在所有预期场景中工作。我将仅在我存储在会话中的实体从未用于数据修改(EF 不再使用它)的情况下使用会话。在这种情况下,您可以使用 FK 属性来避免很多麻烦。

    第一个问题出现在GetWebsiteUserSession 中,您从该方法返回附加实体。如果您还启用了延迟加载,您可以确定在下一个请求中添加跟踪将导致ObjectDisposedException。原因是代理的附加实体保持对当前请求结束时设置的当前上下文的内部引用。当实体尝试加载导航属性时,它使用该引用。如果您关闭代理创建/延迟加载,它可以工作,但更好的解决方案是在从方法返回实体之前分离实体,因为它允许您的其余代码仅在单一场景下工作:

    dataContext.Entry(websiteUserSession).State = EntityState.Detached;
    

    请注意,分离将清空您的 WebSitePageTracking 集合。如果要分离实体图,则必须创建深度克隆(序列化/反序列化)。

    第二个问题是整个LogStockPageView。如果您当前的会话实例已附加到上下文,则无需再次附加它或更改其状态,但在实体分离的情况下您必须这样做。试试这个:

    public static void LogStockPageView(int stockId)
    {
        DataContext dataContext = HttpContext.Current.Items["DataContext"] as DataContext;
    
        WebsiteUserSession websiteUserSession = GetWebsiteUserSession();
    
        dataContext.WebsiteUserSessions.Attach(websiteUserSession);
    
        WebsitePageTracking pageTrack = new WebsitePageTracking();
        pageTrack.DateTime = DateTime.Now;
        pageTrack.TrackUrl = HttpContext.Current.Request.Url.ToString();
    
        dataContext.WebsitePageTracking.Add(pageTrack);
    
        // Make connection after both session and track are correctly configured and attached
        pageTrack.WebsiteUserSession = websiteUserSession;
    
        dataContext.SaveChanges();
    
        // again detach session 
        dataContext.Entry(websiteUserSession).State = EntityState.Detached;
    }
    

    您的示例代码还包含对其他实体的多个引用。那些有同样的问题。如果您将它们存储在会话中并且它们以某种方式与您的保存代码相关,那么您也必须处理它们。

    如果您希望您的websiteUserSession 在会话中保持完整跟踪,它将变得更加复杂,因为您必须使用实体图克隆而不是分离。

    【讨论】:

    • 哇,这很准确,而且解释得很好……抱歉,示例代码引用了其他实体,我的错误是我简化了它,但忘了删掉一些内容。无论如何,我做了你建议的改变,它的工作原理!现在事情变得更加清晰了......新西兰的午夜已经过去了,所以应该去睡觉了,但有兴趣尝试你的建议......明天将再次更详细地讨论它们并继续我的项目:)谢谢一百万!!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-01-26
    • 2020-05-21
    • 2020-03-31
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多