Модел на хранилище: Внедряване и отложено зареждане на моделни взаимоотношения

Имам приложение, което се занимава с продукти и продуктови категории. За всеки от тях имам модели, дефинирани с помощта на POCO.

// Represents a product.
class Product {
  public virtual int ID { get; set; }
  public virtual string Name { get; set; }
  public virtual ProductCategory Category { get; set; }
}

// Represents a product category.
class ProductCategory {
  public virtual int ID { get; set; }
  public virtual string Name { get; set; }
  public virtual IEnumerable<Product> Products { get; set; }
}

Приложението използва хранилище за достъп до тези модели

// The interface implemented by the application's repository
interface IProductRepository {
  IEnumerable<Product> GetAllProducts();

  void Add(Product product);
  void Remove(Product product);
  void Save(Product product);
}

В класа Product, свойството с име Category от тип ProductCategory трябва да се зарежда само когато е необходимо/достъпено (мързеливо зареждане). Искам моите модели да останат POCO и да съдържат само структурата на модела.

Правилният подход ли прилагам?
Трябва ли да имам само идентификатора на категорията в продуктовия клас и да използвам отделно хранилище за продуктови категории, за да заредя категорията?

Внедряване на отложено зареждане и връзки
Засега моето внедряване на интерфейса на хранилището връща обект от тип, който разширява типа продукт и има поддръжка за отложено зареждане през екземпляра на хранилището.

Кой трябва да отговаря за зареждането на продуктовата категория?

Интересувам се как трябва да си взаимодействат хранилищата за продукти и категории, за да се постигне мързеливото зареждане? Трябва ли да се препращат един към друг, или трябва да имам основно хранилище с двете подхранилища и да предам това на моите типове разширени модели?

Какъв подход бихте възприели? (всякакви предложения и критики са добре дошли)


Трябва да отбележа, че искам приложението да е разширяемо и всички интерфейси за хранилищата и самите модели ще бъдат в отделна сглобка. Това означава, че разширителят няма да има директен достъп до дефиницията на класа на модела.


person Cristian Toma    schedule 10.05.2011    source източник


Отговори (4)


Няколко забележки и моите мнения:

1)

Трябва ли да имам само идентификатора на категорията в продуктовия клас и да използвам отделно хранилище за продуктови категории, за да заредя категорията?

Не. Използвате ORM (поне предполагам, че го правите), за да можете да моделирате връзки чрез препратки между екземпляри на класове, а не чрез идентификатори, които използвате след това, за да правите заявки по релационен начин. Пренасянето на идеята ви до последното следствие би означавало, че премахвате всички навигационни свойства изобщо от класовете на модела и имате само скаларни свойства и някои от тях действат като ключове между обекти. Това е само "R" в ORM.

2)

Засега моята реализация на интерфейса на хранилището връща обект от тип, който разширява типа продукт и има поддръжка за отложено зареждане през екземпляра на хранилището.

Не съм сигурен какво точно означава това. (Бих искал да видя кодов фрагмент как да направите това.) Но моето предположение е, че във вашия извлечен Product клас инжектирате по някакъв начин препратка към хранилището, като така:

public class ProductProxy : Product
{
    private IProductRepository _productRepo;

    public ProductProxy(IProductRepository productRepo)
    {
        _productRepo = productRepo;
    }

    // now you use _productRepo to lazily load something on request, do you?
}

Е, очевидно сега е проблем да се заредят категориите, тъй като IProductRepository няма методи за достъп до тях.

3)

Интересувам се как трябва да си взаимодействат хранилищата за продукти и категории, за да се постигне мързеливото зареждане? Трябва ли да се препращат един към друг, или трябва да имам основно хранилище с двете подхранилища и да предам това на моите типове разширени модели?

Вашето ProductRepository и CategoryRepository изглеждат като екземпляри на общо хранилище, което отговаря само за един тип обект (в EF 4.1 това би било подобно на DbSet<T>, където T е Product или Category съответно).

Бих избягвал да има препратки между тези хранилища, тъй като това може да доведе до адски сложни репо-препратки всеки път, когато добавяте нови обекти или свойства за навигация.

Виждам още два варианта:

  • (По принцип това, което вече споменахте) Наличие на хранилище, което отговаря за Product и Category заедно. Все още можете да имате вашите общи хранилища, но бих ги разглеждал повече като вътрешни помощни хранилища и бих ги използвал само като частни членове вътре в основното хранилище. По този начин можете да имате група от хранилища, всяко от които отговаря за някои тясно свързани обекти.

  • Въведете Unit of Work, който е в състояние да създаде всичките ви общи хранилища (отново в EF 4.1 това би било нещо като фабричния метод DbContext.Set<T>(), където DbContext е работната единица) и след това инжектирайте тази единица работа във вашите извлечени екземпляри:

    public class ProductProxy : Product
    {
        private IUnitOfWork _unitOfWork;
    
        public ProductProxy(IUnitOfWork unitOfWork)
        {
            _unitOfWork = unitOfWork;
        }  
    
        public Category Category
        {
            get
            {
                // ...
                var productRepo = _unitOfWork.CreateGenericRepo<Product>();
                var categoryRepo = _unitOfWork.CreateGenericRepo<Category>();
                // you can pull out the repos you need and work with them
            }
            set { ... }
        }
    }
    

Бих предпочел втория вариант, защото при първия може да се окажете в огромни хранилища, за да поддържате зареждането на всички възможни връзки. Помислете за: Поръчката има артикули за поръчка, артикулът за поръчка има продукт, продуктът има категория, поръчката има клиент, клиентът има списък с адреси, адресът има списък с лица за контакт и така нататък, и така нататък...

4) (защото вие също поискахте критика)

Пишете ли свой собствен ORM или пишете приложение? Вашият дизайн отива в посока, която може да стане много сложна и вие преоткривате колелото според мен. Ако планирате да използвате EF или NHibernate (или други ORM), тогава вие създавате функции, които вече са налични извън кутията, вие само поставяте абстракции върху тях, които не добавят стойност. Мързеливото зареждане чрез динамични проксита се случва прозрачно в смисъл, че никога не работите изрично с тези проксита във вашия код, вие винаги работите с вашите POCO обекти. Те са невидими и съществуват само по време на изпълнение. Защо искате да разработите своя собствена инфраструктура за мързеливо зареждане?

person Slauma    schedule 10.05.2011
comment
#2. Предположението ти е абсолютно правилно, точно това искам да направя. #4. Опитвам се да направя приложението разширяемо, като предлагам .dll с необходимите интерфейси и модели за внедряване на хранилище (много сложно, отколкото в този пример) и след това зареждам изпълнението с MEF. За вътрешно внедряване използвам EF Code First, така че моите хранилища ще бъдат чисти (оставяйки тежката работа на EF). Благодаря ви много за този страхотен отговор! - person Cristian Toma; 11.05.2011

NHibernate поддържа отложено зареждане чрез прокси обекти (използвайки Castle DynamicProxy по подразбиране), които подкласират класовете, които се манипулират от хранилището. NHibernate изисква да маркирате членовете като virtual, за да поддържате този сценарий. Не съм сигурен кой компонент инициира извикването за зареждане, когато е необходимо, но подозирам, че това е прокси екземплярът.

Аз лично смятам, че маркирането на членове като virtual е малка цена за функцията за отложено зареждане

person Russ Cam    schedule 10.05.2011
comment
@Russ Наясно съм с тези рамки (също Entity Code First), затова маркирах всички членове в модела като виртуални, но също така се интересувам как трябва да си взаимодействат хранилищата? - person Cristian Toma; 10.05.2011
comment
В идеалния случай трябва да имате хранилища само за вашите сборни корени. Разгледайте stackoverflow.com/questions/1958621/whats-an- aggregate-root. Така че, ако приемем, че вашето хранилище зарежда Product екземпляр с категория прокси върху него, когато get се извика на свойството, проксито трябва да зареди реалния екземпляр от DB и да го присвои на свойството. Може би прокси обектът трябва да съдържа вътрешно идентификатора на сборния корен, към който е свързан. - person Russ Cam; 10.05.2011
comment
Разбирам, но как трябва да управлявам екземплярите ProductsRepository и ProductCategoriesRepository и да ги предам на моделите. Къде трябва да живеят, за да могат да се справят един с друг? - person Cristian Toma; 10.05.2011
comment
Ако приемем MVC/MVP архитектура, хранилището ще бъде предадено на контролера/представителя. Хранилището няма да препраща директно към моделите или домейна. По същия начин моделът на домейн не трябва да знае за хранилището. Може да се наложи да абстрахирате компонентите, които извършват действителните извиквания на DB, в отделна сглобка, за да можете да ги препращате от хранилище и прокси сглобки. - person Russ Cam; 10.05.2011
comment
Това изпълнение е агностик на средата. Специфичните за хранилището модели (типовете, които разширяват базовите типове модели, реализирани от разширителя) трябва да препращат към хранилищата, за да могат да постигнат отложено зареждане. Отново въпросът ми се отнася до това къде живеят екземплярите на ProductsRepository и ProductCategoriesRepository, така че когато даден продукт бъде извлечен от хранилището, към него се предава екземпляр към хранилището на категориите, така че да може да зареди подходящата категория, когато е необходимо през него. Надявам се, че това прави нещата по-ясни за това, което искам да знам, благодаря. - person Cristian Toma; 10.05.2011
comment
Не мисля, че ProductCategoriesRepository трябва да се връща обратно в допълнение към продукта, но изпълнението на проксито трябва да използва/препраща към компонент, който също се използва от хранилищата за извършване на действителните извиквания към източника на данни (база данни) - person Russ Cam; 10.05.2011
comment
Но тогава няма ли да имам дублиран код за извличане на категории? В хранилището на категориите и в самия разширен/прокси модел? Отговорът на @Slauma отговори на въпросите ми. Благодаря и на вас! - person Cristian Toma; 11.05.2011

Правя нещо много подобно и в моя случай имам хранилища както за продукт, така и за продуктова категория. Никой извън хранилището не знае как се зареждат нещата - мързеливи или каквото и да е - така че можете да го промените по-късно, ако искате. В моя случай, аз нетърпеливо зареждам категориите, тъй като те са достъпни толкова много, и зареждам при поискване продуктите и ги кеширам за около час.

person n8wrl    schedule 10.05.2011

Искам моите модели да останат POCO и да съдържат само структурата на модела.

Бих попитал защо формата на данните е важна за тези класове?

Като цяло това означава, че просто излагате нашата схема на база данни директно на цялото ви приложение, с много тънък фурнир на „модел“ отгоре.

Бих предложил данните, съдържащи се в даден обект, да са частни и трябва да бъдат капсулирани и че поведението е това, което трябва да бъде фокусът на обектите.

Освен това е по-лесно и по-ясно да заредите всички данни, които ще ви трябват наведнъж. Ако потенциалното количество данни е твърде голямо, тогава бих предположил, че имате твърде много отговорности, възложени на един обект и ще трябва или допълнително да прецизирате своя модел, или да добавите механизъм за извличане на обект въз основа на контекста, в който се намирате използвайки го.

Това, което имам предвид, е, че ако правите заявка за показване на данни, имате нужда от известен набор от данни. Можете да поискате подобен, но различен набор от данни в друг контекст. Продължавайки по-нататък, ако трябва да извършите действие или модификация, вашият обект трябва да изложи определено поведение. Но в различен контекст - когато извличате обекта, за да извършите различно действие или модификация, тогава имате нужда от изложено различно конкретно поведение.

Опитът да се комбинират всички тези различни употреби в един единствен договор или интерфейс води до повече или по-малко божествени обекти.

Имате нужда само от тази колекция от категории за определени дисплеи и определени действия. Просто причинява болка, опитвайки се да бъде изложена през цялото време.

person quentin-starin    schedule 10.05.2011