首页 > 解决方案 > SaveChanges 上的 EF Core 不断向第二个表添加记录

问题描述

我正在创建一个轮胎存储调度程序,并将所有存储信息存储在数据库中。在完成预订过程时,它不仅将记录添加到tblSchedule表中,还添加了客户数据的副本并将其存储到tblStorageData表中,只是使用设置为标识列的不同 ID。

这是我的计划模型:

[Table("tblSchedule")]
public class Schedule
{
        [Required]
        public int ID { get; set; }
        
        [Required]
        public DateTime ScheduleDateTime { get; set; }

        public bool Confirmed { get; set; } = false;

        public StorageData StorageDataInfo { get; set; }
}

这是我的StorageData模型:

[Table("tblStorageData")]
public class StorageData
{
        [Required]
        public int ID { get; set; }

        [Required(ErrorMessage = "License plate is required to perform search.")]
        [StringLength(20)]
        public string LicensePlate { get; set; }

        [StringLength(100)]
        public string Name { get; set; }

        [StringLength(10)]
        public string BIN { get; set; }
}

这是我AddSchedule获取信息并将其添加到数据库的方法:

public int AddSchedule(Schedule model)
{
    if (CheckSchedule(model.ScheduleDateTime) >= 4)
    {
        return 0;
    }
    else
    {
        Schedule sch = new Schedule()
                {
                    ScheduleDateTime = model.ScheduleDateTime,
                    StorageDataInfo = model.StorageDataInfo
                };

        _context.Schedule.Add(sch);
        _context.SaveChanges();

        return 1;
    }
}

这是我的日程控制器:

[HttpGet]
public IActionResult Book(string id)
{
    string session = HttpContext.Session.GetString("CustomerVerify");
    string customerBooking = HttpContext.Session.GetString("CustomerBooking");

    if (session == null)
    {
        return RedirectToAction("Index", "Customer");
    }

    if (string.IsNullOrEmpty(id))
    {
        return RedirectToAction("Month", "Schedule");
    }

    DateTime dt = DateTime.ParseExact(id, "yyyyMMddHHmmtt", CultureInfo.InvariantCulture);
    FullBookingViewModel fbvm = JsonConvert.DeserializeObject<FullBookingViewModel>(customerBooking);

    Schedule sch = new Schedule();
    sch.ScheduleDateTime = dt;
    sch.StorageDataInfo = fbvm.StorageDataInfo;

    return View(sch);
}

[HttpPost]
public IActionResult Book(Schedule model)
{
     Schedule sch = new Schedule()
            {
                StorageDataInfo = model.StorageDataInfo,
                ScheduleDateTime = model.ScheduleDateTime,
            };

     int result = _scheduleData.AddSchedule(model);

     if (result == 0)
     {
         return RedirectToAction("Day", "Schedule");
     }
     else
     {
         return RedirectToAction("Month", "Schedule");
     }
}

这是 cshtml 视图:

@model Schedule

<h1>Schedule.Book</h1>

<p>Please review the following appointment before confirming:</p>

@*@Model.ScheduleDateTime.ToString("yyyy/MM/dd hh:mm tt")
<br />
@Model.StorageDataInfo.Name @Model.StorageDataInfo.LicensePlate @Model.StorageDataInfo.BIN
<br />*@

<form method="post">
    <label asp-for="@Model.ScheduleDateTime">@Model.ScheduleDateTime</label><br />
    @Html.HiddenFor(m => m.ScheduleDateTime)

    <label asp-for="@Model.StorageDataInfo.Name">@Model.StorageDataInfo.Name</label>
    <label asp-for="@Model.StorageDataInfo.LicensePlate">@Model.StorageDataInfo.LicensePlate</label>
    <label asp-for="@Model.StorageDataInfo.BIN">@Model.StorageDataInfo.BIN</label><br />
    @Html.HiddenFor(m => m.StorageDataInfo.Name)
    @Html.HiddenFor(m => m.StorageDataInfo.LicensePlate)
    @Html.HiddenFor(m => m.StorageDataInfo.BIN)

    <button type="submit">Confirm</button>
    <button>Cancel</button>
    <button>Select Different Month</button>
    <button>Select Different Day</button>
    <button>Select Different Time</button>
</form>

这是 tblSchedule 表的屏幕截图,其中包含正确添加的时间,但 StorageDataInfoID 应该始终相同。

这是 tblStorageData 表的屏幕截图,其中还添加了重复信息。

我不知道为什么它应该只添加到一个引用 tblStorageData 表的 ID 而不是复制客户信息的表中时添加到两个表中。

标签: entity-frameworkasp.net-core-mvc

解决方案


这样做是因为当 EF 使用导航属性时,引用就是一切。如果 DbContext 没有跟踪实体的实例,它将将该实体视为新实例,无论该实体是否具有 ID。

最初的解决方案是告诉 DbContext 跟踪提供的 StorageData 实例,并假设记录存在:

public int AddSchedule(Schedule model)
{
    if (CheckSchedule(model.ScheduleDateTime) >= 4)
        return 0;

    _context.Attach(model.StorageDataInfo);

    _context.Schedule.Add(model);
    _context.SaveChanges();
    return 1;
}

当服务器方法接受“实体”作为参数时,您实际上并没有返回一个实体,而是一个由客户端提供的反序列化数据填充的实体类。您不需要每次都创建新实例,但是在执行更新之类的操作以验证从客户端传入的任何数据时,您还需要小心,因为它可能已被篡改。在 DbContext 已经在跟踪相同 ID 的另一个实例的情况下,附加实体仍然会导致操作错误。上述方法的更安全版本是:

public int AddSchedule(Schedule model)
{
    if (CheckSchedule(model.ScheduleDateTime) >= 4)
        return 0;

    var existingStorageData = _context.StorageData.Local
        .SingleOrDefault(x => x.ID == model.StorageDataInfo.ID);
    if(storageData == null)
        _context.Attach(model.StorageDataInfo);
    else
        model.StorageDataInfo = existingStorageData

    _context.Schedule.Add(model);
    _context.SaveChanges();
    return 1;
}

这样做是检查 DbContext 本地缓存(不访问数据库)以返回任何具有该 ID 的跟踪实例。如果没有返回实例,我们可以附加传入的副本,否则我们将引用替换为跟踪的实例。


推荐阅读