Kolumna obliczeniowa w EF Code First

80

Muszę mieć jedną kolumnę w mojej bazie danych obliczoną przez bazę danych jako (suma wierszy) - (suma wierszyb). Do tworzenia mojej bazy danych używam modelu Code First.

Oto o co mi chodzi:

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

Jak mogę to osiągnąć w EF CodeFirst?

CodeDemen
źródło

Odpowiedzi:

137

W tabelach bazy danych można tworzyć kolumny obliczeniowe . W modelu EF wystarczy dodać adnotację do odpowiednich właściwości za pomocą DatabaseGeneratedatrybutu:

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

Lub z płynnym mapowaniem:

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

Jak zasugerował Matija Grcic oraz w komentarzu, dobrym pomysłem jest utworzenie właściwości private set, ponieważ prawdopodobnie nigdy nie chciałbyś ustawiać jej w kodzie aplikacji. Entity Framework nie ma problemów z ustawieniami prywatnymi.

Uwaga: w przypadku EF .NET Core należy użyć ValueGeneratedOnAddOrUpdate, ponieważ HasDatabaseGeneratedOption nie istnieje, np .:

modelBuilder.Entity<Income>().Property(t => t.Summ)
    .ValueGeneratedOnAddOrUpdate()
Gert Arnold
źródło
26
Wiem o tym, ale jak dodać formułę, aby obliczyć ją do mojej bazy danych za pośrednictwem EF, aby została utworzona przez konsolę commad update-database?
CodeDemen
10
Prosimy o wyraźne określenie tego w swoim pytaniu. Oznacza to, że chcesz, aby migracje utworzyły kolumnę obliczeniową. Oto przykład .
Gert Arnold
2
czy seter musi być prywatny?
Cherven
1
@Cherven Tak, prawdopodobnie lepiej to zrobić.
Gert Arnold
6
Ta odpowiedź powinna zostać zaktualizowana, aby dodać, że dla EF Core Konstruktor modelu powinien używać metody, ValueGeneratedOnAddOrUpdate()ponieważ HasDatabaseGeneratedOptionnie istnieje. W przeciwnym razie świetna odpowiedź.
Max
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 */ }
    }
}

Bibliografia:

Matija Grcic
źródło
+1 za dodanie zestawu prywatnego. Kolumny obliczonej nie należy ustawiać podczas dodawania nowych obiektów.
Taher
1
Zdarzyło się, że ponownie zobaczyłem to pytanie, a teraz widzę, że część /* do your sum here */nie ma zastosowania. Jeśli właściwość jest obliczana wewnątrz klasy, powinna być oznaczona jako [NotMapped]. Ale wartość pochodzi z bazy danych, więc powinna to być tylko prosta getwłaściwość.
Gert Arnold
1
@GertArnold patrz tutaj - „Ponieważ właściwość FullName jest obliczana przez bazę danych, wyłączy się z synchronizacji po stronie obiektu, gdy tylko dokonamy zmiany we właściwości FirstName lub LastName. Na szczęście możemy mieć to, co najlepsze z obu światów tutaj również przez dodanie obliczenia z powrotem do metody pobierającej we właściwości FullName ”
AlexFoxGill,
@AlexFoxGill Więc o co chodzi? Po co zawracać sobie głowę przechowywaniem obliczonych wartości, jeśli chcesz je dynamicznie przeliczać za każdym razem, na wypadek, gdyby „nie były zsynchronizowane”?
Rudey,
@RuudLenders, aby można było używać obliczonej kolumny w zapytaniach LINQ.
AlexFoxGill,
15

Począwszy od 2019 r., EF core pozwala mieć obliczone kolumny w czysty sposób z Fluent API:

Załóżmy, że DisplayNamejest to kolumna obliczeniowa, którą chcesz zdefiniować, musisz zdefiniować właściwość w zwykły sposób, prawdopodobnie za pomocą metody dostępu do właściwości prywatnej, aby zapobiec przypisywaniu jej

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

Następnie w konstruktorze modeli zaadresuj go za pomocą definicji kolumny:

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

Aby uzyskać więcej informacji, zajrzyj na MSDN .

Yennefer
źródło
3

W EF6 możesz po prostu skonfigurować ustawienie mapowania, aby zignorować obliczoną właściwość, na przykład:

Zdefiniuj obliczenia na właściwości get swojego modelu:

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

Następnie ustaw go tak, aby ignorował konfigurację modelu

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //...
    modelBuilder.Entity<Person>().Ignore(x => x.FullName)
}
Fernando Vieira
źródło
1

Jednym ze sposobów jest zrobienie tego za pomocą 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;

Możesz to zrobić w swoim obiekcie POCO public class FirstTable, ale nie sugerowałbym tego, ponieważ uważam, że to nie jest dobry projekt.

Innym sposobem byłoby użycie widoku SQL. Możesz czytać widok jak tabelę za pomocą Entity Framework. W kodzie widoku możesz wykonywać obliczenia lub cokolwiek chcesz. Po prostu stwórz taki widok

-- 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
źródło
0

Zajmę się tym, używając tylko modelu widoku. Na przykład zamiast klasy FirstTable jako jednostki db nie byłoby lepiej po prostu mieć klasę modelu widoku o nazwie FirstTable, a następnie mieć funkcję, która jest używana do zwracania tej klasy, która zawierałaby obliczoną sumę? Na przykład twoja klasa wyglądałaby po prostu:

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

A potem miałbyś wywoływaną funkcję, która zwraca obliczoną sumę:

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

Zasadniczo to samo, co widok SQL i jak wspomniał @Linus, nie sądzę, aby dobrym pomysłem byłoby przechowywanie obliczonej wartości w bazie danych. Tylko kilka myśli.

craigvl
źródło
+1 za I don't think it would be a good idea keeping the computed value in the database- zwłaszcza jeśli zamierzasz używać Azure SQL, które zaczną blokować się przy dużym obciążeniu.
Piotr Kula
1
@ppumkin W większości przypadków zagregowane obliczenia będą lepiej wykonywane w bazie danych. Pomyśl o czymś takim jak „identyfikator ostatniego komentarza”. Nie chcesz wycofywać każdego CommentID, aby wziąć tylko jeden z nich. Nie tylko marnujesz dane i pamięć, ale także zwiększasz obciążenie samej bazy danych i faktycznie nakładasz wspólne blokady na więcej wierszy na dłużej. Co więcej, musiałbyś przez cały czas aktualizować wiele wierszy, co prawdopodobnie jest projektem, który wymaga przeglądu.
JoeBrockhaus
DOBRZE. Chodziło mi o przechowywanie ich w pamięci, obliczone wartości, buforowanie. Nie przechodź do bazy danych dla każdego odwiedzającego. To spowoduje problemy. Widziałem to zbyt wiele razy.
Piotr Kula
-2

Natknąłem się na to pytanie, próbując mieć model EF Code First z kolumną ciągu „Slug”, pochodzącą z innej kolumny ciągu „Name”. Podejście, które obrałem, było nieco inne, ale wyszło dobrze, więc podzielę się nim tutaj.

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

To, co jest fajne w tym podejściu, to automatyczne generowanie informacji o kule, bez narażania ustawiacza. Metoda .ToUrlSlug () nie jest istotną częścią tego postu, możesz użyć w jej miejsce czegokolwiek, aby wykonać pracę, którą potrzebujesz. Twoje zdrowie!

Patrick Michalina
źródło
Nie można edytować wszystkie bity, które faktycznie łączą Slugsię Name? Jak obecnie napisano, Namesetter nie powinien się nawet kompilować.
Auspex
Nie, przed edycją był to praktyczny przykład. Po edycji nie ma to sensu.
Auspex