Entity Framework, Code First, обновление отношений "один ко многим" с независимыми ассоциациями
Мне потребовалось слишком много времени, чтобы найти решение сценария, описанного ниже. То, что казалось бы простым делом, оказалось довольно сложным. Вопрос в том:
Используя Entity Framework 4.1 (подход Code First) и "Независимые ассоциации", как назначить другой конец существующим отношениям "многие к одному" в "отдельном" сценарии ( Asp.Net в моем случае).
Модель:
Я понимаю, что использование отношений ForeignKey вместо независимых ассоциаций было бы возможным вариантом, но я предпочел, чтобы в моих Pocos не было реализации ForeignKey.
У клиента есть одна или несколько целей:
public class Customer:Person
{
public string Number { get; set; }
public string NameContactPerson { get; set; }
private ICollection<Target> _targets;
// Independent Association
public virtual ICollection<Target> Targets
{
get { return _targets ?? (_targets = new Collection<Target>()); }
set { _targets = value; }
}
}
У цели есть один клиент:
public class Target:EntityBase
{
public string Name { get; set; }
public string Description { get; set; }
public string Note { get; set; }
public virtual Address Address { get; set; }
public virtual Customer Customer { get; set; }
}
Клиент происходит из класса Person:
public class Person:EntityBase
{
public string Salutation { get; set; }
public string Title { get; set; }
public string FirstName { get; set; }
public string LastName { get; set ; }
public string Telephone1 { get; set; }
public string Telephone2 { get; set; }
public string Email { get; set; }
public virtual Address Address { get; set; }
}
Класс EntityBase предоставляет некоторые общие свойства:
public abstract class EntityBase : INotifyPropertyChanged
{
public EntityBase()
{
CreateDate = DateTime.Now;
ChangeDate = CreateDate;
CreateUser = HttpContext.Current.User.Identity.Name;
ChangeUser = CreateUser;
PropertyChanged += EntityBase_PropertyChanged;
}
public void EntityBase_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (Id != new Guid())
{
ChangeDate = DateTime.Now;
ChangeUser = HttpContext.Current.User.Identity.Name;
}
}
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, e);
}
public event PropertyChangedEventHandler PropertyChanged;
public Guid Id { get; set; }
public DateTime CreateDate { get; set; }
public DateTime? ChangeDate { get; set; }
public string CreateUser { get; set; }
public string ChangeUser { get; set; }
}
Контекст:
public class TgrDbContext : DbContext
{
public DbSet<Person> Persons { get; set; }
public DbSet<Address> Addresses { get; set; }
public DbSet<Customer> Customers { get; set; }
public DbSet<Target> Targets { get; set; }
public DbSet<ReportRequest> ReportRequests { get; set; }
// If OnModelCreating becomes to big, use "Model Configuration Classes"
//(derived from EntityTypeConfiguration) instead
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<Person>().HasOptional(e => e.Address);
modelBuilder.Entity<Customer>().HasMany(c => c.Targets).WithRequired(t => t.Customer);
}
public static ObjectContext TgrObjectContext(TgrDbContext tgrDbContext)
{
return ((IObjectContextAdapter)tgrDbContext).ObjectContext;
}
}
2 ответа
Я ждал ответа @Martin, потому что есть больше решений для этой проблемы. Вот еще один (по крайней мере, он работает с API ObjectContext, поэтому он должен работать и с API DbContext):
// Existing customer
var customer = new Customer() { Id = customerId };
// Another existing customer
var customer2 = new Customer() { Id = customerId2 };
var target = new Target { ID = oldTargetId };
// Make connection between target and old customer
target.Customer = customer;
// Attach target with old customer
context.Targets.Attach(target);
// Attach second customer
context.Customers.Attach(customer2);
// Set customer to a new value on attached object (it will delete old relation and add new one)
target.Customer = customer2;
// Change target's state to Modified
context.Entry(target).State = EntityState.Modified;
context.SaveChanges();
Проблема здесь заключается в модели внутреннего состояния и валидации состояния внутри EF. Сущность в неизмененном или измененном состоянии с обязательным отношением (на многих сторонах) не может иметь независимую ассоциацию в добавленном состоянии, когда нет другого в удаленном состоянии. Измененное состояние для ассоциации вообще не допускается.
По этой теме можно найти много информации; на stackru я нашел идеи Ладислава Мрнки особенно полезными. Более подробную информацию можно найти здесь: улучшения NTier для Entity Framework и здесь Что нового в Entity Framework 4?
В моем проекте (Asp.Net Webforms) у пользователя есть возможность заменить Customer, назначенный объекту Target, другим (существующим) объектом Customer. Эта транзакция выполняется элементом управления FormView, связанным с ObjectDataSource. ObjectDataSource связывается со слоем BusinessLogic проекта, который, в свою очередь, передает транзакцию в класс репозитория для объекта Target на уровне DataAccess. Метод Update для объекта Target в классе репозитория выглядит следующим образом:
public void UpdateTarget(Target target, Target origTarget)
{
try
{
// It is not possible to handle updating one to many relationships (i.e. assign a
// different Customer to a Target) with "Independent Associations" in Code First.
// (It is possible when using "ForeignKey Associations" instead of "Independent
// Associations" but this brings about a different set of problems.)
// In order to update one to many relationships formed by "Independent Associations"
// it is necessary to resort to using the ObjectContext class (derived from an
// instance of DbContext) and 'manually' update the relationship between Target and Customer.
// Get ObjectContext from DbContext - ((IObjectContextAdapter)tgrDbContext).ObjectContext;
ObjectContext tgrObjectContext = TgrDbContext.TgrObjectContext(_tgrDbContext);
// Attach the original origTarget and update it with the current values contained in target
// This does NOT update changes that occurred in an "Independent Association"; if target
// has a different Customer assigned than origTarget this will go unrecognized
tgrObjectContext.AttachTo("Targets", origTarget);
tgrObjectContext.ApplyCurrentValues("Targets", target);
// This will take care of changes in an "Independent Association". A Customer has many
// Targets but any Target has exactly one Customer. Therefore the order of the two
// ChangeRelationshipState statements is important: Delete has to occur first, otherwise
// Target would have temporarily two Customers assigned.
tgrObjectContext.ObjectStateManager.ChangeRelationshipState(
origTarget,
origTarget.Customer,
o => o.Customer,
EntityState.Deleted);
tgrObjectContext.ObjectStateManager.ChangeRelationshipState(
origTarget,
target.Customer,
o => o.Customer,
EntityState.Added);
// Commit
tgrObjectContext.Refresh(RefreshMode.ClientWins, origTarget);
tgrObjectContext.SaveChanges();
}
catch (Exception)
{
throw;
}
}
Это работает для метода Update для целевого объекта. Примечательно, что процедура вставки нового целевого объекта намного проще. DbContext правильно распознает клиентскую сторону независимой ассоциации и передает изменения в базу данных без лишних слов. Метод Insert в классе репозитория выглядит следующим образом:
public void InsertTarget(Target target)
{
try
{
_tgrDbContext.Targets.Add(target);
_tgrDbContext.SaveChanges();
}
catch (Exception)
{
throw;
}
}
Надеюсь, это будет полезно для кого-то, имеющего дело с подобной задачей. Если вы заметили проблему с этим подходом, описанным выше, пожалуйста, дайте мне знать в ваших комментариях. Спасибо!