Hello World
Spiga

在LINQ to SQL中使用Translate方法以及修改查询用SQL

2008-02-19 03:02 by 老赵, 25467 visits

目前LINQ to SQL的资料不多——老赵的意思是,目前能找到的资料都难以摆脱“官方用法”的“阴影”。LINQ to SQL最权威的资料自然是MSDN,但是MSDN中的文档说明和实例总是显得“大开大阖”,依旧有清晰的“官方”烙印——这简直是一定的。不过从按照过往的经验,在某些时候如果不按照微软划定的道道来走,可能就会发现别样的风景。老赵在最近的项目中使用了LINQ to SQL作为数据层的基础,在LINQ to SQL开发方面积累了一定经验,也总结出了一些官方文档上并未提及的有用做法,特此和大家分享。

言归正传,我们先看一个简单的例子。

1.jpg

Item实体对应Item表,每个Item拥有一些评论,也就是ItemComment。Item实体中有一个Comments属性,是ItemComment实体的集合。这个例子将会使用这个再简单不过的模型。

为用户显示他的Item列表是非常常见的需求,如果使用LINQ to SQL来获取Item的话,我们可能会这么做:

public List<Item> GetItemsForListing(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
    var query = from item in dataContext.Items
                where item.UserID == ownerId
                orderby item.CreateTime descending
                select item;
 
    return query.ToList();
}

这么做自然可以实现我们想要的功能,这的确没错。但是这种做法有个很常见的问题,那就是可能会获得太多不需要的数据。一个Item数据量最大的是Introduction字段,而显示列表的时候我们是不需要显示它的。如果我们在获取Item列表时把Introduction一起获得的话,那么应用服务器和数据库服务器之间的数据通信量将会成百甚至上千地增长了。因此我们在面向此类需求的话,都会忽略每个Item对象的Introduction字段。那么我们该怎么做呢?对LINQ有简单了解的朋友们可能会想到这么做:

public List<Item> GetItemsForListing(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
    var query = from item in dataContext.Items
                where item.UserID == ownerId
                orderby item.CreateTime descending
                select new Item
                {
                    ItemID = item.ItemID,
                    Title = item.Title,
                    UserID = item.UserID,
                    CreateTime = item.CreateTime
                };

 
    return query.ToList();
}

这个做法很直观,利用了C# 3.0中的Object Initializer特性。编译通过了,理应没有错,可是在运行时却抛出了NotSupportedException:“Explicit construction of entity type 'Demo.Item' in query is not allowed.”,意思就是不能在LINQ to SQL中显式构造Demo.Item对象。

事实上在RTM之前的版本中,以上的语句是能运行通过的——我是指通过,不是正确。LINQ to SQL在RTM之前的版本有个Bug,如果在查询中显式构造一个实体的话,在某些情况下会得到一系列完全相同的对象。很可惜这个Bug我只在资料中看到过,而在RTM版本的LINQ to SQL中这个Bug已经被修补了,确切地说是绕过了。直接抛出异常不失为一种“解决问题”的办法,虽然这实际上是去除了一个功能——没有功能自然不会有Bug,就像没有头就不会头痛了一个道理。

但是我们还得做,难道我们只能自己SQL语句了吗?

使用Translate方法

幸亏DataContext提供了Translate方法,Translate方法的作用就是从一个DbDataReader对象中生成一系列的实例。其中最重要的就是一个带范型的重载:

public static List<Item> GetItemsForListing(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
    dataContext.Connection.Open();
 
    SqlCommand command = new SqlCommand(
        "SELECT [ItemID], [Title], [UserID], [CreateTime]" +
        " FROM [Item] WHERE [UserID] = " + ownerId +
        " ORDER BY [CreateTime]",
        (SqlConnection)dataContext.Connection);
 
    using (DbDataReader reader = command.ExecuteReader(CommandBehavior.CloseConnection))
    {
        return dataContext.Translate<Item>(reader).ToList();
    }
}

在这段代码里,我们拼接出了一段SQL语句,实现了我们需要的逻辑。在ExecuteReader之后即使用dataContext.Translate方法将DbDataReader里的数据转换成Item对象。使用Translate方法除了方便之外,生成的对象也会自动Attach到DataContext中,也就是说,我们可以继续对获得的对象进行操作,例如访问Item对象的Comments属性时会自动去数据库获取数据,改变对象属性之后调用SubmitChange也能将修改提交至数据库。Translate方法从DbDataReader中生成对象的规则和内置的DataContext.ExecuteQuery方法一样,大家可以查看MSDN中的说明(中文英文)。

此外,这里有两个细节值得一提:

  • 为什么调用ExecuteReader方法时要传入CommandBehavior.CloseConnection:LINQ to SQL中的DataContext对象有个特点,如果在使用时它的Connection对象被“显式”地打开了,即使调用了DataContext对象的Dispose方法也不会自动关闭。因此我们在开发程序的时候一定要注意这一点。例如,在调用ExecuteReader是传入CommandBehavior.CloseConnection,这样就保证了在关闭DbDataReader时同时关闭Connection——当然,我们也可以不这么做。
  • 在调用Translate方法后为什么要直接调用ToList方法:因为GetItemsForListing方法的返回值是List<Item>,这是原因之一。另一个原因是Translate方法并不会直接生成所有的对象,而是在外部代码访问Translate方法返回的IEnmuerable<T>时才会生成其中每个对象。这也是一种Lasy Load,但是也导致了所有的对象必须在Reader对象关闭之前生成,所以我一般都会在Translate方法后直接调用ToList方法,保证所有的对象已经生成了。虽然事实上我们也可以不使用using关键字而直接返回Translate方法生成的IEnumerable<Item>,不过这么做的话当前链接就得不到释放(释放,而不是关闭),也就是把处理数据连接的问题交给了方法的使用者——很可能就是业务逻辑层。为了确保分层结构的职责分明,我一般倾向于在这里确保所有对象的已经生成了。

上面的例子使用拼接SQL字符串的方式来访问数据库,那我们又该如何使用LINQ to SQL呢?幸亏LINQ to SQL中的DataContext提供了GetCommand方法。我们直接来看一个完整的扩展:

public static class DataContextExtensions
{
    public static List<T> ExecuteQuery<T>(this DataContext dataContext, IQueryable query)
    {
        DbCommand command = dataContext.GetCommand(query);
        dataContext.OpenConnection();
 
        using (DbDataReader reader = command.ExecuteReader())
        {
            return dataContext.Translate<T>(reader).ToList();
        }
    }
 
    private static void OpenConnection(this DataContext dataContext)
    {
        if (dataContext.Connection.State == ConnectionState.Closed)
        {
            dataContext.Connection.Open();
        }
    }
}

自从有了C# 3.0中的Extension Method,很多扩展都会显得非常优雅,我非常喜欢这个特性。DataContextExtensions是我对于LINQ to SQL中DataContext对象的扩展,如果以后有新的扩展也会写在这个类中。OpenConnection方法用于打开DataContext中的数据连接,今后的例子中也会经常看到这个方法。而这次扩展的关键在于新的ExecuteQuery方法,它接受一个IQueryable类型的对象作为参数,返回一个范型的List。方法中会使用DataContext的GetCommand方法来获得一个DbCommand。在我之前的文章,以及MSDN中的示例都只是通过这个DbCommand对象来查看LINQ to SQL所生成的查询语句。也就是说以前我们用它进行Trace和Log,而我们这次将要真正地执行这个DbCommand了。剩下的自不必说,调用ExecuteReader方法获得一个DbDataReader对象,再通过Translate方法生成一个对象列表。

新的ExecuteQuery方法很容易使用:

public static List<Item> GetItemsForListing(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
    var query = from item in dataContext.Items
                where item.UserID == ownerId
                orderby item.CreateTime descending
                select new
                {
                    ItemID = item.ItemID,
                    Title = item.Title,
                    CreateTime = item.CreateTime,
                    UserID = item.UserID
                };
 
    using (dataContext.Connection)
    {
        return dataContext.ExecuteQuery<Item>(query);
    }
}

在通过LINQ to SQL获得一个query之后,我们不再直接获得查询数据了,而是将其交给我们的ExecuteQuery扩展来执行。现在这种做法既保证了使用LINQ to SQL进行查询,又构造出Item对象的部分字段,算是一种较为理想的解决方案。不过使用这个方法来获得仅有部分字段的对象时需要注意一点:在构造匿名对象时使用的属性名,可能和目标实体对象(例如之前的Item)的属性名并非一一对应的关系。

这种情况会在实体对象的属性名与数据表字段名不同的时候发生。在使用LINQ to SQL时默认生成的实体对象,其属性名与数据库的字段名完全对应,这自然是最理想的情况。但是有些时候我们的实体对象属性名和数据库字段名不同,这就需要在ColumnAttribute标记中设置Name参数了(当然,如果使用XmlMappingSource的话也可以设置),如下:

[Table(Name = "dbo.Item")]
public partial class Item : INotifyPropertyChanging, INotifyPropertyChanged
{
    [Column(Storage = "_OwnerID", DbType = "Int NOT NULL", Name = "UserID")]
    public int OwnerID
    {
        get {...}
        set {...}
    }
}

OwnerID属性上标记的ColumnAttribute的Name属性设为UserID,这表示它将与Item表中的UserID字段对应。那么如果我们要在这种情况下改写之前的GetItemsForListing方法,我们该怎么做呢?可能有朋友会很自然的想到:

public static List<Item> GetItemsForListing(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
    var query = from item in dataContext.Items
                where item.OwnerID == ownerId
                orderby item.CreateTime descending
                select new
                {
                    ItemID = item.ItemID,
                    Title = item.Title,
                    CreateTime = item.CreateTime,
                    OwnerID = item.OwnerID
                };
 
    using (dataContext.Connection)
    {
        return dataContext.ExecuteQuery<Item>(query);
    }
}

按照“常理”判断,似乎只要将所有的UserID改为OwnerID即可——其实不然。查看方法返回的结果就能知道,所有对象的OwnerID的值都是默认值“0”,这是怎么回事呢?使用SQL Profiler观察以上代码所执行SQL语句之后我们便可明白一切:

SELECT [t0].[ItemID], [t0].[Title], [t0].[CreateTime], [t0].[UserID] AS [OwnerID]
FROM [dbo].[Item] AS [t0]
WHERE [t0].[UserID] = @p0
ORDER BY [t0].[CreateTime] DESC

由于我们所使用的query实际上是用于生成一系列匿名对象的,而这些匿名对象所包含的是“OwnerID”而不是“UserID”,因此LINQ to SQL实际在生成SQL语句的时候会将UserID字段名转换成OwnerID。由于Item的OwnerID上标记的ColumnAttribute把Name设置成了UserID,所以Translate方法读取DbDataReader对象时事实上会去寻找UserID字段而不是OwnerID字段——这很显然就造成了目前的问题。因此,如果您使用了ColumnAttribute中的Name属性改变了数据库字段名与实体对象属性名的映射关系,那么在创建匿名对象的时候还是要使用数据库的字段名,而不是实体对象名,如下:

public static List<Item> GetItemsForListing(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
    var query = from item in dataContext.Items
                where item.OwnerID == ownerId
                orderby item.CreateTime descending
                select new
                {
                    ItemID = item.ItemID,
                    Title = item.Title,
                    CreateTime = item.CreateTime,
                    UserID = item.OwnerID
                };
 
    using (dataContext.Connection)
    {
        return dataContext.ExecuteQuery<Item>(query);
    }
}

这样就能解决问题了——不过显得不很漂亮,因此在使用LINQ to SQL时,我建议保持实体对象属性名与数据库字段名之间的映射关系。

改变LINQ to SQL所执行的SQL语句

按照一般的做法我们很难改变LINQ to SQL查询所执行的SQL语句,但是既然我们能够将一个query转化为DbCommand对象,我们自然可以在执行之前改变它的CommandText。我这里通过一个比较常用的功能来进行演示。

数据库事务会带来锁,锁会降低数据库并发性,在某些“不巧”的情况下还会造成死锁。对于一些查询语句,我们完全可以显式为SELECT语句添加WITH (NOLOCK)选项来避免发出共享锁。因此我们现在扩展刚才的ExecuteQuery方法,使它接受一个withNoLock参数,表明是否需要为SELECT添加WITH (NOLOCK)选项。请看示例:

public static class DataContextExtensions
{
    public static List<T> ExecuteQuery<T>(
        this DataContext dataContext, IQueryable query, bool withNoLock)
    {
        DbCommand command = dataContext.GetCommand(query, withNoLock);
 
        dataContext.OpenConnection();
 
        using (DbDataReader reader = command.ExecuteReader())
        {
            return dataContext.Translate<T>(reader).ToList();
        }
    }
 
    private static Regex s_withNoLockRegex =
        new Regex(@"(] AS \[t\d+\])", RegexOptions.Compiled);
 
    private static string AddWithNoLock(string cmdText)
    {
        IEnumerable<Match> matches =
            s_withNoLockRegex.Matches(cmdText).Cast<Match>()
            .OrderByDescending(m => m.Index);
        foreach (Match m in matches)
        {
            int splitIndex = m.Index + m.Value.Length;
            cmdText =
                cmdText.Substring(0, splitIndex) + " WITH (NOLOCK)" +
                cmdText.Substring(splitIndex);
        }
 
        return cmdText;
    }
 
    private static SqlCommand GetCommand(
        this DataContext dataContext, IQueryable query, bool withNoLock)
    {
        SqlCommand command = (SqlCommand)dataContext.GetCommand(query);
 
        if (withNoLock)
        {
            command.CommandText = AddWithNoLock(command.CommandText);
        }
 
        return command;
    }
}

上面这段逻辑的关键在于使用正则表达式查找需要添加WITH (NOLOCK)选项的位置。在这里我查找SQL语句中类似“] AS [t0]”的字符串,并且在其之后添加WITH (NOLOCK)选项。其他的代码大家应该完全能够看懂,我在这里就不多作解释了。我们直接来看一下使用示例:

public static List<Item> GetItemsForListingWithNoLock(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
    var query = from item in dataContext.Items
                where item.UserID == ownerId
                orderby item.CreateTime descending
                select new
                {
                    ItemID = item.ItemID,
                    Title = item.Title,
                    CreateTime = item.CreateTime,
                    UserID = item.UserID
                };
 
    using (dataContext.Connection)
    {
        return dataContext.ExecuteQuery<Item>(query, true);
    }
}

使用SQL Profiler查看上述代码所执行的SQL语句,就会发现:

SELECT [t0].[ItemID], [t0].[Title], [t0].[CreateTime], [t0].[UserID]
FROM [dbo].[Item] AS [t0] WITH (NOLOCK)
WHERE [t0].[UserID] = @p0
ORDER BY [t0].[CreateTime] DESC

很漂亮。事实上只要我们需要,就可以在DbCommand对象生成的SQL语句上作任何修改(例如添加事务操作,容错代码等等),只要其执行出来的结果保持不变即可(事实上变又如何,如果您真有自己巧妙设计的话,呵呵)。

以上扩展所受限制

以上的扩展并非无可挑剔。由于Translate方法的特点,此类做法都无法充分发挥LINQ to SQL查询的所有能力——那就是所谓的“LoadWith”能力。

在LINQ to SQL中,默认会使用延迟加载,然后在必要的时候才会再去数据库进行查询。这个做法有时候会降低系统性能,例如:

List<Item> itemList = GetItems(1);
foreach (Item item in itemList)
{
    foreach (ItemComment comment in item.Comments)
    {
        Console.WriteLine(comment.Content);
    }
}

这种做法的性能很低,因为默认情况下每个Item对象的ItemComment集合不会被同时查询出来,而是会等到内层的foreach循环执行时再次查询数据库。为了避免不合适的Lazy Load降低性能,LINQ to SQL提供了DataLoadOptions机制进行控制:

public static List<Item> GetItems(int ownerId)
{
    ItemDataContext dataContext = new ItemDataContext();
 
    DataLoadOptions loadOptions = new DataLoadOptions();
    loadOptions.LoadWith<Item>(item => item.Comments);
    dataContext.LoadOptions = loadOptions;
 
    var query = from item in dataContext.Items
                where item.UserID == ownerId
                orderby item.CreateTime descending
                select item;
 
    return query.ToList();
}

当我们为DataContext对象设置了LoadOptions并且指明了“Load With”关系,LINQ to SQL就会根据要求查询数据库——在上面的例子中,它将生成如下的SQL语句:

SELECT [t0].[ItemID], [t0].[Title], [t0].[Introduction], [t0].[UserID], [t0].[CreateTime], [t1].[ItemCommentID], [t1].[ItemID] AS [ItemID2], [t1].[Content], [t1].[UserID], [t1].[CreateTime] AS [CreateTime2], (
    SELECT COUNT(*)
    FROM [dbo].[ItemComment] AS [t2]
    WHERE [t2].[ItemID] = [t0].[ItemID]
    ) AS [value]
FROM [dbo].[Item] AS [t0]
LEFT OUTER JOIN [dbo].[ItemComment] AS [t1] ON [t1].[ItemID] = [t0].[ItemID]
WHERE [t0].[UserID] = @p0
ORDER BY [t0].[CreateTime] DESC, [t0].[ItemID], [t1].[ItemCommentID]

相信大家已经了解Translate方法为何无法充分发挥LINQ to SQL的能力了。那么我们又该如何解决这个问题呢?如果您希望同时使用本文类似的扩展和Load With能力,可能就需要通过查询两次数据库并加以组合的方式来生成对象了——虽然查询了两次,但总比查询100次的性能要高。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

68 条回复

  1. 坏人
    *.*.*.*
    链接

    坏人 2008-02-19 03:50:00

    很实用的技巧.

  2. carysun
    *.*.*.*
    链接

    carysun 2008-02-19 07:13:00

    我看msdn总觉得他的资料很分散
    觉得该放到一起的东西,他放在不同的类别里

  3. eidolon[未注册用户]
    *.*.*.*
    链接

    eidolon[未注册用户] 2008-02-19 08:21:00

    实际上, 如果业务逻辑层返回IQueryable<T>到前端的话就可以解决很多的问题, 但是这样对客户端的调用要求稍微高一点, 另外就是分层方面不那么优雅.

  4. 驿路梨花
    *.*.*.*
    链接

    驿路梨花 2008-02-19 08:32:00

    老赵都已经在项目中使用VS2008了呀!太快了呀。

  5. 老赵
    admin
    链接

    老赵 2008-02-19 09:23:00

    @eidolon
    如果把IQueryable释放到上层会有什么额外的好处呢?

  6. 老赵
    admin
    链接

    老赵 2008-02-19 09:23:00

    @驿路梨花
    我2005和2008都是beta时就用起的,而且第一时间用于产品开发,呵呵。

  7. eidolon[未注册用户]
    *.*.*.*
    链接

    eidolon[未注册用户] 2008-02-19 09:56:00

    就像你说的, 很多情况下你需要的List<Item>并不需要包含Item的所有字段信息, 为了达到最优化, 你可能只会选择必须的字段返回, 首先当然它有一定的局限性, 客户端不知道Item究竟哪些property被初始化了, 第二个就是如果针对包含不同字段的列表要吗你提供不同的方法, 要吗还是要返回包含覆盖全部字段的列表集合. 而如果返回IQueryable<T>的话, 这个是完全可以复用的, 客户端需要哪些字段就绑定哪些字段, 执行的时候SQL也只会选择这些字段, 不会有多余的字段被选择. 第二个问题就是排序相对方便一些, 而且可以是强类型的排序, 当然这个也可以封装在你的商业方法里面, 但是不可避免的要做一些转换, 要吗动态执行t-sql, 要吗需要动态执行LINQ, 但我觉得这样的代码放在客户端可能更容易理解一些:
    if(条件成立)
    q = q.OrderBy(u => u.UserID);
    else
    q = q.OrderByDescending(u => u.UserID);

    ControlID.DataSource = q;
    ControlID.DataBind();

  8. 老赵
    admin
    链接

    老赵 2008-02-19 10:15:00

    @eidolon
    这个思路似乎不错,让我思考一下。:)
    如果LINQ to SQL的IQueryable真能担负起如此重任应该还不错,但是我觉得如果这样的话,上层也就要知道下层(LINQ to SQL)的一些细节了,比如LoadOptions,否则可能会给系统带来麻烦。

  9. eidolon[未注册用户]
    *.*.*.*
    链接

    eidolon[未注册用户] 2008-02-19 10:37:00

    @Jeffrey Zhao
    的确, IQueryable<T>提供了太大的灵活性以至于可能产生一些问题, 你说的LoadOptions的确是一个很大的问题, 因为中间层无法控制, 如果前端开发人员不了解一点LINQ to SQL的话, 根本是不会去注意性能方面的影响的. 目前实际上我也是在IQueryable<T>和List<T>之间徘徊, 暂时也没有看到一个很好的方案能够完全解决所有的问题.

  10. lovecherry
    *.*.*.*
    链接

    lovecherry 2008-02-19 11:05:00

    我不在逻辑层直接采用自动生成的实体,而是在数据层通过LINQ TO SQL转化为逻辑实体(逻辑实体可能和数据层的实体差别比较大,1是避免传输和从数据库中获取比必要的数据,2是为表现层的显示进行优化)

  11. eidolon[未注册用户]
    *.*.*.*
    链接

    eidolon[未注册用户] 2008-02-19 11:41:00

    @lovecherry
    如果你自定义DTO的话, 对于select之类的操作自然是ok, 如果对于insert, update的话呢? 也是一样初始化DTO, 然后传入中间层之后再转化为LINQ to SQL的实体然后再调用SubmitChanges()吗?

  12. 一瞬间
    *.*.*.*
    链接

    一瞬间 2008-02-19 11:56:00

    老赵,我觉得第一个Query错误的原因可能是因为你没有定义的Item实体类吧,如果要用select Item的话你必须要声明一个public class Item 来和你的实体进行映射吧,假如你和后面一样用select new 的话应该不会有这样的错误吧?
    还有不太清楚你所说的DataLoadOptions机制是什么意思呢 :)

  13. 无常
    *.*.*.*
    链接

    无常 2008-02-19 12:52:00

    博主的“个人信息”和“我的衣橱”几栏是怎么加上去的?
    我怎么找不到

  14. 老赵
    admin
    链接

    老赵 2008-02-19 13:18:00

    @lovecherry
    这么做感觉很古怪,如果要方便的话,我会选择扩展自动生成的实体类,比如添加新的成员封装自动生成的成员——这也是我经常做的。

  15. 老赵
    admin
    链接

    老赵 2008-02-19 13:19:00

    @一瞬间
    Item是LINQ to SQL自动生成的(代码里有阿)。你尝试一下就知道了,是LINQ to SQL的一个“特点”,如果使用了new Item,编译能成功但是运行会失败,估计你没有看清楚文章内容。:)
    至于DataLoadOptions,你可以看一下DataLoadOptions类,以及DataContext的LoadOptions属性。

  16. 老赵
    admin
    链接

    老赵 2008-02-19 13:30:00

    @无常
    自己定制,系统没有提供这个功能。

  17. 一瞬间
    *.*.*.*
    链接

    一瞬间 2008-02-19 14:29:00

    谢谢老赵,select Item笔误吧应该是select new Item
    另外,有个问题,就是Loadwith,为什么后面设置了Loadwith以后就可以直接toList了,这里是怎么回事?

  18. 老赵
    admin
    链接

    老赵 2008-02-19 15:21:00

    @一瞬间
    多谢提醒已经改正。
    LoadWith后直接ToList是什么意思?

  19. 一瞬间
    *.*.*.*
    链接

    一瞬间 2008-02-19 16:49:00

    @ Jeffrey zhao
    这个return query.ToList();和前面的只是更改了LoadOption而已。

  20. 老赵
    admin
    链接

    老赵 2008-02-19 17:25:00

    @一瞬间
    直接ToList就立即执行了,LoadOptions是指定某个对象加载时“同时”加载哪些对象,两者没有联系。

  21. Tristan(Guozhijian)
    *.*.*.*
    链接

    Tristan(Guozhijian) 2008-02-19 19:27:00

    这么做感觉很古怪,如果要方便的话,我会选择扩展自动生成的实体类,比如添加新的成员封装自动生成的成员——这也是我经常做的。
    @Jeffrey Zhao

    我也比较倾向这种做法。

  22. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2008-02-20 22:02:00

    非常非常漂亮!我觉得在RTM之前,那个bug很符合一句话:It's not a bug; It's a feature!

  23. Cat Chen
    *.*.*.*
    链接

    Cat Chen 2008-02-20 22:10:00

    不过,反观Linq to Sql要如此复杂才能完成上述几个功能,其中一部分Rails轻松能做到,这也算是Linq to Sql搞得过于复杂的地方。

    在Rails中,要选择特定的列,Model不需要改变,在find()方法中给出你要的列就是了,不用的列自然不会填充。如果需要在SQL语句最后追加指令,例如"WITH(NOLOCK)"?也是直接在find()方法里面增加参数就可以了。

  24. 老赵
    admin
    链接

    老赵 2008-02-20 22:40:00

    @Cat Chen
    WITH (NOLOCK)是修改,规则可能很复杂,rails里怎么追加参数呢?

  25. 光影传说
    *.*.*.*
    链接

    光影传说 2008-02-21 10:01:00

    @Jeffrey Zhao
    老赵,我是非常喜欢返回List这种类型,这样的后续处理好像与数据库无关了,因为是一个很通用的集合。
    这里带来了一个问题,如果是远程处理,那么,用List这个返回数据还可以吗?
    在服务器端,可能通过引用传递,变更是不用操心记录到后台的。有客户端如何处理呢?

  26. 老赵
    admin
    链接

    老赵 2008-02-21 11:22:00

    @光影传说
    List自然可以序列化后进行传递,呵呵。

  27. 光影传说
    *.*.*.*
    链接

    光影传说 2008-02-22 09:20:00

    @Jeffrey Zhao
    关键是如何记录变化了的数据,返回的List如何方便感知哪些数据是变化的,哪些数据是删除的,哪些数据是新添加的。因为从客户端传过来之后,则触发也断了,没有了Table<>,只有List如何处理呢?如果让手动去处理,那么,容易出错,工作量也是很大的。如果能把这个问题解决,我的ORM层会换成Linq。
    从[Column(Storage="_CustomerID", DbType="UniqueIdentifier NOT NULL", IsPrimaryKey=true)]
    public System.Guid CustomerID
    {
    get
    {
    return this._CustomerID;
    }
    set
    {
    if ((this._CustomerID != value))
    {
    this.OnCustomerIDChanging(value);
    this.SendPropertyChanging();
    this._CustomerID = value;
    this.SendPropertyChanged("CustomerID");
    this.OnCustomerIDChanged();
    }
    }
    }

    这里可以看的出,Linq的历史记录应该是保存在Table<>里的,我没有跟踪过,不敢确定。
    当转换成List并序列化后,则这种关联应该去掉了。这种做法感觉是把单个对象与集合捆的太死,如果对象是单个的,则处理起来感觉有一些不方便,被束缚住了手脚。这也是比较困扰我的一个地方,我也想把这一块理清楚。
    如果与我的服务层关联,也要对数据处理上加一些改造,还有业务层的组织,这些都要考虑,又是一个很大的工作量。
    希望有机会能跟您探讨一下....

  28. 老赵
    admin
    链接

    老赵 2008-02-22 11:07:00

    @光影传说
    不用担心,Translate后的数据是进入DataContext的Table中的,您可以修改并SubmitChanges,DataContext会跟踪对象状态。

  29. 光影传说
    *.*.*.*
    链接

    光影传说 2008-02-22 11:30:00

    @Jeffrey Zhao
    关键是序列化、反序列化之后,如果全部是在服务器端运行,不跨进程、CPU、网络,应该没有问题,是通过引用传递的。但是序列化传输之后,已经没有了DataContext,没有了Table,如何跟踪对象状态?

  30. 老赵
    admin
    链接

    老赵 2008-02-22 14:53:00

    @光影传说
    这也是非常常见的场景,是个ORM框架都会遇到,所以Table提供了Attach方法,用于把一个对象“附加”给某个DataContext.

  31. 光影传说
    *.*.*.*
    链接

    光影传说 2008-02-22 18:02:00

    @Jeffrey Zhao
    处理的感觉是半手工处理,要很多人工代码参与进去。
    示例上的代码与想像相似。是有Attach方法,但是达到的效果还有改进的余地。手工参与的还是比较多的。没有实现统一处理。

  32. eidolon[未注册用户]
    *.*.*.*
    链接

    eidolon[未注册用户] 2008-03-06 08:21:00

    Attach并不是万能的, 更新效率不高, 更重要的还需要一个timestamp的字段来配合, 做版本比较, 这个比较郁闷.

  33. 老赵
    admin
    链接

    老赵 2008-03-06 11:03:00

    @eidolon
    不过似乎没有更好的解决方案

  34. Guest[未注册用户]
    *.*.*.*
    链接

    Guest[未注册用户] 2008-03-12 18:15:00

    不太明白这句:因为默认情况下每个Item对象的ItemComment集合不会被同时查询出来,而是会等到内层的foreach循环执行时再次查询数据库


    能详细一点吗?

  35. Weatherreport[未注册用户]
    *.*.*.*
    链接

    Weatherreport[未注册用户] 2008-03-25 14:08:00

    public List<Item> GetItemsForListing(int ownerId)
    {
    ItemDataContext dataContext = new ItemDataContext();
    var query = from item in dataContext.Items
    where item.UserID == ownerId
    orderby item.CreateTime descending
    select new MyItem{
    UserID = item.UserID
    };

    return query.ToList();
    }

    public MyItem : Item{}

    这样的做法好像 还是可以的

  36. 光影传说
    *.*.*.*
    链接

    光影传说 2008-03-26 09:36:00

    @Weatherreport
    这样处理是不错,但是根本的问题还是没有解决。

  37. S.Sams
    *.*.*.*
    链接

    S.Sams 2008-04-08 10:43:00

    .ToList() 无法返回指定的实体类属性, 在最新版本中问题还没有解决.
    方式1: var query = from s in siteconfig.SiteConfig select s;
    方式2: var query = from s in siteconfig.SiteConfig
    select new
    {
    s.ID,
    s.Application,
    s.CompanyName
    };

    在进行 .ToList() 转换时 方式2 还是出错, 需要转换的属性不存在!
    最新的 .ExecuteQuery<T>("select id,application,companyname from").ToList() 是没问题, 但这样处理直接写SQl体现不出LinQ的优势.

    有没有更好的解决方案?




  38. 老赵
    admin
    链接

    老赵 2008-04-08 14:51:00

    @S.Sams
    抛出什么异常?生成的SQL语句是什么?

  39. Vincent Love
    *.*.*.*
    链接

    Vincent Love 2008-04-08 17:37:00

    linq to sql 里面有没有像link这样的关键字啊

    StartsWith,contains好像不太符合要求

    我想在一段字符串中查找某个词组,就是模糊查询
    比如在【在LINQ to SQL中使用Translate方法以及修改查询用SQL】中找
    LINQ to SQL

    where title like '%LINQ to SQL%' 这种方式如何转换成LINQ to SQL的写法呢

  40. 老赵
    admin
    链接

    老赵 2008-04-08 23:23:00

    @Vincent Love
    有些SQL语句的确是LINQ to SQL无法表示的。

  41. S.Sams
    *.*.*.*
    链接

    S.Sams 2008-04-09 12:07:00

    @Jeffrey Zhao
    编译无法通过 List<SiteConfig> 类型无法转换

    @Vincent Love
    like '%LINQ to SQL%' 这个是可以的
    得用 SqlMethods.Like(a.Title, "C%")

  42. 老赵
    admin
    链接

    老赵 2008-04-09 12:09:00

    @S.Sams
    生成的SQL语句是什么呢?

  43. S.Sams
    *.*.*.*
    链接

    S.Sams 2008-04-09 12:10:00

    @Jeffrey Zhao
    编译无法通过 List<SiteConfig> 类型无法转换
    估计是在 from s in siteconfig.SiteConfig
    select new
    {
    s.ID,
    s.Application,
    s.CompanyName
    }; 时, 限制了属性的输出, 在用 ToList() 转换时 其它属性不存在.
    而用 List<SiteConfig> = (from s in siteconfig.SiteConfig select s).ToList() 编译运行都是没问题的.

    用ExecuteQuery是没问题的, 只是其它属性为空而已
    .ExecuteQuery("select id,application,companyname from").ToList()

  44. Vincent Love
    *.*.*.*
    链接

    Vincent Love 2008-04-09 12:27:00

    --引用--------------------------------------------------
    S.Sams: @Jeffrey Zhao
    编译无法通过 List&lt;SiteConfig&gt; 类型无法转换

    @Vincent Love
    like '%LINQ to SQL%' 这个是可以的
    得用 SqlMethods.Like(a.Title, &quot;C%&quot;)
    --------------------------------------------------------
    SqlMethods.Like 这个直接这么写吗?
    在哪个命名空间下?

  45. Vincent Love
    *.*.*.*
    链接

    Vincent Love 2008-04-09 12:30:00

    @S.Sams
    找到了,呵呵
    using System.Data.Linq.SqlClient;

  46. 老赵
    admin
    链接

    老赵 2008-04-09 13:40:00

    @S.Sams
    我的文章里写了解决方法了阿,我不是扩展了一个ExecuteQuery方法吗?接受一个IQuerable作为参数。
    直接ToList()肯定无法通过的,因为是一个匿名类型的List。

  47. <∩扫地僧∩>
    *.*.*.*
    链接

    <∩扫地僧∩> 2008-06-03 14:49:00

    找了好久,终于在这里发现了,谢谢谢谢!

  48. lauralxj[未注册用户]
    *.*.*.*
    链接

    lauralxj[未注册用户] 2008-07-02 17:14:00

    有個問題:
    通過存储过程获取的数据如何绑定到控件上(非Grid),如TextBox
    比如:mdc.GetBlogByID(1) 可以直接做為數據源進行與grid 控制項的綁定
    但只綁其中一個值要如何做?
    下冇這樣做:
    Blog b = (Blog)(mdc.GetBlogByID(1));
    報錯:
    法將型別 'SingleResult`1[Blog]' 的物件轉換為型別 'Blog'
    請幫忙解答!謝謝老趙!

  49. js.daiwei[未注册用户]
    *.*.*.*
    链接

    js.daiwei[未注册用户] 2008-07-08 14:44:00

    你好,老赵,我现在有个疑惑,烦请你能帮忙看一下

    var ctx = dataContext = new ItemDataContext();

    var list = ctx.GetTable<T>().ToList();

    //这时候,ctx的sql connection的状态是关闭的.然后

    using(var scope = new TransactionScope())
    {
    var ctx = dataContext = new ItemDataContext();

    var list = ctx.GetTable<T>().ToList();

    //这时候ctx的sql connection的连接是打开的.

    //我现在要做的是var list = ctx.GetTable<T>().ToList();这个方法执行后,ctx自动关闭sql connection,不知道怎么做.谢谢!
    }

  50. jilu
    *.*.*.*
    链接

    jilu 2008-08-01 16:45:00

    把键直接去掉,省的麻烦,反正没有什么作用

  51. last-time[未注册用户]
    *.*.*.*
    链接

    last-time[未注册用户] 2008-09-14 23:09:00

    看了赵老师的文章后受益匪浅,我最近才接触LINQ TO SQL也发现了您文中提到的缺点,也在致力解决这个问题,下面我提一下我的思路,希望赵老师指正。
    一个Customer类包含2个字段,string UserID,string LoginID
    我也是关注ExecuteQuery<T>()这个方法,不同是的我将Custoemr类作为一个条件容器,比如我要查询UserID为‘2’的用户我只需要将一个customer对象的UserID赋值2即可以下是部分代码
    Customer c=new Customer();
    c.UserID = 2;
    SelectPanel<Customer> selectPanel = new SelectPanel<Customer>(c, SelectMode.All);
    OpenIDDataContext linq = new OpenIDDataContext();
    List<Customer> listCustomer = linq.ExecuteQuery<Customer>(selectPanel.StrSql, selectPanel.Parameters).ToList();
    现在我解决了简单的And关系的查询稍微复杂的还不能解决,虽然和您实现的方式不一样,但是目的是一样

  52. last-time[未注册用户]
    *.*.*.*
    链接

    last-time[未注册用户] 2008-09-14 23:18:00

    补充一下,我用反射来创建查询条件,通过读取当前实体的属性然后处理成sql语句。我观察了下LINQ TO SQL生成的实体类除了主键外,貌似都支持NullAble所以都能用Property.GetVale()来判断当前属性是否为NULL。我的这个方法有局限性,不能在循环中构建SQL语句,因为使用了反射,那会降低效率,所以只能是用于一次查使询代码编写者尽量减少不必要的查询结果,然后再使用linq to sql进行精加工数据。

  53. airwolf2026
    *.*.*.*
    链接

    airwolf2026 2008-12-15 13:57:00

    先看了老外的才知道为啥这个异常存在.再看老赵的,才知道可以解决的那么漂亮.

    不过有个想偷懒的东西不明白,就是.如果就以这里面的两张表来说.Item和itemcomments,如果我要获得每个Item的itemname和它的评论条数的话,好像还是得需要写一个实体类?比如 ItemCount包含itemName 和Count两个属性,不然不知道如何在不同的层之间传递.

  54. airwolf2026
    *.*.*.*
    链接

    airwolf2026 2008-12-15 16:12:00

    -_-!!!.下午一个问题用老赵的方法好像不行,在看看老赵额外补充说明的,如果扩展的属性在原来数据库表里面没有相应字段的话,是不会出来结果的.
    比如有个需求是这样的.需要Item表的一些字段,同时需要ItemComment对应的一条评论内容Content
    var query = from i in db.Item
    select new
    {
    ItemID=i.ItemID,
    Title = i.Title,
    Content = i.ItemComment.Content
    }
    return db.ExecuteQuery<Item>(query);

    查看生成的SQL是有内联查询Content的,可是返回的结果集里面Content就是null

  55. 魔方网[未注册用户]
    *.*.*.*
    链接

    魔方网[未注册用户] 2009-03-03 10:00:00

    上乘之作,赞! http://www.mofun.cc

  56. 深蓝
    *.*.*.*
    链接

    深蓝 2009-03-16 20:26:00

    好文章,受教了

  57. 菩提树下的杨过
    *.*.*.*
    链接

    菩提树下的杨过 2009-05-31 15:55:00

    虽然N久以前就看过,今天又过来重温了一遍,产生了一个疑问:整个项目中,需求是多样化的,有时候可能只需要显示一个字段,又时候又需要显示二个字段...,如何让代码最简单?(按文中思路,好象这种情况得做N个版本的Method,有没有象传统方式那样,传入DisplayFields列表就能搞定的解决办法),另外这样做性能上肯定是优化了,但可能会有一个潜在问题,因为取出的实体中,只有部分属性有值,其它未查询出来的字段,对应的属性应该是默认值,如果其它开发成员,在不清楚哪些属性有值,哪些属性是默认值的情况下,随便调用Item.xxx会不会产生问题?

  58. taskman[未注册用户]
    *.*.*.*
    链接

    taskman[未注册用户] 2009-07-20 15:09:00

    item.Comments是哪里来的 如何定义

  59. 十口
    *.*.*.*
    链接

    十口 2009-09-01 12:25:00

    Models.JLSMDataContext jl=new Models.JLSMDataContext();
    var q = from c in jl.Member
    where c.MemberId == id
    select c.MemberName.ToString();
    Models.Member model1 = jl.ExecuteQuery<Models.Member>("select MemberId,MemberName from member where memberid={0}", id).First();
    Response.Write(model1);
    var q1 = from c in jl.Member
    where c.MemberId == id
    select new Models.Member(){MemberName=c.MemberName,MemberId=c.MemberId };
    Response.Write(q1.ToList()); //可恶就这里出现了 不允许在查询中显式构造实体类型“Models.Member”。异常
    System.Collections.Generic.List<Models.Member> list = new System.Collections.Generic.List<Models.Member> (){new Models.Member() { MemberName="admin",MemberId=1}};
    Response.Write(SiteHelper.ToJson(list));

  60. 十口
    *.*.*.*
    链接

    十口 2009-09-01 12:33:00

    还有如果有关联的表的话。比如文章里的items,这个类里多了个ItemComment 属性 被查询出来后。
    items.ItemComment也查出来了。
    我那个好好的ToJson结果gameover了。
    后来我人为的在dbml里把那条关联线给砍了。
    结果我的toJson复活了。

  61. 十口
    *.*.*.*
    链接

    十口 2009-09-01 12:38:00

    后来发现其实建立视图也可以再需求变化不是很大的情况下。可以用用。

  62. 深圳WEB[未注册用户]
    *.*.*.*
    链接

    深圳WEB[未注册用户] 2009-10-29 09:15:00

    如果我要做一个两表的查询,返回每个表的相应字段,如何转换?
    现扩充一下,如果是三个表、四个、五个,甚至更多,我要一个一个的这样写代码吗?

  63. Silent Void
    *.*.*.*
    链接

    Silent Void 2010-01-09 14:26:00

    ItemDataContext dataContext = new ItemDataContext();
    借问下,用完的DataContext需要手动Dispose吗?

    DataContext实现了IDisposable接口,不知道是否有必要手动释放一次?我看MSDN中的示例和网上的很多例子,都没有手动释放。

  64. 淘宝点评网[未注册用户]
    *.*.*.*
    链接

    淘宝点评网[未注册用户] 2010-03-20 00:31:00

    不行啊,DbCommand command = dataContext.GetCommand(query);在这句还是会报这个错误.

  65. hui
    220.232.110.*
    链接

    hui 2011-04-05 13:01:25

    博主,如果我要多表查询,并返回每张表里的相应字段,该怎么做呢?

  66. lose.zhang
    114.242.165.*
    链接

    lose.zhang 2011-11-09 11:47:09

    老赵,我来写个分页的吧,如果把整个表都ToList(),那数据量太大了吧,呵呵

  67. lose.zhang
    114.242.165.*
    链接

    lose.zhang 2011-11-09 17:00:25

    不好意思,刚才发的有问题,再发一个:

    /// <summary>
    /// DataContext扩展方法
    /// </summary>
    public static class DataContextExtends
    {
        /// <summary>
        /// ExecuteQuery方法扩展,将对象以redader方式转换为实体
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dataContext"></param>
        /// <param name="query"></param>
        /// <returns></returns>
        public static List<T> ExecuteQuery<T>(this DataContext dataContext, IQueryable<object> query)
        {
            return ExecuteQuery<T>(dataContext, query, 1, query.Cast<T>().Count());
        }
    
        /// <summary>
        /// ExecuteQuery方法扩展,代表分页的
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="dataContext"></param>
        /// <param name="query"></param>
        /// <param name="pageIndex"></param>
        /// <param name="pageSize"></param>
        /// <returns></returns>
        public static List<T> ExecuteQuery<T>(this DataContext dataContext, IQueryable<object> query, int pageIndex, int pageSize)
        {
            int total = query.Count();
            int totalPages = total / pageSize;
    
            if (total % pageSize > 0)
                totalPages++;
    
            if (pageIndex > totalPages)
            {
                pageIndex = totalPages;
            }
            if (pageIndex < 1)
            {
                pageIndex = 1;
            }
            query.Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();
            DbCommand command = dataContext.GetCommand(query);
            dataContext.OpenConnection();
            using (DbDataReader reader = command.ExecuteReader())
            {
                return dataContext.Translate<T>(reader).ToList();
            }
        }
        private static void OpenConnection(this DataContext dataContext)
        {
            if (dataContext.Connection.State == ConnectionState.Closed)
                dataContext.Connection.Open();
        }
    
    }
    
  68. shell
    114.83.63.*
    链接

    shell 2011-11-30 18:38:01

    说真的,直接写SQL多舒服,不用拐着弯子去把原本很直接的SQL语句翻译成Linq方式,可能是我Linq语法用的少,不习惯吧。

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我