英文:
How to update a total count on parent entity when adding a new child collection entity?
问题
我有一个UserPoints集合,它是User上的一个集合,用于跟踪授予用户的所有积分。当我向User添加一个新的UserPoint时,我希望根据授予的积分数量(例如3、5、1、6等)自动更新User上的TotalPoints。
问题 - 当我添加一个新的UserPoint行时,是否可以在不直接修改User上的值的情况下更新TotalPoints?
例如,User上的TotalPoints现在应该是4。
public class User : IdentityUser<int>
{
public int TotalPoints { get; set; } // 应该是4
public ICollection<UserPoint> UserPoints { get; set; }
}
public class UserPoint : BaseEntity
{
public UserPointType UserPointType { get; set; }
public int Points { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
英文:
I have UserPoints which is a collection on User which keeps track of all points awarded to a User. When I add a new UserPoint to User I want to automatically update TotalPoints on User, based on how many points are awarded (ex. 3, 5, 1, 6, etc).
QUESTION - Is it possible to update TotalPoints without modifying the value directly on User when I add a new UserPoint row?
Ex. TotalPoints on User should now be 4
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
var userPoint = new UserPoint {
Points = 1,
UserPointType = UserPointType.Priority,
UserId = user.Id,
};
_unitOfWork.Repository<UserPoint>().Add(userPoint);
var userPoint = new UserPoint {
Points = 3,
UserPointType = UserPointType.Tip,
UserId = user.Id,
};
_unitOfWork.Repository<UserPoint>().Add(userPoint);
// save 2 new child entities in db
await _unitOfWork.Complete();
<!-- end snippet -->
<!-- begin snippet: js hide: false console: true babel: false -->
<!-- language: lang-js -->
public class User : IdentityUser<int>
{
public int TotalPoints { get; set; } // should be 4
public ICollection<UserPoint> UserPoints { get; set; }
}
public class UserPoint : BaseEntity
{
public UserPointType UserPointType { get; set; }
public int Points { get; set; }
public int UserId { get; set; }
public User User { get; set; }
}
<!-- end snippet -->
答案1
得分: 1
以下是您要翻译的内容:
如果用户表有一个常规的TotalPoints字段,那么从数据结构的角度来看,这是一种反规范化的形式。从数据库的角度来看,无法强制执行TotalPoints反映UserPoints记录的Points总和,因为UserPoints可能会被添加、删除或更改。
模式优先的实体配置的一种选项是为TotalPoints使用计算列,并使用标量函数:
CREATE FUNCTION getTotalPointsForUser
(
@userId int
)
RETURNS int
AS
BEGIN
DECLARE @sum INT
SELECT @sum = SUM(Points) FROM dbo.UserPoints WHERE UserId = @userId
RETURN @sum
END
GO
然后在User表中将TotalPoints声明为计算列,并将其指向getTotalPointsForUser函数:
TotalPoints AS dbo.getTotalPointsForUser(UserId)
现在在User实体中,我们将TotalPoints标记为计算列。这告诉EF在读取时期望计算一个值:
public class User
{
// ...
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public int TotalPoints { get; protected set; }
}
这种方法的一个考虑因素是,它可能会产生性能成本,因为每次读取行时都会计算这个计算列。因此,根据用户总数和/或用户积分记录的总数,这可能会变得昂贵。
您可以考虑的其他选项:
这种方式计算的值通常是一个表示层的关注点。与依赖数据域报告此计算值不同,可以在必要时在读取时执行此操作。例如,如果我有一个UserViewModel,我只想从User记录中显示一些信息和该用户的总积分:
[Serializable]
public class UserViewModel
{
public int UserId { get; set; }
public string Name { get; set; }
public int TotalPoints { get; set; }
}
现在当我读取我的用户时:
var users = _context.Users
.Select(x => new UserViewModel
{
UserId = x.UserId,
Name = x.FirstName + " " + x.LastName,
TotalPoints = x.UserPoints.Sum(up => up.Points)
}).ToList();
这种映射可以在Automapper中配置,并简化为:
var users = _context.Users
.ProjectTo<UserViewModel>(config).ToList();
...其中"config"是一个MapperConfiguration,说明如何从UserPoints映射Name(如果像示例中那样组合)和TotalPoints。
这个选项将用户和用户积分的总点数方面作为一种规范化选项,只有在我们希望以视图可以使用的方式提取数据时才会发生"展平"。
最后一个选项可以帮助防止反规范化,如果您的应用程序是唯一应该触及数据库的东西。这使用更多的DDD方法来规范如何修改用户积分,以确保只有通过User才能进行更改。
这将引入诸如以下方法:
public UserPoint AddPoints(int points)
{
if (points < 0) throw new ArgumentOutOfRangeException(nameof(points));
var userPoint = new UserPoint { Points = points };
}
public void RemoveUserPoints(int userPointId)
{
var userPoint = UserPoints.Single(x => x.UserPointId == userPointId);
TotalPoints -= userPoint.Points;
UserPoints.Remove(userPoint);
}
public void ReplaceUserPoints(int userPointId, int points);
{
if (points < 0) throw new ArgumentOutOfRangeException(nameof(points));
var userPoint = UserPoints.Single(x => x.UserPointId == userPointId);
TotalPoints -= userPoint.Points;
userPoint.Points = points;
TotalPoints += points;
}
...到User实体,然后通过使用internal
设置器来保护UserPoint实体上的"Points"值,以阻止代码修改UserPoint值,并引导代码调用User.AddPoints / RemoveUserPoints / ReplaceUserPoints:
这还涉及到一个工作单元,用于限定DbContext并确保在调用用户的操作后调用SaveChanges()
,并处理引发的任何异常。
这将是我最不推荐的选项,因为它需要最多的工作,并且不能保证反规范化数据不会仍然失去同步。
无论如何,这应该给您一些解决这种问题的选项的想法。
英文:
If the User table has a regular TotalPoints field then this is a form of a denormalization in terms of the data structure. From a database perspective there is no way to enforce that TotalPoints reflects the sum of Points for UserPoints records as UserPoints may be added, removed, or altered.
One option for schema-first entity configurations would be to use a computed column for the TotalPoints with a Scalar function:
CREATE FUNCTION getTotalPointsForUser
(
@userId int
)
RETURNS int
AS
BEGIN
DECLARE @sum INT
SELECT @sum = SUM(Points) FROM dbo.UserPoints WHERE UserId = @userId
RETURN @sum
END
GO
Then in the User table we declare TotalPoints as a Computed column and point it at the getTotalPointsForUser function:
TotalPoints AS dbo.getTotalPointsForUser(UserId)
Now in the entity for the User, we mark TotalPoints as a Computed column. This tells EF to expect a value to calculated on read:
public class User
{
// ...
[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public int TotalPoints { get; protected set; }
}
One consideration with this approach is that it can incur a performance cost as this computed column will be calculated each time the row is read. So depending on the total # of users and/or user point records this could start getting expensive to run.
Other options you can consider:
Computed values like this are often a presentation concern. Rather than relying on the data domain to report this calculated value, this could instead be done at read where necessary. For example if I have a UserViewModel where I want to just display some info from the User record and a sum of the total points for that user:
[Serializable]
public class UserViewModel
{
public int UserId { get; set; }
public string Name { get; set; }
public int TotalPoints { get; set; }
}
Now when I read my User(s):
var users = _context.Users
.Select(x => new UserViewModel
{
UserId = x.UserId,
Name = x.FirstName + " " + x.LastName,
TotalPoints = x.UserPoints.Sum(up => up.Points)
}).ToList();
This mapping can be configured in Automapper and be simplified to:
var users = _context.Users
.ProjectTo<UserViewModel>(config).ToList();
... where "config" is a MapperConfiguration explaining to map the Name (if combined like the example) and TotalPoints from the UserPoints.
This option leaves the Total Points aspect of users and user points as a normalized option where the "flattening" only occurs when we want to pull the data in a way the view can consume.
The last option that can help guard de-normalization can work if your application is the only thing that should be touching the database. This uses a more DDD approach to regulate how user points are modified to ensure that changes are only done through the User.
This would introduce methods like:
public UserPoint AddPoints(int points)
{
if (points < 0) throw new ArgumentOutOfRangeException(nameof(points));
var userPoint = new UserPoint { Points = points };
}
public void RemoveUserPoints(int userPointId)
{
var userPoint = UserPoints.Single(x => x.UserPointId == userPointId);
TotalPoints -= userPoint.Points;
UserPoints.Remove(userPoint);
}
public void ReplaceUserPoints(int userPointId, int points);
{
if (points < 0) throw new ArgumentOutOfRangeException(nameof(points));
var userPoint = UserPoints.Single(x => x.UserPointId == userPointId);
TotalPoints -= userPoint.Points;
userPoint.Points = points;
TotalPoints += points;
}
... to the User entity, then guarding the "Points" value on the UserPoint entity with an internal
setter to deter code altering UserPoint values and directing code to call User.AddPoints / RemoveUserPoints / ReplaceUserPoints:
This would also involve a unit of work to scope the DbContext and ensure that SaveChanges()
is called after calling the action against the user to alter the points, and handle any exceptions raised.
This would be my least recommended option given it is the most work and does not guarantee that the de-normalized data doesn't still end up getting out of sync.
In any case that should give you some ideas on options to tackle that kind of problem.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论