2013-05-13 14 views
16

Korzystamy z EF 5.0 jako naszego ORM-a z wyboru w naszym biznesowym rozwiązaniu, zbudowanym w sposób n-warstwowy, z tym, że wszystko jest oddzielone od siebie i ładnym rootem kompozycji z ninject.Entity Framework 5 nieprawidłowy typ danych w zapytaniu

Ostatnio budowaliśmy bazę danych, która używa partycjonowania pod spodem, i mamy kilka ważnych indeksów na kolumnach DATE.

Kolumny są poprawnie zadeklarowane na Sql Server 2008. Dodaliśmy również poprawny typ danych w mapowaniach EF, z instrukcją HasColumnType("Date").

Nadal, podczas odpytywania tabeli przez Linq do Entities, parametry, od których filtrujemy daty, są tworzone z typu DateTime2, a nawet kolumny są przesyłane do zapytania DateTime2 w zapytaniach, aby typ odpowiadał parametrom.

To zachowanie ma kilka problemów. Po pierwsze, jeśli mówię silnikowi EF, że kolumna w bazie danych to DATE, dlaczego ma ją przesłać do DateTime2?

Po drugie, ta obsada powoduje, że baza danych zignoruje indeksy, a zatem nie używa partycjonowania. Mamy jeden rok na partycjonowanie fizyczne i jeśli zapytam o zakres dat, powiedzmy, luty 2013 do marca 2013, skanowanie powinno odbyć się tylko na jednej partycji fizycznej. Działa poprawnie, jeśli ręcznie używa prawidłowego typu danych: DATE, ale z rzutowaniem na DateTime2 wszystkie partycje zostaną przeskanowane, co drastycznie obniży wydajność.

Teraz jestem pewien, że czegoś brakuje, ponieważ byłoby dość głupie, że Microsoft ORM nie działa dobrze na Microsoft Sql Server.

Nie mogę znaleźć żadnej dokumentacji dotyczącej sposobu, w jaki EF używa poprawnych typów danych w zapytaniach, więc proszę o to tutaj. Każda pomoc zostanie doceniona.

Dzięki.

Odpowiedz

4

Nie wierzę, że jest to możliwe w Entity Framework. This requested enhancement prawdopodobnie zrobi to, czego potrzebujesz. This MSDN page pokazuje mapowanie między typami SQL Server i typami CLR. Zauważ, że date jest obsługiwany i jest mapowany na DateTime, ale ponieważ kilka typów SQL odwzorowuje ten sam typ CLR, EF najwyraźniej wybiera jeden typ SQL jako preferowany eqivalent typu CLR.

Czy możesz zawinąć swój kod wyboru w procedurze przechowywanej? Jeśli tak, wydaje się to rozsądnym rozwiązaniem. Możesz użyć DbSet{T}.SqlQuery, aby zmaterializować obiekty od wykonania sp.

Przykładowy kod

Poniższa krótka aplikacja konsoli demonstruje tę koncepcję. Zwróć uwagę, jak powiązane obiekty są z powodzeniem ładowane z opóźnieniem.

using System; 
using System.Collections.Generic; 
using System.Collections.ObjectModel; 
using System.ComponentModel.DataAnnotations; 
using System.ComponentModel.DataAnnotations.Schema; 
using System.Data.Entity; 
using System.Data.SqlClient; 
using System.Linq; 

namespace ConsoleApplication1 
{ 
    [Table("MyEntity")]  
    public class MyEntity 
    { 
     private Collection<MyRelatedEntity> relatedEntities; 

     [Key] 
     public virtual int MyEntityId { get; set; } 

     [DataType(DataType.Date)] 
     public virtual DateTime MyDate { get; set; } 

     [InverseProperty("MyEntity")] 
     public virtual ICollection<MyRelatedEntity> RelatedEntities 
     { 
      get 
      { 
       if (this.relatedEntities == null) 
       { 
        this.relatedEntities = new Collection<MyRelatedEntity>(); 
       } 

       return this.relatedEntities; 
      } 
     } 

     public override string ToString() 
     { 
      return string.Format("Date: {0}; Related: {1}", this.MyDate, string.Join(", ", this.RelatedEntities.Select(q => q.SomeString).ToArray())); 
     } 
    } 

    public class MyRelatedEntity 
    { 
     [Key] 
     public virtual int MyRelatedEntityId { get; set; } 

     public virtual int MyEntityId { get; set; } 

     [ForeignKey("MyEntityId")] 
     public virtual MyEntity MyEntity { get; set; } 

     public virtual string SomeString { get;set;} 
    } 

    public class MyContext : DbContext 
    { 
     public DbSet<MyEntity> MyEntities 
     { 
      get { return this.Set<MyEntity>(); } 
     } 
    } 

    class Program 
    { 
     const string SqlQuery = @"DECLARE @date date; SET @date = @dateIn; SELECT * FROM MyEntity WHERE MyDate > @date"; 

     static void Main(string[] args) 
     { 
      Database.SetInitializer(new DropCreateDatabaseAlways<MyContext>()); 

      using (MyContext context = new MyContext()) 
      { 
       context.MyEntities.Add(new MyEntity 
        { 
         MyDate = DateTime.Today.AddDays(-2), 
         RelatedEntities = 
         { 
          new MyRelatedEntity { SomeString = "Fish" }, 
          new MyRelatedEntity { SomeString = "Haddock" } 
         } 
        }); 

       context.MyEntities.Add(new MyEntity 
       { 
        MyDate = DateTime.Today.AddDays(1), 
        RelatedEntities = 
         { 
          new MyRelatedEntity { SomeString = "Sheep" }, 
          new MyRelatedEntity { SomeString = "Cow" } 
         } 
       }); 

       context.SaveChanges(); 
      } 

      using (MyContext context = new MyContext()) 
      { 
       IEnumerable<MyEntity> matches = context.MyEntities.SqlQuery(
        SqlQuery, 
        new SqlParameter("@dateIn", DateTime.Today)).ToList(); 

       // The implicit ToString method call here invokes lazy-loading of the related entities. 
       Console.WriteLine("Count: {0}; First: {1}.", matches.Count(), matches.First().ToString()); 
      } 

      Console.Read(); 
     } 
    } 
} 
+0

Używam leniwego ładowania na tonę właściwości nawigacyjnych. Potrzebowałbym wszystkiego, aby załadować wszystko i ręcznie utworzyć mój wykres obiektów, który jest bardzo złożony i głęboki, więc nie jest to opcja. Może dobrym wyborem będzie przejście na inną ORM jak NHibernate. –

+0

@MatteoMosca - Nie sądzę, że tak jest, chyba że źle zrozumiałem problem. EF pozwala zmaterializować wyniki sp jako * załączone * encje ("śledzone przez kontekst" zgodnie z MSDN), więc powinno działać leniwy ładowanie. Czego nie możesz zrobić, to wspierać * chętny * ładowanie za pomocą tej metody. – Olly

+0

To może być interesujące. Myślę, że tęskniłem za tą funkcją. Przyjrzę się temu jak najszybciej. –

2

Nie mam rozwiązania. Nigdy nie widziałem zapytania LINQ-do-Entites z parametrami .NET DateTime, które wykorzystywały typ parametru w zapytaniu SQL innym niż datetime2(7). Wątpię, że możesz się tego pozbyć. Po prostu spróbuj wyjaśnić, dlaczego jest taki:

Załóżmy, że masz jednostkę o właściwości SomeNumber typu int. Jaki wynik można oczekiwać dla kwerendy tak:

....Where(e => e.SomeNumber >= 7.3).... 

Prawdopodobnie wszystkie podmioty gdzie SomeNumber jest 8 lub większa. Jeśli parametr (zmiennoprzecinkowy dziesiętny dziesiętny) 7.3 zostałby przeniesiony do typu int przechowywanego w bazie danych, musiałbyś zdecydować, jak zaokrąglić 7.3 - do 7 (doprowadzi to do błędnego wyniku) lub do 8? OK, można powiedzieć, ponieważ moje zapytanie mówi: >= i wiem, że typ w DB jest liczbą całkowitą, zaokrąglanie do 8 musi być poprawne. Jeśli użyłbym <=, to zaokrąglanie do 7 musi być poprawne. Gdybym użył ==, oh ... nie mogę zaokrąglić wcale lub wiem, że wynik musi być pusty i mógłbym bezpośrednio przetłumaczyć tę klauzulę Where na false. I != do true. Ale parametr 7.0 jest szczególnym przypadkiem. Itp....

Dylemat w tym przykładzie ma proste rozwiązanie: Zdecyduj po stronie klienta, co chcesz, najpierw używając parametru int (7 lub 8).

Rozwiązanie z DateTime nie jest takie proste, ponieważ .NET nie ma typu Date. Zapytania z DateTime parametrów zawsze będą miały formę ...

DateTime dateTime = new DateTime(2013, 5, 13, 10, 30, 0); 
....Where(e => e.SomeDateTime >= dateTime).... 

... a jeśli SomeDateTime jest przechowywana jako date w SQL Server trzeba ponownie dylemat zaokrąglenia. Czy muszę przesłać do 2013.05.13 lub 2013.05.14? Dla powyższego zapytania klient z pewnością oczekiwałby wszystkich podmiotów z datą 14 lub późniejszą.

Cóż, możesz zrobić to inteligentnie, na przykład: jeśli część czasu mojego parametru DateTime jest północy, rzuć na porcję daty. Jeśli używam rzutowania >= na następny dzień, itd., Itp. Albo możesz zawsze rzucić na datetime2(7). Wynik kwerendy jest zawsze poprawny, a klient (.NET) tego oczekuje. Poprawnie ... ale może z suboptymalnym użyciem indeksu.

+0

Nie rozumiem, dlaczego Twój pierwszy przykład powinien być problemem. SQL Server pozwala już filtrować kolumnę całkowitą, używając wartości niecałkowitej. Rozważ ten skrypt: https: //gist.github.com/kappa7194/5574037 Mogę filtrować 'MyColumn' przy użyciu float, a SQL Server nie rzuca kolumny na wartość float, aby to zrobić: https://gist.github.com/kappa7194/5574040, więc dlaczego to skręca, gdy Entity Framework jest zaangażowany? – Albireo

+0

Twoje wyjaśnienie jest jasne, ale wciąż myślę, że czegoś brakuje. Jeśli SQL ma typ Date, EF może po prostu zignorować część czasu obiektu .Net DateTime. Ma nawet właściwość .Date, która zwraca wartość DateTime o północy. Czy sugerujesz, że powinniśmy używać w naszych bazach danych tylko typów zgodnych z .Net? To wydaje się dość ograniczeniem. –

+0

To jest bardziej odpowiedni przykład: https://gist.github.com/kappa7194/5574997 Tutaj SQL Server wykonuje "Clustered Index Seek" szukając wartości 'GT' z' DATETIME', które określiłem, myślę, że Entity Framework powinien po prostu przekazuj wartości do bazowej bazy danych (jeśli obsługuje je) i pozwól jej działać. – Albireo

4

Zakres typów DateTime w .NET i SQL Server jest inny.

zakres

.NET DateTime jest: 0000-Jan-01 do 9999-Dec-31 zakres SQL DateTime jest: 1900-Jan-01 2079-Jun-06

Aby dopasować zakres, EF konwertować .NET DataTime do SQL Server Typ DateTime2, który ma taki sam zakres jak zakres .NET DateTime.

Myślę, że problem występuje tylko wtedy, gdy masz właściwość date, która nie jest przypisana i przekazywana do serwera SQL za pośrednictwem EF. Gdy data nie jest przypisana z określoną wartością, domyślnie jest to DateTime.Min, która jest 0000-Jan-01 i która powoduje konwersję na DateTime2.

Myślę, że możesz albo uczynić właściwość DateTime zerowalną -> DateTime? lub napisać pomocnika, aby przekonwertować swój DateTime.Min, aby osiągnąć zakres SQL DateTime.

Mamy nadzieję, że to pomoże.

+1

Właściwie to nie jest. Jednym z przykładów naszego problemu jest wyszukiwanie przy użyciu dwóch dat jako zakresu wyszukiwania. Kolumny w db są DATE nie DateTime lub DateTime2. Wysyłam do EF dwie poprawne wartości DateTime .net, idealnie w zasięgu (na przykład 1 marca 2013 i 1 21013), a konwersja wciąż się odbywa. –

Powiązane problemy