国エンティティのネストされたコレクション(都市)を更新しようとしています。
単純なエンティティとdto:
// EF Models
public class Country
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<City> Cities { get; set; }
}
public class City
{
public int Id { get; set; }
public string Name { get; set; }
public int CountryId { get; set; }
public int? Population { get; set; }
public virtual Country Country { get; set; }
}
// DTo's
public class CountryData : IDTO
{
public int Id { get; set; }
public string Name { get; set; }
public virtual ICollection<CityData> Cities { get; set; }
}
public class CityData : IDTO
{
public int Id { get; set; }
public string Name { get; set; }
public int CountryId { get; set; }
public int? Population { get; set; }
}
そして、コード自体(簡単にするためにコンソールアプリでテストされています):
using (var context = new Context())
{
// getting entity from db, reflect it to dto
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
// add new city to dto
countryDTO.Cities.Add(new CityData
{
CountryId = countryDTO.Id,
Name = "new city",
Population = 100000
});
// change existing city name
countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name";
// retrieving original entity from db
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
// mapping
AutoMapper.Mapper.Map(countryDTO, country);
// save and expecting ef to recognize changes
context.SaveChanges();
}
このコードは例外をスローします:
操作が失敗しました:1つ以上の外部キープロパティがnull可能ではないため、関係を変更できませんでした。リレーションシップが変更されると、関連する外部キープロパティはnull値に設定されます。外部キーがnull値をサポートしていない場合は、新しい関係を定義するか、foreign-keyプロパティに別のnull以外の値を割り当てるか、無関係なオブジェクトを削除する必要があります。
最後のマッピング後のエンティティは問題なく、すべての変更を適切に反映しています。
私は解決策を見つけるのに多くの時間を費やしましたが、結果はありませんでした。助けてください。
問題は、データベースから取得しているcountry
にすでにいくつかの都市があることです。次のようにAutoMapperを使用する場合:
_// mapping
AutoMapper.Mapper.Map(countryDTO, country);
_
AutoMapperは、_IColletion<City>
_を正しく作成し(例では1つの都市を使用)、この新しいコレクションを_country.Cities
_プロパティに割り当てています。
問題は、EntityFrameworkが古い都市のコレクションをどう処理するかを認識していないことです。
実際、EFが決定することはできません。 AutoMapperを使い続けたい場合は、次のようにマッピングをカスタマイズできます。
_// AutoMapper Profile
public class MyProfile : Profile
{
protected override void Configure()
{
Mapper.CreateMap<CountryData, Country>()
.ForMember(d => d.Cities, opt => opt.Ignore())
.AfterMap(AddOrUpdateCities);
}
private void AddOrUpdateCities(CountryData dto, Country country)
{
foreach (var cityDTO in dto.Cities)
{
if (cityDTO.Id == 0)
{
country.Cities.Add(Mapper.Map<City>(cityDTO));
}
else
{
Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id));
}
}
}
}
_
Cities
に使用されるIgnore()
構成により、AutoMapperはEntityFramework
によって構築された元のプロキシ参照を保持します。
次に、AfterMap()
を使用して、思い通りのアクションを実行します。
Map
のオーバーロードを使用して、既存のエンティティを2番目のパラメーターとして渡し、市のプロキシーを最初のパラメーターとして渡すため、オートマッパーは既存のエンティティのプロパティを更新するだけです。その後、元のコードを保持できます。
_using (var context = new Context())
{
// getting entity from db, reflect it to dto
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
// add new city to dto
countryDTO.Cities.Add(new CityData
{
CountryId = countryDTO.Id,
Name = "new city",
Population = 100000
});
// change existing city name
countryDTO.Cities.FirstOrDefault(x => x.Id == 4).Name = "another name";
// retrieving original entity from db
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
// mapping
AutoMapper.Mapper.Map(countryDTO, country);
// save and expecting ef to recognize changes
context.SaveChanges();
}
_
これはそれ自体はOPに対する答えではありませんが、今日同様の問題を検討している人は AutoMapper.Collection の使用を検討する必要があります。処理に多くのコードが必要であったこれらの親子コレクションの問題をサポートします。
良い解決策や詳細が含まれていないことをお詫び申し上げますが、今はスピードアップするところです。上記のリンクに表示されているREADME.mdには、優れた単純な例があります。
これを使用するには少し書き換えが必要ですが、特に劇的にEFを使用していて、必要なコードの量を削減し、 AutoMapper.Collection.EntityFramework
。
保存の変更を行うと、EFは時間を節約するまでそれらについて都市が考慮しなかったため、すべての都市が追加されたと見なされます。そのため、EFは古い都市の外部キーにnullを設定し、更新の代わりに挿入しようとします。
ChangeTracker.Entries()
を使用すると、EFによってCRUDに加えられる変更がわかります。
既存の都市を手動で更新するだけの場合は、次のようにすることができます。
foreach (var city in country.cities)
{
context.Cities.Attach(city);
context.Entry(city).State = EntityState.Modified;
}
context.SaveChanges();
Alissonの非常に優れたソリューション。これが私の解決策です... EFは要求が更新または挿入のどちらであるかを認識していないため、RemoveRange()メソッドを使用して最初に削除し、コレクションを送信して再度挿入します。バックグラウンドでこれがデータベースの動作方法であり、この動作を手動でエミュレートできます。
これがコードです:
_//country object from request for example
_
var cities = dbcontext.Cities.Where(x=>x.countryId == country.Id);
dbcontext.Cities.RemoveRange(cities);
_/* Now make the mappings and send the object this will make bulk insert into the table related */
_
私は解決策を見つけたようです:
var countryDTO = context.Countries.FirstOrDefault(x => x.Id == 1).ToDTO<CountryData>();
countryDTO.Cities.Add(new CityData { CountryId = countryDTO.Id, Name = "new city 2", Population = 100000 });
countryDTO.Cities.FirstOrDefault(x => x.Id == 11).Name = "another name";
var country = context.Countries.FirstOrDefault(x => x.Id == 1);
foreach (var cityDTO in countryDTO.Cities)
{
if (cityDTO.Id == 0)
{
country.Cities.Add(cityDTO.ToEntity<City>());
}
else
{
AutoMapper.Mapper.Map(cityDTO, country.Cities.SingleOrDefault(c => c.Id == cityDTO.Id));
}
}
AutoMapper.Mapper.Map(countryDTO, country);
context.SaveChanges();
このコードは編集されたアイテムを更新し、新しいアイテムを追加します。しかし、今のところ検出できない落とし穴があるのでしょうか?