次のようなクラスが1つあるとします。
public class Entity
{
public IList<string> SomeListOfValues { get; set; }
// Other code
}
ここで、EF Core Code Firstを使用してこれを永続化し、SQL ServerのようなRDMBSを使用するとします。
考えられるアプローチの1つは、文字列をラップするラッパークラスWraper
を作成することです。
public class Wraper
{
public int Id { get; set; }
public string Value { get; set; }
}
そして、クラスをリファクタリングして、Wraper
オブジェクトのリストに依存するようにします。その場合、EFはEntity
のテーブルとWraper
のテーブルを生成し、「1対多」の関係を確立します。エンティティごとに一連のラッパーがあります。
これは機能しますが、持続性の懸念から非常に単純なモデルを変更しているため、このアプローチはあまり好きではありません。確かに、ドメインモデルとコードだけを考えると、永続性がなければ、Wraper
クラスはまったく意味がありません。
ラッパークラスを作成する以外に、EF Core Code Firstを使用して、文字列のリストを持つ1つのエンティティをRDBMSに永続化する他の方法はありますか?もちろん、最終的には同じことを行う必要があります。文字列を保持するために別のテーブルを作成し、「1対多」の関係を構築する必要があります。ドメインモデルでラッパークラスをコーディングする必要なく、EF Coreでこれを実行したいだけです。
これは、Entity Framework Core2.1から始まるはるかに簡単な方法で実現できます。 EFは 値の変換 をサポートするようになりました。これは、プロパティをストレージ用に別のタイプにマップする必要があるこのようなシナリオに特に対処するためです。
文字列のコレクションを永続化するには、次の方法でDbContext
を設定できます。
protected override void OnModelCreating(ModelBuilder builder)
{
var splitStringConverter = new ValueConverter<IEnumerable<string>, string>(v => string.Join(";", v), v => v.Split(new[] { ';' }));
builder.Entity<Entity>().Property(nameof(Entity.SomeListOfValues)).HasConversion(splitStringConverter);
}
このソリューションは、DBの懸念でビジネスクラスを散らかさないことに注意してください。
言うまでもなく、このソリューションでは、文字列に区切り文字が含まれないようにする必要があります。ただし、もちろん、カスタムロジックを使用して変換(JSONとの間の変換など)を行うこともできます。
もう1つの興味深い事実は、null値が変換ルーチンに渡されるのではなくではなく、フレームワーク自体によって処理されることです。したがって、nullチェックについて心配する必要はありません。
あなたは正しいです、あなたは永続性の懸念でドメインモデルを散らかしたくないです。実は、ドメインと永続性に同じモデルを使用している場合、問題を回避することはできません。特にEntity Frameworkを使用しています。
解決策は、データベースをまったく意識せずにドメインモデルを構築することです。次に、翻訳を担当する別のレイヤーを構築します。 「リポジトリ」パターンの線に沿った何か。
もちろん、今では2倍の作業があります。したがって、モデルをクリーンに保つことと余分な作業を行うことの間の適切なバランスを見つけるのはあなた次第です。ヒント:大きなアプリケーションでは、余分な作業は価値があります。
便利な AutoMapper をリポジトリで使用して、物事をきちんと保ちながらこれを実現できます。
何かのようなもの:
MyEntity.cs
public class MyEntity
{
public int Id { get; set; }
public string SerializedListOfStrings { get; set; }
}
MyEntityDto.cs
public class MyEntityDto
{
public int Id { get; set; }
public IList<string> ListOfStrings { get; set; }
}
Startup.csでAutoMapperマッピング構成をセットアップします。
Mapper.Initialize(cfg => cfg.CreateMap<MyEntity, MyEntityDto>()
.ForMember(x => x.ListOfStrings, opt => opt.MapFrom(src => src.SerializedListOfStrings.Split(';'))));
Mapper.Initialize(cfg => cfg.CreateMap<MyEntityDto, MyEntity>()
.ForMember(x => x.SerializedListOfStrings, opt => opt.MapFrom(src => string.Join(";", src.ListOfStrings))));
最後に、MyEntityRepository.csのマッピングを使用して、ビジネスロジックが永続化のためにリストがどのように処理されるかを認識したり気にする必要がないようにします。
public class MyEntityRepository
{
private readonly AppDbContext dbContext;
public MyEntityRepository(AppDbContext context)
{
dbContext = context;
}
public MyEntityDto Create()
{
var newEntity = new MyEntity();
dbContext.MyEntities.Add(newEntity);
var newEntityDto = Mapper.Map<MyEntityDto>(newEntity);
return newEntityDto;
}
public MyEntityDto Find(int id)
{
var myEntity = dbContext.MyEntities.Find(id);
if (myEntity == null)
return null;
var myEntityDto = Mapper.Map<MyEntityDto>(myEntity);
return myEntityDto;
}
public MyEntityDto Save(MyEntityDto myEntityDto)
{
var myEntity = Mapper.Map<MyEntity>(myEntityDto);
dbContext.MyEntities.Save(myEntity);
return Mapper.Map<MyEntityDto>(myEntity);
}
}
これは遅いかもしれませんが、それが誰に役立つかは決してわかりません。前の回答に基づいて私の解決策を見る
まず、この参照が必要になりますusing System.Collections.ObjectModel;
次に、ObservableCollection<T>
と標準リストの暗黙の演算子オーバーロードを追加します
public class ListObservableCollection<T> : ObservableCollection<T>
{
public ListObservableCollection() : base()
{
}
public ListObservableCollection(IEnumerable<T> collection) : base(collection)
{
}
public ListObservableCollection(List<T> list) : base(list)
{
}
public static implicit operator ListObservableCollection<T>(List<T> val)
{
return new ListObservableCollection<T>(val);
}
}
次に、抽象EntityString
クラスを作成します(ここで良いことが起こります)
public abstract class EntityString
{
[NotMapped]
Dictionary<string, ListObservableCollection<string>> loc = new Dictionary<string, ListObservableCollection<string>>();
protected ListObservableCollection<string> Getter(ref string backingFeild, [CallerMemberName] string propertyName = null)
{
var file = backingFeild;
if ((!loc.ContainsKey(propertyName)) && (!string.IsNullOrEmpty(file)))
{
loc[propertyName] = GetValue(file);
loc[propertyName].CollectionChanged += (a, e) => SetValue(file, loc[propertyName]);
}
return loc[propertyName];
}
protected void Setter(ref string backingFeild, ref ListObservableCollection<string> value, [CallerMemberName] string propertyName = null)
{
var file = backingFeild;
loc[propertyName] = value;
SetValue(file, value);
loc[propertyName].CollectionChanged += (a, e) => SetValue(file, loc[propertyName]);
}
private List<string> GetValue(string data)
{
if (string.IsNullOrEmpty(data)) return new List<string>();
return data.Split(';').ToList();
}
private string SetValue(string backingStore, ICollection<string> value)
{
return string.Join(";", value);
}
}
次に、それをそのように使用します
public class Categorey : EntityString
{
public string Id { get; set; }
public string Name { get; set; }
private string descriptions = string.Empty;
public ListObservableCollection<string> AllowedDescriptions
{
get
{
return Getter(ref descriptions);
}
set
{
Setter(ref descriptions, ref value);
}
}
public DateTime Date { get; set; }
}
新しいStringBackedList
クラスを作成して、可能な解決策を実装しました。実際のリストの内容は文字列で裏付けられています。 Newtonsoft.Json をシリアライザとして使用して、リストが変更されるたびにバッキング文字列を更新することで機能します(プロジェクトで既に使用していますが、すべて機能します)。
次のようなリストを使用します。
_public class Entity
{
// that's what stored in the DB, and shouldn't be accessed directly
public string SomeListOfValuesStr { get; set; }
[NotMapped]
public StringBackedList<string> SomeListOfValues
{
get
{
// this can't be created in the ctor, because the DB isn't read yet
if (_someListOfValues == null)
{
// the backing property is passed 'by reference'
_someListOfValues = new StringBackedList<string>(() => this.SomeListOfValuesStr);
}
return _someListOfValues;
}
}
private StringBackedList<string> _someListOfValues;
}
_
これがStringBackedList
クラスの実装です。使いやすくするために、バッキングプロパティは this solution を使用して参照渡しされます。
_using Newtonsoft.Json;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
namespace Model
{
public class StringBackedList<T> : IList<T>
{
private readonly Accessor<string> _backingStringAccessor;
private readonly IList<T> _backingList;
public StringBackedList(Expression<Func<string>> expr)
{
_backingStringAccessor = new Accessor<string>(expr);
var initialValue = _backingStringAccessor.Get();
if (initialValue == null)
_backingList = new List<T>();
else
_backingList = JsonConvert.DeserializeObject<IList<T>>(initialValue);
}
public T this[int index] {
get => _backingList[index];
set { _backingList[index] = value; Store(); }
}
public int Count => _backingList.Count;
public bool IsReadOnly => _backingList.IsReadOnly;
public void Add(T item)
{
_backingList.Add(item);
Store();
}
public void Clear()
{
_backingList.Clear();
Store();
}
public bool Contains(T item)
{
return _backingList.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
_backingList.CopyTo(array, arrayIndex);
}
public IEnumerator<T> GetEnumerator()
{
return _backingList.GetEnumerator();
}
public int IndexOf(T item)
{
return _backingList.IndexOf(item);
}
public void Insert(int index, T item)
{
_backingList.Insert(index, item);
Store();
}
public bool Remove(T item)
{
var res = _backingList.Remove(item);
if (res)
Store();
return res;
}
public void RemoveAt(int index)
{
_backingList.RemoveAt(index);
Store();
}
IEnumerator IEnumerable.GetEnumerator()
{
return _backingList.GetEnumerator();
}
public void Store()
{
_backingStringAccessor.Set(JsonConvert.SerializeObject(_backingList));
}
}
// this class comes from https://stackoverflow.com/a/43498938/2698119
public class Accessor<T>
{
private Action<T> Setter;
private Func<T> Getter;
public Accessor(Expression<Func<T>> expr)
{
var memberExpression = (MemberExpression)expr.Body;
var instanceExpression = memberExpression.Expression;
var parameter = Expression.Parameter(typeof(T));
if (memberExpression.Member is PropertyInfo propertyInfo)
{
Setter = Expression.Lambda<Action<T>>(Expression.Call(instanceExpression, propertyInfo.GetSetMethod(), parameter), parameter).Compile();
Getter = Expression.Lambda<Func<T>>(Expression.Call(instanceExpression, propertyInfo.GetGetMethod())).Compile();
}
else if (memberExpression.Member is FieldInfo fieldInfo)
{
Setter = Expression.Lambda<Action<T>>(Expression.Assign(memberExpression, parameter), parameter).Compile();
Getter = Expression.Lambda<Func<T>>(Expression.Field(instanceExpression, fieldInfo)).Compile();
}
}
public void Set(T value) => Setter(value);
public T Get() => Getter();
}
}
_
警告:バッキング文字列は、リスト自体が変更されたときにのみ更新されます。直接アクセス(リストインデクサーなど)を介してリスト要素を更新するには、Store()
メソッドを手動で呼び出す必要があります。