英文:
Insert a List of complex objects without inserting navigational properties in Entity Framework
问题
以下是翻译好的部分:
给定以下模型类:
public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public List<Rule> Rules { get; set; }
}
public class Rule
{
public int RuleId { get; set; }
public string RuleName { get; set; }
public Owner Owner { get; set; }
public Manager Manager { get; set; }
}
public class Owner
{
public int OwnerId { get; set; }
public string Code { get; set; }
}
public class Manager
{
public int ManagerId { get; set; }
public string Code { get; set; }
}
在使用 Entity Framework 6 时,我从 API 调用中接收到一组需要添加的用户列表。
在映射到 dbContext
类型之后,我尝试通过 context.Users.Add(users)
来添加用户列表,但我实际上不想添加导航属性,即 Owner 和 Manager,我只对它们进行外键引用,但 EF 试图将它们添加到数据库中,即使它们已经存在,然后引发异常。
我通过创建我需要添加的实例并忽略导航属性来使其工作。但是,我想在一个 SaveChanges()
中执行插入操作,同时忽略 Owner 和 Manager 属性,只保留它们的 IDs,用作 Rules 表中的外键值。
我假设有一种更好的方法来解决这个问题,而不是递归地创建要添加的项目。
英文:
Given the following model classes:
Public class User
{
public int UserId { get; set; }
public string Name { get; set; }
public List<Rule> Rules { get; set; }
}
public class Rule
{
public int RuleId { get; set; }
public string RuleName { get; set; }
public Owner Owner { get; set; }
public Manager Manager { get; set; }
}
public class Owner
{
public int OwnerId { get; set; }
public string Code { get; set; }
}
public class Manager
{
public int ManagerId { get; set; }
public string Code { get; set; }
}
With Entity Framework 6, I receive a list of users from an API call which need to be added.
After mapping into the dbContext
type, I attempt to add the list of users via context.Users.Add(users)
essentially I do not want to add the navigation properties i.e Owner and manager, I'm only interested in these for Foreign key reference purposes but EF attempts to add these into the database even though they already exist and then throws an exception.
I got this working by newing up instances of what I need to add and then ignore the navigational properties. However what I'd like is to do the insert in one SaveChanges()
whilst ignoring the Owner and Manager properties except their ids which serves and a FK value in the Rules table.
I assume there is a much better way to work around this than the recursively new up items to be added.
答案1
得分: 3
参考资料和跟踪是在处理EF实体时非常重要的因素。当您向EF添加一个包含对此DbContext实例未跟踪的实体的导航属性的新实体时,EF会将这些实体视为需要添加的新实体,无论DB中是否存在记录。当您假设它应该能够识别您意图引用现有的Manager或Owner时,这会引起问题。
所以假设您想要插入一个新用户以及一组新的规则。每个规则将引用现有的Owner和Manager。我们传入的内容可以是Owner和Manager实体,或者我们只需传递一个具有OwnerId和ManagerId的Rule的DTO。
选项1:使用Owner和Manager "实体"。我们希望EF能够识别这些实体指向现有行。做到这一点的简单方法是将它们"Attach"。然而,有一个警告,即DbContext可能已经在跟踪具有这些ID的实例,因此我们需要检查本地跟踪缓存,如果找到具有相同ID的实例,则在保存新实体之前替换引用:
foreach (var rule in user.Rules)
{
var existingOwner = _context.Owners.Local
.FirstOrDefault(x => x.OwnerId == rule.Owner.OwnerId);
if (existingOwner != null)
rule.Owner = existingOwner;
else
_context.Attach(rule.Owner);
var existingManager = _context.Managers.Local
.FirstOrDefault(x => x.ManagerId == rule.Manager.ManagerId);
if (existingManager != null)
rule.Manager = existingManager;
else
_context.Attach(rule.Manager);
}
_context.Users.Add(user);
_context.SaveChanges();
这会检查上下文的"Local"缓存以查找跟踪的引用。如果找到一个,我们会在保存之前替换新用户规则上的引用。如果没有找到,我们可以安全地附加它(假设Owner或Manager实际上存在于DB中)。这种方法的缺点是,如果我们传递了在DB中不存在的Owner或Manager,将在"SaveChanges()"上引发异常。如果Owner或Manager的实体不是"完整"的,而我们附加了它,也会出现问题,例如,如果Owner具有未填充的其他属性或导航属性,因为我们只需要ID。当我们"Attach"它时,在该DbContext的请求的其余部分中,获取该Owner的任何调用都会返回该不完整的实例。如果您曾经附加了存根、不完整的实体,或者包含可能不准确/不最新数据的实体,那么在保存更改后分离任何这些存根是个好主意。这样,如果DbContext要求该Owner,它将转到DB而不是提供不完整的存根。
选项2:使用OwnerId和ManagerId。在这种情况下,如果我们传递的是DTO或类似的内容而不是实体,并构造要添加的新用户和规则,我们可以从DbContext中读取我们的Owner和Manager。在这种情况下,我们传递一个包含用户和规则的DTO或ViewModel,其中规则只有用于引用现有Owner和Manager的ID。
var user = new User(userDTO); // 从DTO复制值。
var ownerIds = userDTO.Rules.Select(x => x.OwnerId).ToList();
var managerIds = userDTO.Rules.Select(x => x.ManagerId).ToList();
var owners = _context.Owners
.Where(x => ownerIds.Contains(x.OwnerId))
.ToList();
var managers = _context.Managers
.Where(x => managerIds.Contains(x.ManagerId))
.ToList();
foreach (var ruleDTO in userDTO.Rules)
{
var owner = owners.First(x => x.OwnerId == ruleDTO.OwnerId);
var manager = managers.First(x => x.ManagerId == ruleDTO.ManagerId);
user.Rules.Add(new Rule(ruleDTO, owner, manager));
}
_context.Users.Add(user);
_context.SaveChanges();
此代码接受包含新用户和规则字段的DTO,其中规则只包含引用现有Owner和Manager的ID。我们获取这些ID,以便每个集合的每个RuleDTO都会定位匹配的已跟踪Owner和Manager引用,以关联到我们的新Rule。在这里对"First"的调用将在我们意外得到不存在于DB中的OwnerId或ManagerId时引发异常。在这种情况下,异常将在有意义的位置发生,以确定缺少什么,而不是在"SaveChanges"上发生。在这种情况下,实体的构造函数只接受DTO以复制值。您可以手动内联填充它们,或者使用类似Automapper的工具从DTO构造它们。在Rule构造函数的情况下,我们传递DTO的值以及找到的Owner和Manager的引用。当DbContext将User和Rules添加为新实体时,这些规则将引用已知的已跟踪Owner和Manager。
英文:
References and tracking are important factors when working with EF entities. When you add a new entity to EF that contains navigation properties to entities that this DbContext instance is not tracking, it will treat these entities as new ones that need to be added whether a record exists in the DB or not. This causes problems when you assume it should be able to work out that you mean to reference an existing Manager or Owner.
So say you want to insert a new user with a set of new Rules. Each rule will reference an existing Owner and Manager. What we pass in can either be Owner and Manager entities, or we could just pass a DTO for the Rule that has an OwnerId and ManagerId.
Option 1: With an Owner and Manager "entity". We want EF to recognize these entities as pointing to existing rows. The simple way to do this is to Attach
them. However there is a caveat that the DbContext might already be tracking an instance with those IDs, so we need to check the local tracking cache and if we find an instance with the same ID, replace the reference before we save our new entitiy:
foreach (var rule in user.Rules)
{
var existingOwner = _context.Owners.Local
.FirstOrDefault(x => x.OwnerId == rule.Owner.OwnerId);
if (existingOwner != null)
rule.Owner = existingOwner;
else
_context.Attach(rule.Owner);
var existingManager = _context.Managers.Local
.FirstOrDefault(x => x.ManagerId == rule.Manager.ManagerId);
if (existingManager != null)
rule.Manager = existingManager;
else
_context.Attach(rule.Manager);
}
_context.Users.Add(user);
_context.SaveChanges();
This checks the Context's Local
caches for tracked references. If we find one, we replace the reference on our new User Rule before saving. If we don't, we can safely attach it. (Assuming that owner or manager actually exist in the DB) The downsides of this approach is what happens if we pass an Owner or Manager that doesn't exist in the DB. An exception would be raised on SaveChanges()
. This option also poses issues if the entity for the Owner or Manager wasn't "complete" and we attach it. For instance if an Owner has additional properties or navigation properties that weren't populated since we only really needed the ID. When we Attach
it, for the remainder of the request on that DbContext, any calls to get that Owner would return that incomplete instance. If you are ever attaching stubbed, incomplete entities, or ones containing data that might not be accurate/up-to-date then after saving your changes it is a good idea to detach any of those stubs. That way if the DbContext
is asked for that Owner, it would go to the DB rather than serve up an incomplete stub.
Option 2: With an OwnerId and ManagerId. In this case if we are passing a DTO or such rather than entities and constructing a new User and Rules to add, we can read our Owners and Managers from the DbContext. In this case we pass around a DTO or ViewModel for the user & rules where the rules just have IDs for the Manager and Owner.
var user = new User(userDTO); // copy values from DTO.
var ownerIds = userDTO.Rules.Select(x => x.OwnerId).ToList();
var managerIds = userDTO.Rules.Select(x => x.ManagerId).ToList();
var owners = _context.Owners
.Where(x => ownerIds.Contains(x.OwnerId))
.ToList();
var managers = _context.Managers
.Where(x => managerIds.Contains(x.ManagerId))
.ToList();
foreach (var ruleDTO in userDTO.Rules)
{
var owner = owners.First(x => x.OwnerId == ruleDTO.OwnerId);
var manager = managers.First(x => x.ManagerId == ruleDTO.ManagerId);
user.Rules.Add(new Rule(ruleDTO, owner, manager));
}
_context.Users.Add(user);
_context.SaveChanges();
This code accepts a DTO containing fields for the new User and Rules which the rule contains just the IDs for the existing Owner and Manager to reference. We get those Ids so that we can read all of the referenced Owners and Managers in one DB call for each set, then as we go through each RuleDTO to add a new Rule to our new User, we locate the matching, tracked Owner and Manager reference to associate to our new Rule. The calls to First
here will throw an exception if we happened to get an OwnerId or ManagerId that didn't exist in the DB. The exception in that case will happen in a meaningful spot to identify what is missing rather than on SaveChanges
. The constructors for the entities in this case are just accepting the DTO to copy values from. You could manually fill them out in-line or use something like Automapper to construct them from the DTO. In the case of the Rule constructor we pass the DTO for values for the rule and the references to the found Owner and Manager. When The DbContext adds the User and Rules as new entities, these rules will have references to known, tracked Owners and Managers.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论