EF Code First中的计算列

105

我需要在我的数据库中计算一列作为 (行和) - (行b和),我正在使用代码优先模型来创建我的数据库。

这是我的意思:

public class Income {
      [Key]
      public int UserID { get; set; }
      public double inSum { get; set; }
}

public class Outcome {
      [Key]
      public int UserID { get; set; }
      public double outSum { get; set; }
}

public class FirstTable {
      [Key]
      public int UserID { get; set; }
      public double Sum { get; set; } 
      // This needs to be calculated by DB as 
      // ( Select sum(inSum) FROM Income WHERE UserID = this.UserID) 
      // - (Select sum(outSum) FROM Outcome WHERE UserID = this.UserID)
}

如何在EF CodeFirst中实现这个?


获取 { 返回 /在此计算/ } - Ali NajafZadeh
[DatabaseGenerated(DatabaseGeneratedOption.Computed)] public double Summ { get; private set; } - user16559547
7个回答

165

您可以在数据库表中创建计算列。在EF模型中,您只需使用DatabaseGenerated属性注释相应的属性:

[DatabaseGenerated(DatabaseGeneratedOption.Computed)]
public double Summ { get; private set; } 

或者使用流畅的映射:

modelBuilder.Entity<Income>().Property(t => t.Summ)
    .HasDatabaseGeneratedOption(DatabaseGeneratedOption.Computed)

Matija Grcic所建议并在评论中提到的,将属性private set是一个好主意,因为您可能永远不想在应用程序代码中设置它。Entity Framework对私有setter没有问题。 注意:对于EF .NET Core,您应该使用ValueGeneratedOnAddOrUpdate,因为HasDatabaseGeneratedOption不存在,例如:
modelBuilder.Entity<Income>().Property(t => t.Summ)
    .ValueGeneratedOnAddOrUpdate()

37
我知道这件事,但是我该如何通过EF向我的数据库添加一个计算公式,以便可以通过控制台命令update-database创建它? - CodeDemen
12
请在您的问题中明确说明这一点。这意味着您希望迁移创建一个计算列。这里有一个示例(http://www.davepaquette.com/archive/2012/09/23/calculated-columns-in-entity-framework-code-first-migrations.aspx)。 - Gert Arnold
2
设置器必须是私有的吗? - Cherven
1
@Cherven 是的,最好这样做。 - Gert Arnold
7
这个答案需要更新,加入EF Core的模型生成器应该使用ValueGeneratedOnAddOrUpdate()方法,因为HasDatabaseGeneratedOption不存在。总之,这个答案很棒。 - Max
显示剩余5条评论

47

截至2019年,EF Core允许您使用流利的API以简洁的方式拥有计算列:

假设您要定义DisplayName作为计算列,则必须像通常一样定义该属性,可能还需要使用私有属性访问器以防止分配它。

public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    // this will be computed
    public string DisplayName { get; private set; }
}

然后,在模型构建器中,使用列定义来处理它:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Person>()
        .Property(p => p.DisplayName)
        // here is the computed query definition
        .HasComputedColumnSql("[LastName] + ', ' + [FirstName]");
}

如需更多信息,请查看MSDN


6
这仅适用于 EF Core。该问题提及的是 EF5,它不支持所引用的方法。 - joelmdev

42

1
+1 表示添加私有设置。当添加新对象时,计算列不应该被设置。 - Taher
1
刚好又看到了这个问题,现在我明白了/* do your sum here */部分不适用。如果属性是在类内计算的,应该被注释为[NotMapped]。但是如果值来自数据库,那么它应该只是一个简单的get属性。 - Gert Arnold
1
@GertArnold 点击这里 - *"由于FullName属性是由数据库计算的,一旦我们更改了FirstName或LastName属性,它就会在对象方面失去同步。幸运的是,在FullName属性的getter中添加计算,我们可以同时拥有最佳效果。"* - Alex
@AlexFoxGill 那这样做有什么意义呢?如果你动态地每次都要重新计算已存储的值,以防它们“不同步”,那还有什么必要呢? - Rudey
@RuudLenders 这样你就可以在LINQ查询中使用计算列了。 - Alex

5
在EF6中,您可以通过配置映射设置来忽略计算属性,如下所示:
在模型的get属性中定义计算操作:
public class Person
{
    // ...
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName => $"{FirstName} {LastName}";
}

然后在模型配置中将其设置为忽略。
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //...
    modelBuilder.Entity<Person>().Ignore(x => x.FullName)
}

我认为你的解决方案是最简单和优雅的。谢谢! - hotfusion

3

一种方法是使用LINQ:

var userID = 1; // your ID
var income = dataContext.Income.First(i => i.UserID == userID);
var outcome = dataContext.Outcome.First(o => o.UserID == userID);
var summ = income.inSumm - outcome.outSumm;

您可以在POCO对象public class FirstTable中执行此操作,但我不建议这样做,因为我认为这不是一个好的设计。

另一种方法是使用SQL视图。您可以像读取表一样使用Entity Framework来读取视图。并且在视图代码内部,您可以进行计算或任何您想要的操作。只需创建一个视图即可:

-- not tested
SELECT FirstTable.UserID, Income.inCome - Outcome.outCome
  FROM FirstTable INNER JOIN Income
           ON FirstTable.UserID = Income.UserID
       INNER JOIN Outcome
           ON FirstTable.UserID = Outcome.UserID

1
我会通过使用视图模型来处理这个问题。例如,不要将FirstTable类作为数据库实体,而是最好只有一个名为FirstTable的视图模型类,然后有一个函数用于返回包含计算和的该类。例如,您的类应该只包含:
public class FirstTable {
  public int UserID { get; set; }
  public double Sum { get; set; }
 }

然后你需要调用一个函数,该函数返回计算出的总和:

public FirsTable GetNetSumByUserID(int UserId)
{
  double income = dbcontext.Income.Where(g => g.UserID == UserId).Select(f => f.inSum);
  double expenses = dbcontext.Outcome.Where(g => g.UserID == UserId).Select(f => f.outSum);
  double sum = (income - expense);
  FirstTable _FirsTable = new FirstTable{ UserID = UserId, Sum = sum};
  return _FirstTable;
}

基本上与SQL视图相同,正如@Linus所提到的,我认为在数据库中保留计算值并不是一个好主意。这只是一些想法。

+1,我认为将计算值保留在数据库中不是一个好主意,特别是如果你要使用Azure SQL,在重负载下会开始出现死锁问题。 - Piotr Kula
2
在大多数情况下,聚合计算最好在数据库中执行。想象一下像“最近的评论ID”这样的东西。您不希望只取其中一个评论ID就必须将每个CommentID都拉回来。这不仅浪费数据和内存,而且还增加了DB本身的负载,并且您实际上会更长时间地对更多行进行共享锁定。更重要的是,您可能需要经常更新很多行,这可能需要进行审查设计。 - JoeBrockhaus
好的。我只是想让你把它们保留在内存中,计算值和缓存。不要为每个访问者都去数据库。这会引起问题。我已经看到这种情况发生太多次了。 - Piotr Kula

0
我在尝试使用EF Code First模型时,遇到了一个问题:如何让一个名为“Slug”的字符串列从另一个名为“Name”的字符串列中派生出来。我采取的方法略有不同,但效果很好,所以在这里分享一下。
private string _name;

public string Name
{
    get { return _name; }
    set
    {
        _slug = value.ToUrlSlug(); // the magic happens here
        _name = value; // but don't forget to set your name too!
    }
}

public string Slug { get; private set; }

这种方法的好处在于您可以获得自动化的slug生成,同时不会暴露slug setter。.ToUrlSlug()方法并不是本文的重点,您可以使用任何其他方法来完成所需的工作。祝福!


你没有编辑掉所有将“Slug”链接到“Name”的位吗?按照当前的写法,“Name”设置器甚至不应该编译。 - Auspex
不,你编辑之前,它是一个可行的例子。但是在你编辑之后,它变得毫无意义。 - Auspex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接