Знаю, что подобных вопросов было много, но ответа в них для себя, к сожалению, найти не смог. Вероятно буду писать глупости, не судите строго, я еще учусь)
Если использовать GenericRepository с Entity Framework в связке с UnitOfWork, то для того, что бы делать сложные запросы с несколькими join нам придется делать тип возвращаемого значения IQuerable а не IEnumerable, что в свою очередь подходит только для тех ORM, которые поддерживают Query Builder.
А Если например делать реализацию для Dapper, то я вообще не понимаю, как можно делать сложные запросы, если считать что Repository это in-memory коллекция доменных объектов. Т.е. я имею ввиду, что либо придется сначала загружать всю коллекцию объектов из базы в память, либо изобретать свою версию Query Builder для ОРМ, с помощью каких нибудь спецификаций. Вообще реально ли использовать Repository с Dapper или чистым SQL, учитывая, что существует постоянная необходимость джойнить несколько таблиц?
Хочу уточнить, что задаю этот вопрос именно в рамках паттерна Repository а не DAO.
Первое и главное, что нужно понять про Репозиторий — он возвращает сущности предметной области, даже более того — не просто сущности, а агрегаты.
Для примера рассмотрим сайт с зарегистрированными пользователями. У пользователей есть идентификатор, логин и пароль.
class User
{
public int Id { get; set; }
public string Login { get; set; }
public string Password { get; set; }
}
Замечательный класс сущности, который легко отобразить на таблицу в БД. К сожалению, такая лобовая реализация нарушает все принципы ООП. В частности, пароль пользователя не может быть изменён, если пользователь не знает старый пароль. Точно также идентификатор пользователя не должен меняться вообще, а смена логина может потребовать серьёзного сценария с поиском дубликата в БД.
Класс-сущность должен выглядеть так:
class User
{
public int Id { get; }
public string Login { get; }
private string _password;
public void ChangePassword(string oldPassword, string newPassword)
{
if (_password != oldPassword)
throw new Exception();
_password = newPassword;
}
internal User(int id, string login, string password)
{
Id = id;
Login = login;
_password = password;
}
}
Такой класс реализует бизнес-логику. Это сущность предметной области. Его можно было бы отобразить на таблицу в БД «как есть», если не думать о безопасности. Рекомендации OWASP предлагают в частности, никогда не хранить пароли в открытом виде, а хранить их хеши, более того, для вычисления хешей использовать случайную затравку.
Для хранения в базе нам потребуется другой класс с отличной структурой. Поскольку он используется для связи объекта в БД, мы будем считать его объектом переноса данных (data transfer object — DTO).
class UserDto
{
public int Id { get; set; }
public string Login { get; set; }
public byte[] PasswordHash { get; set; }
public byte[] Salt { get; set; }
}
Теперь у нас есть класс предметной области User
и объект переноса данных UserDto
, с помощью которого мы можем складывать пользователей в базу. Обратите внимания, мы не накладываем сейчас никаких особых ограничений на то, где именно будут храниться пользователи. UserDto
определяет только, какие поля должны в конце концов сохраняться.
Класс User
можно переписать с учётом существования UserDto
:
class User
{
private UserDto _dto;
private User(UserDto dto)
{
_dto = dto;
}
public int Id => _dto.Id;
public string Login => _dto.Login;
public void ChangePassword(string oldPassword, string newPassword)
{
var hash = CalculateHash(oldPassword, _dto.Salt);
if (!hash.SequenseEqual(_dto.PasswordHash))
throw new Exception();
_dto.PasswordHash = CalculateHash(newPassword, _dto.Salt);
}
}
Теперь посмотрим, где в этой схеме появляется репозиторий. Репозиторий позволяет полностью скрывать детали хранения. Единственное, что мы должны знать о хранилище — оно большое, поэтому мы не можем загрузить все объекты в память.
На уровне предметной области — то есть физически в том же проекте, где объявлены User
и UserDto
мы описываем только интерфейс репозитория.
interface IUserRepository
{
User Create(string login, string password);
User GetById(int id);
User GetByLogin(string login);
User UpdateLogin(int id, string login);
}
Этот интерфейс может быть реализован с помощью ADO, Dapper, EF или любым другим способом. Вы можете хранить пользователей в SQL Server, Postres, MongoDB и даже в XML-файле, если их не очень много.
Создаём новый проект, скажем, для репозиториев ADO.
class AdoUserRepository : IUserRepository
{
private Func<SqlConnection> _connectionFactory;
public AdoUserRepository(Func<SqlConnection> connectionFactory)
{
_connectionFactory = connectionFactory;
}
User GetById(int id)
{
using (var connection = _connectionFactory())
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM [Users] WHERE Id = @Id";
command.Parameters.Add("@Id", id);
. . .
}
}
}
Построение запросов и отображение данных из запросов в UserDto
решаются на уровне конкретных репозиториев. Если у вас EF, то репозитории получаются простые.
У нас остался важный практический вопрос: если мы заполнили UserDto
, кто превратит этот объект в доменную сущность User
? В книге про паттерны GoF описан паттерн Memento
, который как раз занимается сохранением и восстановлением объектов.
Логика Memento
отличается от того, что нужно нам, но не кардинально, поэтому свою реализацию класса для сохранения и восстановления мы можем называть Mememto
. Это будет отдельный класс, чтобы не нагружать User
второй ответственностью. И, поскольку он должен иметь доступ к приватным членам класса User
, он будет вложенным.
public class User
{
private UserDto _dto;
public static class Memento
{
public static UserDto Serialize(User domain) => domain._dto;
public static User Deserialize(UserDto dto) => new User(dto);
}
}
Теперь реализации репозиториев, например AdoUserRepository
может вызывать User.Memento.Deserialize
, чтобы создать User
из UserDto
.
User GetById(int id)
{
using (var connection = _connectionFactory())
using (var command = connection.CreateCommand())
{
command.CommandText = "SELECT * FROM [Users] WHERE Id = @Id";
command.Parameters.Add("@Id", id);
var reader = command.ExecuteReader();
if (!reader.Read())
throw new Exception();
return User.Memento.Deserialize(new UserDto
{
Id = reader.GetInt32(0),
Login = reader.GetString(1),
PasswordHash = reader.GetBytes(),
Salt = reader.GetBytes(),
});
}
}
Последний вопрос о том, почему я подчеркнул разницу между сущностями и агрегатами. Агрегат это составная сущность, элементы которой не сильно нужны друг без друга. В базе такая сущность хранится в нескольких таблицах, но в предметной области это один большой объект, содержащий другие сущности или их коллекции.
Представим, что у наших пользователей есть профиль. Это набор параметров ключ-значение, которых не очень много. Мы могли бы всегда подгружать эти параметры, при загрузке пользователя. Если мы делаем так, мы прячем от бизнес-логики такие детали, как таблицы, связи, внешние ключи. Конечно, все они остаются на уровне реализации, но наверх не проникают.
Вот так можно загрузить агрегат вместе с дочерними параметрами с помощью энергичной загрузки EF (метод Include
):
User GetById(int id)
{
using (var context = _contextFactory())
{
var dto = context.Users
.Include(x => x.Parameters)
.Single(x => x.Id == id);
return User.Memento.Deserialize(dto);
}
}
Как видим, реализация может быть посложнее и попроще. EF пошлёт в базу один запрос и сумеет корректно отобразить значения полей на свойства UserDto
. Мы сами могли бы послать два запроса в базу и реализовать простой мапинг, если бы использовали ADO.
В любом случае, это деталь реализации, про которую мы можем не думать на уровне предметной области.
Вообще реально ли использовать Repository с Dapper или чистым SQL, учитывая, что существует постоянная необходимость джойнить несколько таблиц?
Паттерн репозиторий можно использовать с любым источником данных. Даже если это сетевой ресурс или файл.
Более того, репозиторий не должен знать ничего о физической структуре данных и способе их хранения. Для этого есть источник данных и их объектная модель. Задача репозитория предоставить фасадный интерфейс к данным.
Айфон мало держит заряд, разбираемся с проблемой вместе с AppLab
Не страшно же, если во время работы цикла я изменю его условие?
Я добавляю object[] в List objectКак я могу удалить этот object[]?
Целиком задача заключается в загрузке трех картинок для трех PictureBoxПо условию задачи пользователь может загружать их в любой момент, поэтому...
Хотите улучшить этот вопрос? Добавьте больше подробностей и уточните проблему, отредактировав это сообщение