Каков наилучший способ инкапсуляции доступа к данным Linq to SQL? - PullRequest
5 голосов
/ 28 февраля 2009

Я пытался инкапсулировать сопоставление объектов в хранилище данных проектов. Возможно, EF обеспечит необходимый уровень абстракции, но по ряду причин я сейчас использую Linq to SQL. Следующий код предназначен для возврата пользователей в базе данных в виде списка объектов ModUser, где ModUser - это POCO, предоставляемый хранилищем:

public List<ModUser> GetUsers() {
    Users.Select(MapUser).ToList();
}

public Expression<Func<User, ModUser>> MapUser {
    get {
        return u => new ModUser() {
            UserId = u.User_Id,
            UserResources = u.Resources(MapResource)
        }
    }
}

public Expression<Func<Resource, ModResource>> MapResource { ...

Код не будет выполнен, поскольку я не могу вызвать выражение MapResource, так как я пытаюсь вызвать его из другого выражения. Мне удалось обойти это, заменив 'MapResource' на u => new ModResource (), затем используя ExpressionVisitor, чтобы найти этот заполнитель и заменить его выражением MapResource.

У меня также есть похожие проблемы, когда я пытаюсь назначить свойство ModUser с выражением, включающим одно свойство, то есть UserResource = MapResource. Мне удалось обойти эту вторую проблему, вручную объединив выражения, необходимые с помощью методов класса Expression.

Я понимаю, что могу изменить код выше на

UserResources = u.Resources(r => MapResource.Compile().Invoke(r));

Но тогда в конечном произведенном SQL-запросе нужно будет получить все атрибуты r, а не только те, которые необходимы MapResouce, поскольку мы сейчас имеем дело с функцией. Кроме того, если MapResouce потребуется доступ к другим таблицам на r, это будет невозможно, поскольку она используется как функция, а не как выражение. Я мог бы установить для DeferredLoadingEnabled значение true, но это породило бы множество отдельных запросов вместо того, чтобы изменять основной запрос для объединения с любыми необходимыми таблицами.

Кто-нибудь знает, будут ли эти операции упрощаться в будущих версиях .NET, или я поступаю неправильно? Мне действительно нравятся функции Linq и Expression, я просто хотел бы использовать их, используя более читаемый код.

Обновлено

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

public Expression<Func<User, ModUser>> MapUser {
    get {
        Expression<Func<User, ModUser>> mapUser = u => new ModUser() {
            UserId = u.User_Id,
            UserResources = u.Resources(r => new ModResource())
        };
        return mapUser.MapResources(this);
    }
}

public Expression<Func<Resource, ModResource>> MapResource { ... }


public static Expression<Func<T0, T1>> MapResources<T0, T1>(this Expression<Func<T0, T1>> exp, DataContext dc) {
    return exp.Visit<MethodCallExpression, Expression<Func<T0, T1>>>(m => {
        if(m.Arguments.Count > 1 && m.Arguments[1].Type == typeof(Func<DataContext.Resource, ModResource>)) { //Find a select statement that has the sub expression as an argument
            //The resource mapping expression will require the Resource object, which is obtained here
            ParameterExpression resourceParam =  ((LambdaExpression)m.Arguments[1]).Parameters[0];
            return Expression.Call(m.Method, m.Arguments[0], //The first argument is the record selection for the 'select' method
                Expression.Lambda<Func<DataContext.Resource, ModResource>>(//Provide the proper mapping expression as the projection for the 'select' method
                     Expression.Invoke(dc.MapResource, resourceParam),
                     resourceParam)
                );
        }
        return m;
    });
}

Так что я здесь делаю? Обратите внимание, что в этой версии MapUser я не создаю объект ModResource правильно, я просто создаю фиктивную версию. Затем я вызываю метод посетителя выражения, который ищет фиктивный вызов и заменяет его тем, который я изначально хотел там. Мне кажется, что синтаксис выражений отсутствует, так как я могу по существу построить дерево выражений, которое я изначально хотел, но для этого мне нужно просто увидеть дерево. Ниже приведен еще один обходной путь, который я нашел в случае единственного случая:

public Expression<Func<User, ModUser>> MapUser {
    get {
        Expression<Func<User, ModResource, ModUser>> mapUser = (u, resource) => new ModUser() {
            UserId = u.User_Id,
            UserResource = resource;
        }

        return mapUser.CollapseArgument(MapResource, user => user.MainResource);
    }
}

public Expression<Func<Resource, ModResource>> MapResource { ... }

public static Expression<Func<T0, T3>> CollapseArgument<T0, T1, T2, T3>(this Expression<Func<T0, T1, T3>> exp, Expression<Func<T2, T1>> exp0, Expression<Func<T0, T2>> exp1) {
    var param0 = Expression.Parameter(typeof(T0), "p0");
    var argExp = Expression.Invoke(exp0, Expression.Invoke(exp1, param0));
    return Expression.Lambda<Func<T0, T3>>(
         Expression.Invoke(exp, param0, argExp),
         param0);
}

Во втором примере я знаю, что могу получить данные ресурса из данных пользователя, но не могу «встроить» выражение, чтобы показать, как это сделать, и сопоставить данные ресурса с POCO ресурса. Но я могу вручную создать дерево выражений, которому присвоен уже сопоставленный ресурс POCO, и использовать его. Затем я могу создать другое выражение, показывающее, как получить необработанные данные ресурса от пользователя, и окончательное выражение, показывающее, как сопоставить необработанные данные ресурса в POCO ресурса. Теперь стало возможным, что я могу объединить всю эту информацию в одно дерево выражений таким образом, чтобы «свернуть» параметр, специфичный для ресурса, поскольку я могу получить его из основного пользовательского параметра. Это то, что делает код выше.

Итак, я нашел способы сделать выражения легко компонуемыми ... Это просто не кажется чистым.

Ответы [ 3 ]

1 голос
/ 16 марта 2009

Способ, которым Linq To SQL поддерживает POCO, немного отличается.

Для достижения постоянного невежества вы должны использовать файл сопоставления, который описывает, как сопоставляется modUser (столбцы, ассоциации и т. Д.), А не конструктор LTS. Когда вы создаете новый контекст, вы передаете ему файл сопоставления XML как XMLMappingSource.

Таким образом, LTS вернет ваши объекты из базы данных.

Я тут и там читал, что определения свойств ассоциации вашей коллекции как свойств чтения / записи типа IList (of T) достаточно для LinqToSQL, чтобы обеспечить отложенную загрузку этих коллекций, но я не пробовал, поэтому не могу ручаться за это.

Entity Framework будет еще хуже для поддержки POCO в его текущей версии (в основном ни один, поскольку большинство людей понимают термин POCO).

Все обычные ограничения LTS применимы к этому, поэтому нет отображения «объекта значения». Если вы хотите, чтобы что-то еще было удалено из базы данных и поддержки POCO, вам нужно взглянуть на NHibernate.

1 голос
/ 20 марта 2009

Хорошо, я должен признать, что я на самом деле не закончил читать вопрос ОП (смущенная улыбка), но знаете ли вы, что вы можете использовать атрибуты Linq-to-SQL для украшения любого объекта POCO? Вам не нужно использовать дизайнер.

Вот случайный пример из кода, который сейчас открыт передо мной. Это POCO с именем «Product», к которому применены некоторые атрибуты, которые позволяют ему взаимодействовать с Linq-to-SQL DataContext.

НТН

using System;
using System.Collections.Generic;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;
using System.Web;

namespace Redacted.Site.Models.Store
{
    /// <summary>
    /// A "Product" is a good for purchase at the store.
    /// </summary>
    [Table(Name = "s.products")]
    public partial class Product
    {
        /// <summary>Gets or sets the PK of the object/row.</summary>
        [Column(Name = "id", IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL")]
        public Int32 ID { get; set; }

        /// <summary>Gets or sets the Title.</summary>
        [Column(Name = "title", DbType = "NVARCHAR(500) NOT NULL")]
        public String Title { get; set; }

        /// <summary>Gets or sets the Lede.</summary>
        [Column(Name = "lede", DbType = "NVARCHAR(MAX) NOT NULL")]
        public String Lede { get; set; }

        /// <summary>Gets or sets the Description.</summary>
        [Column(Name = "description", DbType = "NTEXT NOT NULL")]
        public String Description { get; set; }

        /// <summary>Gets or sets the Price.</summary>
        [Column(Name = "price", DbType = "FLOAT NOT NULL")]
        public Double Price { get; set; }

        /// <summary>Gets or sets the FK to the <see cref="Department"/>.</summary>
        [Column(Name = "department_id", DbType = "TINYINT NOT NULL")]
        public Byte DepartmentID { get; set; }

        /// <summary>Gets or sets the date/time the product was released to the store.</summary>
        [Column(Name = "released_on_utc", DbType = "DATETIME NOT NULL")]
        public Int32 ReleasedOnUtc { get; set; }

    }
}
0 голосов
/ 14 марта 2009

Я думаю, если вы хотите использовать POCO, Linq to SQL - не лучший выбор. Я думаю, вам, наверное, будет намного лучше, если вы используете что-то вроде NHibernate. Использование Linq to SQL с POCO означает, что вы строите слой поверх слоя данных (Linq to SQL) поверх базы данных. С NHibernate вы будете собирать весь свой код и отображать его прямо в базу данных. Меньше слоев == меньше кода == меньше работы.

...