Entity Framework Entities - некоторые данные из веб-службы - лучшая архитектура?

10

В настоящее время мы используем Entity Framework в качестве ORM для нескольких веб-приложений, и до сих пор она подходила нам, поскольку все наши данные хранятся в одной базе данных. Мы используем шаблон репозитория, и у нас есть службы (уровень домена), которые используют их и возвращают сущности EF непосредственно в контроллеры ASP.NET MVC.

Однако возникло требование использовать сторонний API (через веб-сервис), который даст нам дополнительную информацию, касающуюся пользователя в нашей базе данных. В нашей локальной базе данных пользователей мы будем хранить внешний идентификатор, который мы можем предоставить API для получения дополнительной информации. Доступно довольно мало информации, но для простоты один из них относится к компании пользователя (имя, менеджер, номер, должность, местоположение и т. Д.). Эта информация будет использоваться в разных местах наших веб-приложений, а не в одном месте.

Поэтому мой вопрос: где лучше всего заполнить эту информацию и получить к ней доступ? Так как он используется в разных местах, на самом деле нецелесообразно извлекать его на разовой основе, где бы мы ни использовались в веб-приложении, - поэтому имеет смысл возвращать эти дополнительные данные со слоя домена.

Сначала я хотел создать класс модели-оболочки, который будет содержать сущность EF (EFUser) и новый класс «ApiUser», содержащий новую информацию - и когда мы получим пользователя, мы получим EFUser, а затем получим дополнительный информация из API и заполнить объект ApiUser. Однако, хотя это было бы хорошо для получения отдельных пользователей, оно падает при получении нескольких пользователей. Мы не можем использовать API при получении списка пользователей.

Моей второй мыслью было просто добавить одноэлементный метод к сущности EFUser, который возвращает ApiUser, и просто заполнить его при необходимости. Это решает вышеуказанную проблему, поскольку мы получаем к ней доступ только тогда, когда она нам нужна.

Или последняя мысль заключалась в том, чтобы сохранить локальную копию данных в нашей базе данных и синхронизировать ее с API, когда пользователь входит в систему. Это минимальная работа, так как это всего лишь процесс синхронизации - и у нас нет накладных расходов на попадание БД и API каждый раз, когда мы хотим получить информацию о пользователе. Однако это означает хранение данных в двух местах, а также означает, что данные устарели для любого пользователя, который не вошел в систему в течение некоторого времени.

У кого-нибудь есть какие-либо советы или предложения о том, как лучше всего справиться с таким сценарием?

stevehayter
источник
it's not really sensible to fetch it on an ad-hoc basis-- Почему? По причинам производительности?
Роберт Харви
Я не имею в виду использование API на разовой основе - я просто имею в виду сохранение существующей структуры сущностей в том виде, в каком она есть, а затем вызову API ad-hoc в веб-приложении, когда это необходимо - я просто подразумевал, что это не будет разумно, поскольку это должно быть сделано во многих местах.
Stevehayter

Ответы:

3

Ваш случай

В вашем случае все три варианта являются жизнеспособными. Я думаю, что лучший вариант - это, вероятно, синхронизировать ваши источники данных там, где приложение asp.net даже не знает об этом. То есть избегайте двух выборок на переднем плане каждый раз, синхронизируйте API с БД в режиме без вывода сообщений). Так что, если это приемлемый вариант в вашем случае - я говорю, сделайте это.

Решение, в котором вы делаете выборку «один раз», как предлагает другой ответ, не кажется очень жизнеспособным, поскольку оно нигде не сохраняет ответ, а ASP.NET MVC будет просто делать выборку для каждого запроса снова и снова.

Я бы избегал синглтона, я не думаю, что это хорошая идея по многим обычным причинам.

Если третий вариант не жизнеспособен - один из вариантов являются ленивой загрузкой. То есть, чтобы класс расширял сущность, и чтобы он попадал в API по мере необходимости . Это очень опасная абстракция, так как это еще более волшебное и неочевидное состояние.

Я думаю, это действительно сводится к нескольким вопросам:

  • Как часто меняются данные вызова API? Не часто? Третий вариант. Часто? Вдруг третий вариант не слишком жизнеспособен. Я не уверен, что я так же против специальных звонков, как вы.
  • Насколько дорогой вызов API? Вы платите за звонок? Они быстрые? Бесплатно? Если они быстрые, звонки каждый раз могут работать, если они медленные, вам нужно иметь какое-то предсказание и делать звонки. Если они стоят денег - это большой стимул для кеширования.
  • Как быстро должно быть время отклика? Очевидно, что чем быстрее, тем лучше, но в некоторых случаях жертвовать скоростью ради простоты может стоить того, особенно если это не касается непосредственно пользователя.
  • Насколько данные API отличаются от ваших данных? Это две концептуально разные вещи? Если это так, то может быть даже лучше просто представить API снаружи, а не возвращать результат API вместе с результатом, и позволить другой стороне выполнить второй вызов и обработать его.

Несколько слов о разделении интересов

Позвольте мне поспорить с тем, что Бобсон говорит о разделении проблем здесь. В конце концов - внедрение такой логики в сущности, подобные этим, так же плохо нарушает разделение интересов.

Наличие такого хранилища нарушает разделение проблем так же плохо , помещая логику, ориентированную на представление, на уровень бизнес-логики. Ваш репозиторий теперь внезапно узнает о вещах, связанных с презентацией, например о том, как вы отображаете пользователя в ваших контроллерах asp.net mvc.

В этом связанном вопросе я задал вопрос о доступе к сущностям непосредственно из контроллера. Позвольте мне процитировать один из ответов там:

"Добро пожаловать в BigPizza, специализированный магазин пиццы, могу я принять ваш заказ?" «Ну, я бы хотел пиццу с оливками, но сверху томатный соус и сыр внизу, запекать в духовке 90 минут, пока он не станет черным и твердым, как плоская гранитная скала». «Хорошо, сэр, обычная пицца - наша профессия, мы сделаем это».

Касса идет на кухню. «У прилавка есть псих, он хочет выпить пиццу с… это гранитная скала с… подождите… сначала нам нужно имя», - говорит он повару.

«Нет!» - кричит повар, «не снова! Вы знаете, мы уже пробовали это». Он берет пачку бумаги на 400 страниц: «у нас есть гранитная порода с 2005 года, но ... там не было оливок, а вместо этого паприка ... или вот помидор высшего сорта ... но клиент хотел этого запекать только полминуты. "Может быть, мы должны назвать это TopTomatoGraniteRockSpecial?" «Но это не принимает во внимание сыр на дне ...» Кассир: «Это то, что должен выразить Special». «Но то, что камень« Пицца »сформирован как пирамида, тоже будет особенным», - отвечает повар. «Хм… это сложно…», говорит отчаянный кассир.

«МОЯ ПИЦЦА УЖЕ В ПЕЧЬ?», Вдруг она кричит через кухонную дверь. «Давайте прекратим эту дискуссию, просто скажите мне, как приготовить эту пиццу, у нас не будет такой пиццы во второй раз», - решает повар. «Хорошо, это пицца с оливками, но сверху томатный соус и сыр внизу, запекайте в духовке в течение 90 минут, пока он не станет черным и твердым, как плоская гранитная скала».

(Прочитайте остальную часть ответа, это действительно приятно, IMO).

Наивно игнорировать тот факт, что есть база данных - есть база данных, и как бы вы ни старались абстрагироваться, она никуда не денется. Ваша заявка будет знать об источнике данных. Вы не сможете «горячей замены». ORM полезны, но они дают утечку из-за сложности решаемой ими проблемы и из-за множества причин производительности (например, как Select n + 1).

Бенджамин Грюнбаум
источник
Спасибо за ваш очень тщательный ответ @ Бенджамин. Сначала я начал создавать прототип, используя решение Бобсона, описанное выше (даже до того, как он опубликовал свой ответ), но вы затронули некоторые важные моменты. Чтобы ответить на ваши вопросы: - API-вызов не очень дорогой (бесплатный и быстрый). - Некоторые части данных будут меняться довольно регулярно (некоторые даже каждые пару часов). - Скорость очень важна, но аудитория приложения такова, что облегчение быстрой загрузки не является абсолютным требованием.
Stevehayter
@stevehayter В этом случае я бы, скорее всего, выполнял вызовы API со стороны клиента. Это дешевле и быстрее, и это дает вам более точный контроль.
Бенджамин Грюнбаум
1
Основываясь на этих ответах, я меньше склоняюсь к хранению локальной копии данных. Я на самом деле склоняюсь к тому, чтобы выставлять API отдельно и обрабатывать дополнительные данные таким образом. Я думаю, что это может быть хорошим компромиссом между простотой решения @ Bobson, но также добавляет некоторую степень разделения, которое мне немного более удобно. Я посмотрю на эту стратегию в своем прототипе и доложу о своих результатах (и награждаю щедростью!).
Stevehayter
@ BenjaminGruenbaum - я не уверен, что следую вашему аргументу. Как мое предложение информирует хранилище о презентации? Конечно, он знает, что к полю, поддерживаемому API, был получен доступ, но это не имеет никакого отношения к тому, что представление делает с этой информацией.
Бобсон
1
Я решил переместить все на клиентскую сторону - но как метод расширения в EFUser (который существует на уровне представления, в отдельной сборке). Метод просто возвращает данные из API и устанавливает одиночный код, чтобы он не попадал повторно. Время жизни объектов настолько мало, что у меня нет проблем с использованием здесь синглтона. Таким образом, существует некоторая степень разделения, но я все же получаю удобство работы с сущностью EFUser. Спасибо всем респондентам за помощь. Определенно интересная дискуссия :).
Stevehayter
2

При правильном разделении интересов ничто выше уровня Entity Framework / API не должно даже понимать, откуда поступают данные. Если вызов API не является дорогим (с точки зрения времени или обработки), доступ к данным, которые его используют, должен быть таким же прозрачным, как и доступ к данным из базы данных.

Таким образом, лучший способ реализовать это - добавить дополнительные свойства к EFUserобъекту, которые по мере необходимости загружают данные API. Что-то вроде этого:

partial class EFUser
{
    private APIUser _apiUser;
    private APIUser ApiUser
    {
       get { 
          if (_apiUser == null) _apiUser = API.LoadUser(this.ExternalID);
          return _apiUser;
       }
    }
    public string CompanyName { get { return ApiUser.UserCompanyName; } }
    public string JobTitle{ get { return ApiUser.UserJobTitle; } }
}

Внешне при первом CompanyNameили JobTitleиспользовании будет выполнен единственный вызов API (и, следовательно, небольшая задержка), но все последующие вызовы, пока объект не будет уничтожен, будут такими же быстрыми и простыми, как и доступ к базе данных.

Bobson
источник
Спасибо @Bobson ... это был фактически начальный маршрут, по которому я начал спускаться (с некоторыми добавленными методами расширения, чтобы массово загружать детали для списков пользователей - например, отображая название компании для списка пользователей). Похоже, что до сих пор она хорошо соответствовала моим потребностям - но ниже Бенджамин поднимает некоторые важные моменты, поэтому я собираюсь продолжить оценку на этой неделе.
Stevehayter
0

Одна идея состоит в том, чтобы модифицировать ApiUser, чтобы не всегда иметь дополнительную информацию. Вместо этого вы помещаете метод в ApiUser, чтобы получить его:

ApiUser apiUser = backend.load($id);
//Locally available data directly on the ApiUser like so:
String name = apiUser.getName();
//Expensive extra data available after extra call:
UserExtraData extraData = apiUser.fetchExtraData();
String managerName = extraData.getManagerName();

Вы также можете немного изменить это, чтобы использовать отложенную загрузку дополнительных данных, чтобы вам не нужно было извлекать UserExtraData из объекта ApiUser:

//Extra data fetched on first get:
String managerName = apiUser.lazyGetExtraData().getManagerName();

Таким образом, когда у вас есть список, дополнительные данные не будут выбраны по умолчанию. Но вы все равно можете получить к нему доступ, просматривая список!

Александр Торстлинг
источник
Не совсем уверен, что вы здесь имеете в виду - в backend.load () мы уже выполняем загрузку - настолько уверенно, что это загрузит данные API?
Stevehayter
Я имею в виду, что вам следует подождать выполнения дополнительной загрузки, пока явно не будет запрошено - ленивая загрузка данных API.
Александр Торстлинг