Columna calculada en EF Code First

79

Necesito tener una columna en mi base de datos calculada por la base de datos como (suma de filas) - (suma de filasb). Estoy usando el modelo de código primero para crear mi base de datos.

Esto es lo que quiero decir:

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)
}

¿Cómo puedo lograr esto en EF CodeFirst?

CodeDemen
fuente

Respuestas:

137

Puede crear columnas calculadas en las tablas de su base de datos. En el modelo EF, simplemente anote las propiedades correspondientes con el DatabaseGeneratedatributo:

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

O con mapeo fluido:

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

Como sugirió Matija Grcic y en un comentario, es una buena idea hacer la propiedad private set, porque probablemente nunca querrá establecerla en el código de la aplicación. Entity Framework no tiene problemas con los establecedores privados.

Nota: Para EF .NET Core, debe usar ValueGeneratedOnAddOrUpdate porque HasDatabaseGeneratedOption no existe, por ejemplo:

modelBuilder.Entity<Income>().Property(t => t.Summ)
    .ValueGeneratedOnAddOrUpdate()
Gert Arnold
fuente
26
Lo sé, pero ¿cómo agrego una fórmula para calcularla en mi base de datos a través de EF, para que sea creada por la consola commad update-database?
CodeDemen
10
Indique esto claramente en su pregunta. Significa que desea que las migraciones creen una columna calculada. Hay un ejemplo aquí .
Gert Arnold
2
¿El setter tiene que ser privado?
Cherven
1
@Cherven Sí, probablemente sea mejor hacer eso.
Gert Arnold
6
Esta respuesta debe actualizarse para agregar que para EF Core, el generador de modelos debe usar el método ValueGeneratedOnAddOrUpdate()porque HasDatabaseGeneratedOptionno existe. De lo contrario, gran respuesta.
Máximo
34
public string ChargePointText { get; set; }

public class FirstTable 
{
    [Key]
    public int UserID { get; set; }

    [DatabaseGenerated(DatabaseGeneratedOption.Computed)]      
    public string Summ 
    {
        get { return /* do your sum here */ }
        private set { /* needed for EF */ }
    }
}

Referencias:

Matija Grcic
fuente
+1 para agregar un conjunto privado. La columna calculada no debe establecerse al agregar nuevos objetos.
Taher
1
Ocurrió ver esta pregunta nuevamente y ahora veo que la parte /* do your sum here */no se aplica. Si la propiedad se calcula dentro de la clase, debe anotarse como [NotMapped]. Pero el valor proviene de la base de datos, por lo que debería ser una getpropiedad simple .
Gert Arnold
1
@GertArnold vea aquí - "Dado que la propiedad FullName es calculada por la base de datos, se desincronizará en el lado del objeto tan pronto como hagamos un cambio en la propiedad FirstName o LastName. Afortunadamente, podemos tener lo mejor de ambos mundos aquí agregando también el cálculo al getter en la propiedad FullName "
AlexFoxGill
@AlexFoxGill Entonces, ¿cuál es el punto? ¿Por qué molestarse en almacenar valores calculados si los va a recalcular dinámicamente cada vez en caso de que se "desincronicen"?
Rudey
@RuudLenders para que pueda utilizar la columna calculada en consultas LINQ.
AlexFoxGill
15

A partir de 2019, EF core le permite tener columnas calculadas de una manera limpia con la API fluida:

Suponga que DisplayNamees la columna calculada que desea definir, debe definir la propiedad como de costumbre, posiblemente con un acceso de propiedad privada para evitar asignarla

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; }
}

Luego, en el generador de modelos, abórdelo con la definición de columna:

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

Para obtener más información, consulte MSDN .

Yennefer
fuente
3

En EF6, puede configurar la configuración de mapeo para ignorar una propiedad calculada, como esta:

Defina el cálculo en la propiedad get de su modelo:

public class Person
{
    // ...
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string FullName => $"{FirstName} {LastName}";
}

Luego configúrelo para ignorar en la configuración del modelo

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //...
    modelBuilder.Entity<Person>().Ignore(x => x.FullName)
}
Fernando Vieira
fuente
1

Una forma es hacerlo con 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;

Puede hacerlo dentro de su objeto POCO public class FirstTable, pero no sugeriría hacerlo, porque creo que no es un buen diseño.

Otra forma sería usar una vista SQL. Puede leer una vista como una tabla con Entity Framework. Y dentro del código de vista, puede hacer cálculos o lo que quiera. Simplemente crea una vista como

-- 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
Linus Caldwell
fuente
0

Haría esto simplemente usando un modelo de vista. Por ejemplo, en lugar de tener la clase FirstTable como una entidad db, ¿no sería mejor tener una clase de modelo de vista llamada FirstTable y luego tener una función que se usa para devolver esta clase que incluiría la suma calculada? Por ejemplo, su clase sería simplemente:

public class FirstTable {
  public int UserID { get; set; }
  public double Sum { get; set; }
 }

Y luego tendrías una función a la que llamas que devuelve la suma calculada:

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;
}

Básicamente lo mismo que una vista SQL y como mencionó @Linus, no creo que sea una buena idea mantener el valor calculado en la base de datos. Solo algunos pensamientos.

craigvl
fuente
+1 para I don't think it would be a good idea keeping the computed value in the database, especialmente si va a usar Azure SQL, que comenzará a bloquearse con una carga pesada.
Piotr Kula
1
@ppumkin En la mayoría de los casos, el cálculo agregado se realizará mejor en la base de datos. Piense en algo como 'ID de comentario más reciente'. No querrás tener que retirar cada ID de comentario para tomar solo uno de ellos. No solo está desperdiciando datos y memoria, sino que también está aumentando la carga en la base de datos y, de hecho, coloca bloqueos compartidos en más filas durante más tiempo. Es más, tendrías que actualizar muchas filas todo el tiempo, lo que probablemente sea un diseño que deba revisarse.
JoeBrockhaus
OKAY. Solo quise mantenerlos en memoria, valores calculados, en caché. No vaya a la base de datos de cada visitante. Va a causar problemas. He visto esto pasar demasiadas veces.
Piotr Kula
-2

Me encontré con esta pregunta cuando intentaba tener un modelo EF Code First con una columna de cadena "Slug", derivado de otra columna de cadena "Nombre". El enfoque que tomé fue ligeramente diferente pero funcionó bien, así que lo compartiré aquí.

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; }

Lo bueno de este enfoque es que obtienes la generación automática de babosas, sin exponer nunca al colocador de babosas. El método .ToUrlSlug () no es la parte importante de esta publicación, puede usar cualquier cosa en su lugar para hacer el trabajo que necesita. ¡Salud!

Patrick Michalina
fuente
¿No editar todos aquellos bits que enlazan Sluga Name? Como está escrito actualmente, el Namesetter ni siquiera debería compilar.
Auspex
No, antes de su edición, era un ejemplo viable. Después de su edición, no tiene sentido.
Auspex