Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Uwaga / Notatka
Tylko od EF6 wzwyż — funkcje, interfejsy API itp. omówione na tej stronie zostały wprowadzone w Entity Framework 6. Jeśli używasz starszej wersji, niektóre lub wszystkie informacje nie mają zastosowania.
W przypadku korzystania z funkcji Code First model jest obliczany na podstawie klas przy użyciu zestawu konwencji. Domyślne konwencje Code First określają elementy takie jak właściwość, która staje się kluczem głównym jednostki, nazwa tabeli, do której jednostka jest mapowana, oraz domyślną precyzję i skalę kolumny dziesiętnej.
Czasami te konwencje domyślne nie są idealne dla modelu i trzeba je obejść, konfigurując wiele pojedynczych jednostek przy użyciu adnotacji danych lub interfejsu API Fluent. Niestandardowe konwencje Code First umożliwiają definiowanie własnych konwencji, które zapewniają domyślne ustawienia konfiguracji dla modelu. W tym przewodniku zapoznamy się z różnymi typami konwencji niestandardowych i sposobem ich tworzenia.
Konwencje oparte na modelu
Na tej stronie omówiono interfejs API DbModelBuilder dla konwencji niestandardowych. Ten interfejs API powinien być wystarczający do tworzenia większości konwencji niestandardowych. Istnieje jednak również możliwość tworzenia konwencji opartych na modelu — konwencji, które manipulują ostatnim modelem po jego utworzeniu — do obsługi zaawansowanych scenariuszy. Aby uzyskać więcej informacji, zobacz Konwencje oparte na modelach.
Nasz model
Zacznijmy od zdefiniowania prostego modelu, którego możemy użyć z naszymi konwencjami. Dodaj następujące klasy do projektu.
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
}
public class Product
{
public int Key { get; set; }
public string Name { get; set; }
public decimal? Price { get; set; }
public DateTime? ReleaseDate { get; set; }
public ProductCategory Category { get; set; }
}
public class ProductCategory
{
public int Key { get; set; }
public string Name { get; set; }
public List<Product> Products { get; set; }
}
Wprowadzenie do konwencji niestandardowych
Napiszmy konwencję, która konfiguruje dowolną właściwość o nazwie Key jako klucz podstawowy dla jego typu jednostki.
Konwencje są włączone w narzędziu do budowania modelu, do którego można uzyskać dostęp, przesłaniając metodę OnModelCreating w kontekście. Zaktualizuj klasę ProductContext w następujący sposób:
public class ProductContext : DbContext
{
static ProductContext()
{
Database.SetInitializer(new DropCreateDatabaseIfModelChanges<ProductContext>());
}
public DbSet<Product> Products { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
}
}
Teraz każda właściwość w naszym modelu o nazwie Key zostanie skonfigurowana jako klucz podstawowy dowolnej jednostki.
Możemy również uczynić nasze konwencje bardziej szczegółowymi, filtrując typ właściwości, którą skonfigurujemy:
modelBuilder.Properties<int>()
.Where(p => p.Name == "Key")
.Configure(p => p.IsKey());
Spowoduje to skonfigurowanie wszystkich właściwości o nazwie Klucz jako klucza podstawowego jednostki, ale tylko wtedy, gdy są liczbą całkowitą.
Interesującą cechą metody IsKey jest to, że jest addytywna. Oznacza to, że w przypadku wywołania funkcji IsKey dla wielu właściwości wszystkie staną się częścią klucza złożonego. Jednym z zastrzeżeń jest to, że po określeniu wielu właściwości klucza należy również określić kolejność dla tych właściwości. Możesz to zrobić, wywołując metodę HasColumnOrder, jak pokazano poniżej:
modelBuilder.Properties<int>()
.Where(x => x.Name == "Key")
.Configure(x => x.IsKey().HasColumnOrder(1));
modelBuilder.Properties()
.Where(x => x.Name == "Name")
.Configure(x => x.IsKey().HasColumnOrder(2));
Ten kod skonfiguruje typy w naszym modelu tak, aby miały klucz złożony składający się z kolumny Int Key i kolumny ciągu Nazwa. Jeśli wyświetlimy model w projektancie, będzie on wyglądać następująco:
Innym przykładem konwencji właściwości jest skonfigurowanie wszystkich właściwości DateTime w moim modelu w taki sposób, aby mapowały na typ datetime2 w serwerze SQL Server zamiast datetime. Można to osiągnąć, wykonując następujące czynności:
modelBuilder.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
Klasy konwencji
Innym sposobem definiowania konwencji jest użycie klasy konwencji do hermetyzacji konwencji. W przypadku używania klasy Konwencji należy utworzyć typ dziedziczony z klasy Convention w przestrzeni nazw System.Data.Entity.ModelConfiguration.Conventions.
Możemy utworzyć klasę konwencji z konwencją datetime2, którą pokazaliśmy wcześniej, wykonując następujące czynności:
public class DateTime2Convention : Convention
{
public DateTime2Convention()
{
this.Properties<DateTime>()
.Configure(c => c.HasColumnType("datetime2"));
}
}
Aby poinstruować EF do stosowania tej konwencji, należy dodać ją do kolekcji Conventions w funkcji OnModelCreating, co, jeśli śledziłeś przewodnik, będzie wyglądać następująco:
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Properties<int>()
.Where(p => p.Name.EndsWith("Key"))
.Configure(p => p.IsKey());
modelBuilder.Conventions.Add(new DateTime2Convention());
}
Jak widać, dodajemy wystąpienie naszej konwencji do kolekcji konwencji. Dziedziczenie z konwencji zapewnia wygodny sposób grupowania i udostępniania konwencji między zespołami lub projektami. Możesz na przykład mieć bibliotekę klas z typowym zestawem konwencji używanych przez wszystkie projekty organizacji.
Atrybuty niestandardowe
Innym doskonałym zastosowaniem konwencji jest umożliwienie używania nowych atrybutów podczas konfigurowania modelu. Aby to zilustrować, utwórzmy atrybut, którego możemy użyć do oznaczania właściwości ciągów jako innych niż Unicode.
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class NonUnicode : Attribute
{
}
Teraz utwórzmy konwencję, aby zastosować ten atrybut do naszego modelu:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<NonUnicode>().Any())
.Configure(c => c.IsUnicode(false));
Za pomocą tej konwencji możemy dodać atrybut NonUnicode do dowolnej z naszych właściwości ciągu, co oznacza, że kolumna w bazie danych będzie przechowywana jako varchar zamiast nvarchar.
Jedną z rzeczy, na które należy zwrócić uwagę w tej konwencji, jest to, że jeśli umieścisz atrybut NonUnicode na czymś innym niż właściwość tekstową, zgłosi wyjątek. Dzieje się tak, ponieważ nie można skonfigurować kodu IsUnicode w dowolnym typie innym niż ciąg. Gdyby zaszła taka sytuacja, możesz bardziej sprecyzować swoją konwencję, aby odfiltrować wszystko, co nie jest ciągiem znaków.
Chociaż powyższa konwencja działa do definiowania atrybutów niestandardowych, istnieje inny interfejs API, który może być znacznie łatwiejszy w użyciu, zwłaszcza gdy chcesz używać właściwości z klasy atrybutów.
W tym przykładzie zaktualizujemy nasz atrybut i zmienimy go na atrybut IsUnicode, więc wygląda następująco:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
internal class IsUnicode : Attribute
{
public bool Unicode { get; set; }
public IsUnicode(bool isUnicode)
{
Unicode = isUnicode;
}
}
Gdy to zrobimy, możemy ustawić wartość logiczną na naszym atrybucie, aby określić konwencję, czy właściwość powinna mieć wartość Unicode. Możemy to zrobić w konwencji, która już istnieje, korzystając z właściwości ClrProperty klasy konfiguracji w następujący sposób:
modelBuilder.Properties()
.Where(x => x.GetCustomAttributes(false).OfType<IsUnicode>().Any())
.Configure(c => c.IsUnicode(c.ClrPropertyInfo.GetCustomAttribute<IsUnicode>().Unicode));
Jest to wystarczająco łatwe, ale istnieje bardziej zwięzły sposób osiągnięcia tego, używając metody Having z API konwencji. Metoda Having ma parametr typu Func<PropertyInfo, T>, który akceptuje PropertyInfo tak samo jak metoda Where, ale oczekuje się, że zwróci obiekt. Jeśli zwrócony obiekt ma wartość null, właściwość nie zostanie skonfigurowana, co oznacza, że można odfiltrować właściwości tak jak "Where", ale różni się tym, że przechwytywana jest także zwrócona wartość obiektu i przekazywana do metody Configure. Działa to podobnie do następujących:
modelBuilder.Properties()
.Having(x => x.GetCustomAttributes(false).OfType<IsUnicode>().FirstOrDefault())
.Configure((config, att) => config.IsUnicode(att.Unicode));
Atrybuty niestandardowe nie są jedyną przyczyną używania metody Having. Jest to przydatne w dowolnym miejscu, w którym należy wnioskować o czymś, co filtrujesz podczas konfigurowania typów lub właściwości.
Konfigurowanie typów
Do tej pory wszystkie nasze konwencje dotyczyły właściwości, ale istnieje inny obszar interfejsu API konwencji do konfiguracji typów w twoim modelu. Doświadczenie jest podobne do konwencji, które widzieliśmy do tej pory, ale opcje w configure będą na poziomie elementu zamiast właściwości.
Jedną z rzeczy, w których konwencje na poziomie typu danych mogą być naprawdę przydatne, jest zmiana konwencji nazewnictwa tabel, aby dopasować do istniejącego schematu, który różni się od domyślnego w Entity Framework, lub do utworzenia nowej bazy danych z odmienną konwencją nazewnictwa. Aby to zrobić, najpierw potrzebujemy metody, która może akceptować typeInfo dla typu w naszym modelu i zwracać nazwę tabeli dla tego typu:
private string GetTableName(Type type)
{
var result = Regex.Replace(type.Name, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
Ta metoda przyjmuje typ i zwraca ciąg, który używa małych liter ze znakami podkreślenia zamiast CamelCase. W naszym modelu oznacza to, że klasa ProductCategory zostanie zamapowana na tabelę o nazwie product_category zamiast ProductCategories.
Gdy mamy tę metodę, możemy ją wywołać w konwencji podobnej do następującej:
modelBuilder.Types()
.Configure(c => c.ToTable(GetTableName(c.ClrType)));
Ta konwencja konfiguruje każdy typ w naszym modelu do mapowania na nazwę tabeli zwracaną z naszej metody GetTableName. Ta konwencja jest odpowiednikiem wywoływania metody ToTable dla każdej jednostki w modelu przy użyciu interfejsu API Fluent.
Jedną z rzeczy, które należy zauważyć, jest to, że po wywołaniu funkcji ToTable EF przyjmie podany przez ciebie ciąg jako dokładną nazwę tabeli, bez stosowania typowej liczby mnogiej, która zwykle jest używana podczas określania nazw tabel. Dlatego zgodnie z naszą konwencją nazwa tabeli to product_category zamiast product_categories. Możemy rozwiązać ten problem w naszej konwencji, wywołując usługę pluralizacji.
W poniższym kodzie użyjemy funkcji Rozwiązywanie zależności dodanej w EF6, aby pobrać usługę pluralizacji, której EF by użył, i użyć w liczbie mnogiej naszej nazwy tabeli.
private string GetTableName(Type type)
{
var pluralizationService = DbConfiguration.DependencyResolver.GetService<IPluralizationService>();
var result = pluralizationService.Pluralize(type.Name);
result = Regex.Replace(result, ".[A-Z]", m => m.Value[0] + "_" + m.Value[1]);
return result.ToLower();
}
Uwaga / Notatka
Ogólna wersja GetService jest metodą rozszerzenia w przestrzeni nazw System.Data.Entity.Infrastructure.DependencyResolution, należy dodać instrukcję using do swojego kontekstu, aby móc z niej korzystać.
ToTable i dziedziczenie
Inn nym istotnym aspektem metody „ToTable” jest to, że jeśli jawnie zamapujesz typ na daną tabelę, możesz zmienić strategię mapowania, jaką zastosuje EF. Jeśli wywołasz metodę ToTable dla każdego typu w hierarchii dziedziczenia, przekazując nazwę typu jako nazwę tabeli tak, jak zrobiliśmy to powyżej, zmienisz domyślną strategię mapowania Table-Per-Hierarchy (TPH) na Table-Per-Type (TPT). Najlepszym sposobem na opisanie tego jest konkretny przykład:
public class Employee
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Manager : Employee
{
public string SectionManaged { get; set; }
}
Domyślnie zarówno pracownik, jak i menedżer są mapowane na tę samą tabelę (Employees) w bazie danych. Tabela będzie zawierać zarówno pracowników, jak i menedżerów z dyskryminującą kolumną, która informuje o typie wystąpienia przechowywanego w każdym wierszu. Jest to mapowanie TPH, ponieważ istnieje tylko jedna tabela dla całej hierarchii. Jeśli jednak wywołasz metodę ToTable dla obu klas, każdy typ zostanie odwzorowany w swojej własnej tabeli, znanej również jako TPT, ponieważ każdy typ ma własną tabelę.
modelBuilder.Types()
.Configure(c=>c.ToTable(c.ClrType.Name));
Powyższy kod zostanie odwzorowany w strukturze tabeli, która wygląda następująco:
Możesz tego uniknąć i zachować domyślne mapowanie TPH na kilka sposobów:
- Wywołaj metodę ToTable z tą samą nazwą tabeli dla każdego typu w hierarchii.
- Wywołaj metodę ToTable tylko w klasie bazowej hierarchii, w naszym przykładzie, którą byłaby klasa 'employee'.
Kolejność wykonywania
Konwencje działają w sposób, w którym ostatni wygrywa, tak jak interfejs API Fluent. Oznacza to, że jeśli napiszesz dwie konwencje, które konfigurują tę samą opcję tej samej właściwości, to ostatni do wykonania wygrywa. Na przykład w kodzie poniżej maksymalnej długości wszystkich ciągów jest ustawiona wartość 500, ale następnie konfigurujemy wszystkie właściwości o nazwie Name w modelu tak, aby miały maksymalną długość 250.
modelBuilder.Properties<string>()
.Configure(c => c.HasMaxLength(500));
modelBuilder.Properties<string>()
.Where(x => x.Name == "Name")
.Configure(c => c.HasMaxLength(250));
Ponieważ konwencja ustawiająca maksymalną długość na 250 jest stosowana po tej, która ustawia wszystkie ciągi na 500, wszystkie właściwości o nazwie Name w naszym modelu będą miały maksymalną długość 250, podczas gdy inne ciągi, takie jak opisy, będą miały długość 500. Użycie konwencji w ten sposób oznacza, że można podać ogólną konwencję typów lub właściwości w modelu, a następnie przejąć je dla podzbiorów, które są różne.
W określonych przypadkach można również zastąpić konwencję za pomocą interfejsu API Fluent i adnotacji danych. W naszym przykładzie powyżej, jeśli użyliśmy interfejsu API Fluent do ustawienia maksymalnej długości właściwości, moglibyśmy umieścić ją przed lub po konwencji, ponieważ bardziej szczegółowy interfejs API Fluent wygra bardziej ogólną konwencję konfiguracji.
Konwencje wbudowane
Ponieważ konwencje niestandardowe mogą mieć wpływ na domyślne konwencje Code First, warto dodać konwencje do uruchamiania przed inną konwencją lub po niej. W tym celu można użyć metod AddBefore i AddAfter kolekcji Conventions w pochodnym obiekcie DbContext. Poniższy kod doda utworzoną wcześniej klasę konwencji, aby była uruchamiana przed wbudowaną konwencją odnajdywania kluczy.
modelBuilder.Conventions.AddBefore<IdKeyDiscoveryConvention>(new DateTime2Convention());
Najbardziej przydatne będzie to podczas dodawania konwencji, które muszą być uruchamiane przed lub po wbudowanych konwencjach, listę wbudowanych konwencji można znaleźć tutaj: System.Data.Entity.ModelConfiguration.Conventions Namespace.
Możesz również usunąć konwencje, których nie chcesz stosować do modelu. Aby usunąć konwencję, użyj metody Remove. Oto przykład usuwania nazwy PluralizingTableNameConvention.
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
}