web-dev-qa-db-ja.com

EF&Automapper。ネストされたコレクションを更新する

国エンティティのネストされたコレクション(都市)を更新しようとしています。

単純なエンティティと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以外の値を割り当てるか、無関係なオブジェクトを削除する必要があります。

最後のマッピング後のエンティティは問題なく、すべての変更を適切に反映しています。

私は解決策を見つけるのに多くの時間を費やしましたが、結果はありませんでした。助けてください。

14
Akmal Salikhov

問題は、データベースから取得しているcountryにすでにいくつかの都市があることです。次のようにAutoMapperを使用する場合:

_// mapping 
        AutoMapper.Mapper.Map(countryDTO, country);
_

AutoMapperは、_IColletion<City>_を正しく作成し(例では1つの都市を使用)、この新しいコレクションを_country.Cities_プロパティに割り当てています。

問題は、EntityFrameworkが古い都市のコレクションをどう処理するかを認識していないことです。

  • 古い都市を削除し、新しいコレクションのみを想定する必要がありますか?
  • 2つのリストをマージして、両方をデータベースに保持する必要がありますか?

実際、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()を使用して、思い通りのアクションを実行します。

  • 新しい都市の場合、[〜#〜] dto [〜#〜]からEntityにマッピングし(AutoMapperが新しいインスタンスを作成)、国のコレクションに追加します。
  • 既存の都市の場合、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();
    }
_
29
Alisson

これはそれ自体はOPに対する答えではありませんが、今日同様の問題を検討している人は AutoMapper.Collection の使用を検討する必要があります。処理に多くのコードが必要であったこれらの親子コレクションの問題をサポートします。

良い解決策や詳細が含まれていないことをお詫び申し上げますが、今はスピードアップするところです。上記のリンクに表示されているREADME.mdには、優れた単純な例があります。

これを使用するには少し書き換えが必要ですが、特に劇的にEFを使用していて、必要なコードの量を削減し、 AutoMapper.Collection.EntityFramework

3
pbarranis

保存の変更を行うと、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();
1
esiprogrammer

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 */_

0
Raul Gonzalez

私は解決策を見つけたようです:

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();

このコードは編集されたアイテムを更新し、新しいアイテムを追加します。しかし、今のところ検出できない落とし穴があるのでしょうか?

0
Akmal Salikhov