Уважаемые Хабровчане, решил поделиться одним не очевидным моментом использования LinqToSql, с которым столкнулся некоторое время назад. А именно, про особенности использования каскадных Linq запросов:
Работая над очередным ASP.NET MVC проектом, и проектируя уровень доступа к БД мне потребовалось проверить качество скриптов, генерируемых Framework-ом L2C.
class User {
public long Id { get; set; }
public string Name { get; set; }
public IEnumerable<Parameter> Parameters { get; set; }
}
class Parameter {
public long UserId { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
Для получения данных написан класс UserRepository:
class UserRepository: MyProjectDataContext {
public IEnumerable GetUsers() {
return this.Users.ToModelUsers();
}
}
static class RepositoryHelper {
public static IEnumerable<Model.User> ToModelUsers(this IEnumerable<DataAccess.User> users) {
return users.Select(u => new Model.User { Id = u.Id, Name = u.Name, Parameters = u.Parameters.ToModelParameters() });
}
public static IEnumerable<Model.Parameter> ToModelParameters(this IEnumerable<DataAccess.Parameter> parameters) {
return parameters.Select(u => new Model.Parameter {… });
}
}
пишем
var users = userRepository.GetUsers().ToList();
смотрим профайлер, и с удивлением обнаруживаем что для загрузки 10 пользователей и их параметров было выполнено целых 11 запросов.
Пытаюсь излечить данную проблему, меняю код репозитория у на следующий:
public IEnumerable<User> GetUsers() {
return this.Users.ToModelUsers().Select(u => new Model.User {
Id = u.Id, Name = u.Name, Parameters = u.Parameters.Select(p => new Model.Parameter {… })
});
}
Картина меняется кардинально. Всего 1 запрос, в котором грузятся как user-ы так и Parameter-ы
Все очень просто, все дело в том в первоначальном варианте после вызова метода GetUsers мы получали запрос содержащий 2 разных выражения: Linq2Sql и Linq2Object, а во втором только Linq2Sql.
Как оказалось, Linq Extension-ы ведут себя совершенно по разному в случае использования переменной типа IEnumerable и IQueryable, и это не случайно. Дело в том, что метод user.Select(...) есть у обоих классов System.Linq.Enumerable и System.Linq.Queryable соответственно, но реализация конечно же отличается (в чем легко убедиться с помощью Reflector-а).
Есть 2 пути, либо указывать тип IQueryable<T> для возвращаемого значения
static class RepositoryHelper {
public static IQueryable<Model.User> ToModelUsers(this IQueryable<DataAccess.User> users) {
return users.Select(u => new Model.User { Id = u.Id, Name = u.Name, Parameters = u.Parameters.ToModelParameters() });
}
}
либо явно приводить IEnumerable<T> к IQueryable<T> вызовом метода .AsQueryable<T>()
А самое важное — оставаться внимательным и не забывать, что скрывается за ситаксическим сахаром фич .Net 3.5 / 4.0
Работая над очередным ASP.NET MVC проектом, и проектируя уровень доступа к БД мне потребовалось проверить качество скриптов, генерируемых Framework-ом L2C.
Что имеем (упрощенный вариант модели):
class User {
public long Id { get; set; }
public string Name { get; set; }
public IEnumerable<Parameter> Parameters { get; set; }
}
class Parameter {
public long UserId { get; set; }
public string Name { get; set; }
public string Value { get; set; }
}
Для получения данных написан класс UserRepository:
class UserRepository: MyProjectDataContext {
public IEnumerable GetUsers() {
return this.Users.ToModelUsers();
}
}
static class RepositoryHelper {
public static IEnumerable<Model.User> ToModelUsers(this IEnumerable<DataAccess.User> users) {
return users.Select(u => new Model.User { Id = u.Id, Name = u.Name, Parameters = u.Parameters.ToModelParameters() });
}
public static IEnumerable<Model.Parameter> ToModelParameters(this IEnumerable<DataAccess.Parameter> parameters) {
return parameters.Select(u => new Model.Parameter {… });
}
}
пишем
var users = userRepository.GetUsers().ToList();
смотрим профайлер, и с удивлением обнаруживаем что для загрузки 10 пользователей и их параметров было выполнено целых 11 запросов.
Пытаюсь излечить данную проблему, меняю код репозитория у на следующий:
public IEnumerable<User> GetUsers() {
return this.Users.ToModelUsers().Select(u => new Model.User {
Id = u.Id, Name = u.Name, Parameters = u.Parameters.Select(p => new Model.Parameter {… })
});
}
Картина меняется кардинально. Всего 1 запрос, в котором грузятся как user-ы так и Parameter-ы
Где собака зарыта?
Все очень просто, все дело в том в первоначальном варианте после вызова метода GetUsers мы получали запрос содержащий 2 разных выражения: Linq2Sql и Linq2Object, а во втором только Linq2Sql.
А теперь выводы
Как оказалось, Linq Extension-ы ведут себя совершенно по разному в случае использования переменной типа IEnumerable и IQueryable, и это не случайно. Дело в том, что метод user.Select(...) есть у обоих классов System.Linq.Enumerable и System.Linq.Queryable соответственно, но реализация конечно же отличается (в чем легко убедиться с помощью Reflector-а).
Что делать?
Есть 2 пути, либо указывать тип IQueryable<T> для возвращаемого значения
static class RepositoryHelper {
public static IQueryable<Model.User> ToModelUsers(this IQueryable<DataAccess.User> users) {
return users.Select(u => new Model.User { Id = u.Id, Name = u.Name, Parameters = u.Parameters.ToModelParameters() });
}
}
либо явно приводить IEnumerable<T> к IQueryable<T> вызовом метода .AsQueryable<T>()
А самое важное — оставаться внимательным и не забывать, что скрывается за ситаксическим сахаром фич .Net 3.5 / 4.0