英文:
Initialize a relational property
问题
我想知道如何在EntityFramework中初始化类似这样的属性:
public class Activity : Model
{
public User User { get; set; }
}
User类如下:
public class User : Model
{
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
}
谢谢!
英文:
I wonder how to initialize a property like this in EntityFramework:
public class Activity : Model
{
public User User { get; set; }
}
User looks like:
public class User : Model
{
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
}
Thank you!
答案1
得分: 2
如果你想建立一对一的关系;你可以做以下定义。
```csharp
public class Activity : Model
{
public int UserId { get; set; }
public User User { get; set; }
}
public class User : Model
{
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public int ActivityId { get; set; }
public Activity Activity { get; set; }
}
或者,如果你想建立一对多的关系;你可以做以下定义。
public class Activity : Model
{
public int UserId { get; set; }
public User User { get; set; }
}
public class User : Model
{
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public ICollection<Activity> Activities { get; set; }
}
你可以选择适合数据模型的关系类型。
我还分享了示例上下文定义:
public class SampleContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Activity> Activities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder )
{
//示例是针对 SqlServer 的。
optionsBuilder.UseSqlServer("这里放连接字符串");
}
}
我还分享了迁移的做法。
https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli
<details>
<summary>英文:</summary>
If you want to establish a one-to-one relationship; You can make the following definition.
public class Activity : Model
{
public int UserId { get; set; }
public User User { get; set; }
}
public class User : Model
{
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public int ActivityId { get; set; }
public Activity Activity { get; set; }
}
Or, if you want to establish a One-to-many relationship; You can make the following definition.
public class Activity : Model
{
public int UserId { get; set; }
public User User { get; set; }
}
public class User : Model
{
public string Email { get; set; }
public string Password { get; set; }
public string Name { get; set; }
public ICollection<Activity> Activities { get; set; }
}
You can choose the type of relationship that fits the data model.
I also share the example context definition:
public class SampleContext : DbContext
{
public DbSet<User> Users { get; set; }
public DbSet<Activity> Activities { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder )
{
//Example is for SqlServer.
optionsBuilder.UseSqlServer("ConnectionString here");
}
}
I also share how the migration is done.
https://learn.microsoft.com/en-us/ef/core/managing-schemas/migrations/?tabs=dotnet-core-cli
</details>
# 答案2
**得分**: 0
从问题和评论中听起来,您想要一种正确的面向对象方法来初始化具有用户的新活动,其中您将具有如下构造函数:
```csharp
public class Activity : Model
{
// Activity members...
public User User { get; set; }
public Activity(int id, /* required fields */, User user) : base (id)
{
// set required fields.
User = user;
}
}
这本身在EF方面不会完全有效,因为在幕后,EF将希望构造实体实例和代理,并且它不一定会在每次读取Activity时都要加载User和任何其他相关类。
实体类的简单用例是,在构造活动时只需使用具有默认公共构造函数的公共setter。这涉及到解析来自您将用于添加新Activity的DbContext的任何现有引用,例如来自User的引用:
var user = _context.Users.Single(x => x.Id == userId);
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
Web应用程序中的常见问题是将实体发送到视图,然后将视图数据反序列化回实体,并期望将这些实体关联到正在创建的新实体。例如,POST控制器方法:
[HttpPost]
public ActionResult Create(/*required fields*/, User user)
{
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// ...
}
这将不会按照您的期望方式工作,因为传入的User是不由与请求关联的DbContext实例知道的。_context将把该User实例视为新实体,并在持久化新Activity时尝试将其添加到数据库,这将不可避免地导致错误和/或意外行为。虽然您会在网上看到许多示例,其中实体在视图之间传递,但我强烈建议不要陷入这种陷阱,要么习惯于传递简单的视图模型,要么只传递必要的字段。在上面的示例中,我们不应该尝试传递整个User实体,我们不需要它,也无法可靠地使用它。我们只需要User Id:
[HttpPost]
public ActionResult Create(/*required fields*/, int userId)
{
var user = _context.Users.Single(x => x.Id == userId);
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// ...
}
反对这种方法的典型论点是试图避免访问数据库以获取用户。然而,对立论是,这有助于确保提供的引用仍然有效。在我们有许多关联要处理且不一定想要访问数据库的情况下,我们可以合理信任传入的数据时,可以使用快捷方式:
[HttpPost]
public ActionResult Create(/*required fields*/, int userId)
{
var user = _context.Users.Local.FirstOrDefault(x => x.Id == userId);
if (user == null)
{
user = new User { Id = userId };
_context.Attach(user);
}
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// ...
}
在这里,我们检查DbContext的本地缓存是否存在给定用户。这不会访问数据库,而只是检查DbContext是否在跟踪实例。在这个简单的示例中,这个步骤可以省略,因为DbContext绝对不会跟踪任何内容,它是一个全新的上下文。您希望执行此操作的地方是在可能会为多个活动调用的方法中,或者在处理对同一记录的多个引用的可能性时。 (例如,可能每个引用相同的User ID的子实体集合)我包括这个步骤,因为当人们习惯于只创建存根或附加发送的实体并且可以正常工作时,他们在碰到DbContext恰好已经跟踪该ID的实例的情况时,可能会遇到看似随机的运行时错误。作为一个一般规则,在附加实体之前,您应该检查本地缓存中是否存在现有的跟踪引用。
如果没有跟踪引用,我们创建一个存根,将其附加到DbContext,以便它将其视为现有行,并将存根关联到新Activity并保存。这节省了从数据库加载引用的时间,但是这里有一个重要的警告要考虑。用户及其关联的Activity的数据不完整。如果我们只是想将活动保存到数据库中,然后不做其他操作,这种优化就没问题。我们不想将其发送回视图,也不想将其传递给期望完整Activity和关联User的另一个方法。例如,POST控制器方法:
user = new User { Id = userId };
_context.Attach(user);
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// somewhere later in the request...
var aUser = _context.Users.Single(x => x.Id == userId);
从DbContext获取相同的用户"aUser"的结果将返回您那个存根引用,即使该语句将执行一个查询到数据库的操作。附加存根实体时的最后一个安全步骤是在保存后确保将其分离。这意味着要跟踪是否使用了存根:
[HttpPost]
public ActionResult Create(/*required fields*/, int userId)
{
var user = _context.Users.Local.FirstOrDefault(x => x.Id == userId);
bool useUserStub = user == null;
if (
<details>
<summary>英文:</summary>
From the question and comments it sounds like you want a proper OO approach to initializing a new Activity with a User where you would have a constructor like:
public class Activity : Model
{
// Activity members...
public User User { get; set; }
public Activity(int id, /* required fields */, User user) : base (id)
{
// set required fields.
User = user;
}
}
That alone won't really work with EF because behind the scenes EF will want to construct entity instances and proxies and it won't necessarily always want to load a User and any other related class every time it reads an Activity.
The simple use case for entity classes is that when constructing an activity is to just use public setters with a default public constructor. This involves resolving any existing references like the user from the DbContext you will be using to add your new Activity:
var user = _context.Users.Single(x => x.Id == userId);
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
A common problem in web applications is sending entities to a view then deserializing view data back into entities and expecting to associate those to a new entity being created. For example a POST controller method:
[HttpPost]
public ActionResult Create(/*required fields*/, User user)
{
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// ...
}
This will not work the way you expect because the User that gets passed in is *not* known by the DbContext instance associated to the Request. _context will treat that User instance as a new entity and try to add it to the database when it persists the new Activity. This will invariably lead to errors and/or unintended behaviour. While you will see many examples online where Entities are passed too and from views I highly recommend not falling into this trap and either be accustomed to passing simple view models, or just passing the necessary fields. In the above example we should not attempt to pass an entire User entity, we don't need it and cannot reliably use it. All we need is the User Id:
[HttpPost]
public ActionResult Create(/*required fields*/, int userId)
{
var user = _context.Users.Single(x => x.Id == userId);
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// ...
}
The typical argument against an approach like this is trying to avoid going to the DB to fetch a user. However, the counter-argument is that this serves to ensure that the provided references are still valid. In cases where we have many associations to do and don't necessarily want to go to the database and we can reasonably trust the data coming in. (such as an internal line-of-business system used by employees, not the public) then there are shortcuts available:
[HttpPost]
public ActionResult Create(/*required fields*/, int userId)
{
var user = _context.Users.Local.FirstOrDefault(x => x.Id == userId);
if (user == null)
{
user = new User { Id = userId };
_context.Attach(user);
}
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// ...
}
Here we check the DbContext's local cache for the given user. This doesn't go to the database but just checks if the DbContext happens to be tracking an instance. In this simple example this step can be skipped since 100% the DbContext won't be tracking anything, it is a fresh context. Where you would want to do this is in cases where this is in a method that might get called for several activities, or cases where you are dealing with the possibility of several references to the same record. (I.e. a collection of children entities that might each reference the same User ID) I included this step because it trips people up when they get used to just creating stubs or attaching entities sent in and have it work just fine, but then get a seemingly random runtime error when they hit a situation where the DbContext happens to already be tracking an instance for that ID. As a general rule before ever attaching an entity you should check for an existing tracked reference in the local cache.
Where there isn't a tracked reference, we create a stub, attach it to the DbContext so it treats it as an existing row, then associate the stub to the new Activity and Save. This saves time of loading references from the database but there is a big caveat to consider here. The data for the User and it's associated Activity is ***not*** complete. This type of optimization is fine if we just want to get an activity into the database and leave it at that. We don't want to do anything else with the activity. We don't want to send it back to a view, or pass it to another method that expects a complete Activity and associated User. Accessing something like "newActivity.User.Name" for instance would return #null or cause exceptions as the "newActivity.User" is a reference to our stub User. Even if you try to fetch the user from the DbContext later, you will be given that Stub: For instance if you do something like this:
user = new User { Id = userId };
_context.Attach(user);
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
// somewhere later in the request...
var aUser = _context.Users.Single(x => x.Id == userId);
The result of that request to get that same user "aUser" from the DbContext will return you that stub reference, even though that statement will execute a query to the database. A final safety step whenever you attach a stub entity like this is to ensure you detach it after saving. This means keeping track of whether you used a stub or not:
[HttpPost]
public ActionResult Create(/*required fields*/, int userId)
{
var user = _context.Users.Local.FirstOrDefault(x => x.Id == userId);
bool useUserStub = user == null;
if (useUserStub)
{
user = new User { Id = userId };
_context.Attach(user);
}
var newActivity = new Activity
{
//Activity fields...
User = user
};
_context.Activities.Add(newActivity);
if (useUserStub)
_context.Entry(user).State = EntityState.Detached;
// ...
}
Needless to say this starts getting complicated as you add more references with stubs. The simplest thing is to just fetch referenced entities from the database as fetching entities by ID is about as fast an operation as there is, and it provides meaningful validation where something isn't found at the point it is requested rather than a FK constraint error at `SaveChanges()`.
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论