兴国安邦

C# 3.0, Linq, Linq To Sql

博客园 首页 新随笔 联系 订阅 管理
  33 Posts :: 0 Stories :: 530 Comments :: 52 Trackbacks

动态的生成sql语句,根据不同的条件构造不同的where字句,是拼接sql 字符串的好处。而Linq的推出,是为了弥补编程中的 Data != Object 的问题。我们又该如何实现用object的动态查询呢?

1,用object的查询是什么?
我们可以简单的举这么一个例子。我们到公安局查找一个人。首先,我们会给出他的一些特征,比如,身高多少,年龄多少,性别,民族等。那么,我们把这个人的一些特征输入电脑。我们希望,电脑能给我们返回这个人的信息。而实际上,有相同特征的人太多了,常常返回一个集合。那让我们把这个过程抽象到程式里。我们需要new出来一个对象。这个对象包含了我们能知道的基本信息。而后,把这个对象传给Linq To Sql,等待返回结果。

根据这些基本的需求,我们来定义下面的函数,为了实现这个函数对任何实体都是有用的,我们把它定义为generic的。为了不破坏Linq To Sql延迟加载的规矩,我们把它的返回类型定义为IQueryable。如下:

public IQueryable<TEntity> Find<TEntity>(TEntity obj) where TEntity : class

思路出来了,先new出来一个对象,然后把对象传给这个函数,我们渴望它能返回与这个对象匹配的结果集。为了让它和DataContext有关系,我们把这个函数放到DataContext的partial类里。鼠标右击Linq To Sql文件,选择view code,这个时候,vs会为你创造一个DataContext的partial类,其扩展名比影射文件少了中间的desiger。大家要注意,你如果想自己修改影射文件,请放到这个文件里。这样当影射code被刷新时,才不会冲掉你自己的修改。先大体描述下我们的思路。

NorthwindDataContext db = new NorthwindDataContext();
//先new出一个对象            
Customer c = new Customer();
//添入我们知道的最基本的信息,可以从ui获得
c.City = "London";
c.Phone 
= "23236133";
//call函数find返回结果
var q = db.Find<Customer>(c);

2,原理
Linq To Sql支持用户动态生成lambda表达式。本文中所实现的方法,正是反射加lambda动态表达式。我们先来看如何动态生成lambda表达式。在Linq 中,lambda表达式会首先转化为Expression Tree,本文并不详解Expression Tree。Expression Tree是lambda表达式从code的形式转化为data的结果,是一种更高效的在内存中的数据结构。比如: 
Func<int,int> f = x => x + 1;                               // Code

Expression<Func<int,int>> e = x => x + 1;       // Data

第二个,其实也就是第一个转化后的形式。那好了,有了这个前提,我们就可以动态构造这个Expression Tree了。
// 先构造了一个ParameterExpression对象,这里的c,就是Lambda表达中的参数。(c=>)                           
ParameterExpression param = Expression.Parameter(typeof(TEntity), "c"); 
//构造表达式的右边,值的一边
Expression right = Expression.Constant(p.GetValue(obj, null));
//构造表达式的左边,property一端。
Expression left = Expression.Property(param, p.Name);
//生成筛选表达式。即c.CustomerID == "Tom"
Expression filter = Expression.Equal(left, right);
//生成完整的Lambda表达式。
Expression<Func<TEntity, bool>> pred = Expression.Lambda<Func<TEntity, bool>>(filter, param);
//在这里,我们使用的是and条件。
query = query.Where(pred);

3,反射在本方法中的作用
因为我们采用了模板,也就是说,我们并不知道传进来的对象会有那些property,那反射在这里就提供一个很好的方法。我们可以通过反射去遍历每一个property,只有判断出该property的值不为null时,才将其视为条件。该函数完整的代码如下:

        public IQueryable<TEntity> Find<TEntity>(TEntity obj) where TEntity : class
        
{
            
//获得所有property的信息
            PropertyInfo[] properties = obj.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance);
            
//构造初始的query
            IQueryable<TEntity> query = this.GetTable<TEntity>().AsQueryable<TEntity>();
            
//遍历每个property
            foreach (PropertyInfo p in properties)
            
{
                
if (p != null)
                
{
                    Type t 
= p.PropertyType;
                    
//加入object,Binary,和XDocument, 支持sql_variant,imager 和xml等的影射。
                    if (t.IsValueType || t == typeof(string|| t == typeof(System.Byte[])
                        
|| t == typeof(object|| t == typeof(System.Xml.Linq.XDocument)
                        
|| t == typeof(System.Data.Linq.Binary))
                    
{
                        
//如果不为null才算做条件
                        if ( p.GetValue(obj, null!= null)
                        
{
                            ParameterExpression param 
= Expression.Parameter(typeof(TEntity), "c");
                            Expression right 
= Expression.Constant(p.GetValue(obj, null));
                            Expression left 
= Expression.Property(param, p.Name);
                            Expression filter 
= Expression.Equal(left,right);

                            Expression
<Func<TEntity, bool>> pred = Expression.Lambda<Func<TEntity, bool>>(filter, param);
                            query 
= query.Where(pred);
                        }

                    }

                }

            }

            
return query;
        }

4,测试用例及反思
我们用下面的例子来测试下这个函数

            Customer c = new Customer();
            c.City 
= "London";
            c.Phone 
= "23236133";

            var q 
= db.Find<Customer>(c).ToList();

其生成的sql语句为:

SELECT [t0].[CustomerID][t0].[CompanyName][t0].[ContactName][t0].[ContactTitle][t0].[Address][t0].[City][t0].[Region][t0].[PostalCode][t0].[Country][t0].[Phone][t0].[Fax]
FROM [dbo].[Customers] AS [t0]
WHERE ([t0].[Phone] = @p0AND ([t0].[City] = @p1)
-- @p0: Input NVarChar (Size = 8; Prec = 0; Scale = 0) [23236133]
--
 @p1: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [London]

我们可以看到,其sql语句中,只有city和phone两个条件。并且他们之间是and的关系。我们最开始的设想实现了,但是,它是完美的吗?如果是or条件该怎么办呢?更多的时候,我们是在用模糊查询,那又该怎么办呢?这个问题,就留于下篇。

最后,介绍一种写log的方法。stream流使用static为避免多个datacontext的同时在使用log.txt文件。

    partial class DataMappingDataContext
    
{
        
private static StreamWriter sw = new StreamWriter(Path.Combine(Directory.GetCurrentDirectory(), "log.txt"),true);

        
/// <summary>
        
/// Try to create DataContext with log. 
        
/// </summary>
        
/// <param name="withLog"></param>


        
public DataMappingDataContext(bool withLog)
            : 
this()
        
{
            OnCreated();
            
if (withLog)
            
{
                
if (sw == null)
                
{
                    sw 
= new StreamWriter(Path.Combine(Directory.GetCurrentDirectory(), "log.txt"), true);
                }

                
this.Log = sw;
            }

        }


        
/// <summary>
        
/// try to close streamwriter
        
/// </summary>
        
/// <param name="disposing"></param>

        protected override void Dispose(bool disposing)
        
{
            
base.Dispose(disposing);
            sw.Flush();

        }

    }

在dispose函数里,把输出流flush。使用时,如下
using(northwind db = new norhwind(true))
{
//do something......
}
好,就先讲到这里。

posted on 2007-09-09 21:21 Tom Song 阅读(4413) 评论(45)  编辑 收藏 所属分类: C# 3.0Linq To Sql

Feedback

#1楼  2007-09-09 22:03 路过 [未注册用户]
哇塞~~太高端了~~~
敏捷开发的前提是基本功扎实啊~~~
  回复  引用    

#2楼  2007-09-09 22:10 Spring.Cheung      
沙发,讲的很好。
  回复  引用  查看    

#3楼  2007-09-10 00:51 ekinwt [未注册用户]
good...
  回复  引用    

#4楼  2007-09-10 01:37 路人丁 [未注册用户]
文章不错
  回复  引用    

#5楼  2007-09-10 10:32 古巴 [未注册用户]
“值不为null时,才将其视为条件”

但有时候,就需要查询某条件为null的结果,所以这个方法有一定局限性,而且反射对性能也有一定的影响。
  回复  引用    

#6楼  2007-09-10 15:42 小菲 [未注册用户]
出现下面的错误是什么原因啊?谢谢
错误 1 无法创建关联“pm_dictionaries_pm_people”。属性没有匹配的类型:“id”,“zw_id”。


  回复  引用    

#7楼  2007-09-10 15:47 小菲 [未注册用户]
我检查过关联的字段: id int identity not null,
zw int not null,
  回复  引用    

#8楼  2007-09-10 15:48 小菲 [未注册用户]
我检查过关联的字段: id int identity not null,
zw_id int not null
  回复  引用    

#9楼 [楼主] 2007-09-10 18:33 宋国安      
@小菲
检查它们的数据库类型。
@古巴
是的,你说的对。没有完美的方案。而且,我们也不可能覆盖所有的情况。本文提供的一个方法,只是最常用的一种,而且是比较容易理解的。
  回复  引用  查看    

#10楼  2007-09-12 10:47 int08h [未注册用户]
遇上Nullable类型的时候,似乎Expression不能很好的工作,以下是一个很简单的示例:
首先是自定义一个类来封装一个int?类型
class MyClass
{
private int? myInt;

public int? MyInt
{
get { return myInt; }
set { myInt = value; }
}
}
其次是Expression的生成:
class Program
{
static void Main(string[] args)
{
MyClass i = new MyClass();
i.MyInt = 3;
ParameterExpression param = Expression.Parameter(typeof(MyClass), "c");
Expression left = Expression.Property(param, "MyInt");
Expression right = Expression.Constant(i.MyInt);
Expression filter = null;
try
{
filter = Expression.Equal(left, right);
}
catch (InvalidOperationException ex)
{
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
}
运行此段程序,catch块会捕捉到异常,内容为:
The binary operator Equal is not defined for the types 'System.Nullable`1[System.Int32]' and 'System.Int32'.
  回复  引用    

#11楼  2007-09-12 23:49 张洪庆 [未注册用户]
那位大侠遇到过我这个问题吗?
我在调用DataContext 的 SubmitChanges() 时会发生异常.
SQL Server does not handle comparison of NText, Text, Xml, or Image data types.
我用的是vs orcas beta1 . 表结构很简单。就是简单的更新.但是无论更新还是删除,在我SubmitChanges() 的时候,都会遇到那个异常 .请问有人知道为什么吗?
  回复  引用    

#12楼  2007-09-13 11:07 int08h [未注册用户]
是因为没有IsVersion=true的属性的时候,LINQ产生的SQL会将所有的属性值与数据库中的值进行比较,当有NText, Text, Xml, Image等属性的时候,SQL SERVER不支持比较的功能
给主键的属性加上IsVersion=true应该就没问题了
  回复  引用    

#13楼  2007-09-13 11:08 int08h [未注册用户]
SQL SERVER不支持Text和NText的比较,但是无论是Text还是Char,在LINQ里都用string表示,那么我做动态查询的时候,要怎么去判断后省略掉Text类型呢?用反射取得DBType是不是太慢了
  回复  引用    

#14楼  2007-09-13 11:24 int08h [未注册用户]
昨天试了一下动态查询,还有一些问题,所以改了一下:
1、TEXT,NTEXT,IMAGE,XML等格式的相等比较SQL SERVER是不支持的,所以要排除,以下是具体代码:
//
//SQL Server does not support comparison of TEXT, NTEXT, XML and IMAGE
//
Attribute attr =
Attribute.GetCustomAttribute(property, typeof(ColumnAttribute));
if (attr != null)
{
string dbType = (attr as ColumnAttribute).DbType.ToUpper();
if (dbType.Contains("TEXT") || dbType.Contains("NTEXT") ||
dbType.Contains("XML") || dbType.Contains("IMAGE"))
{
continue;
}
}

2、有一部分类型是需要转换的,因为Expression.Equal不支持两边类型不同的情况, 这一点在Nullable<>出现时很明显,用以下代码改进
Expression left = Expression.Property(param, property);
//
//If convert needed
//
if (convertRequired(property.PropertyType))
{
left = Expression.Convert(left, value.GetType());
}
其中的方法:
private bool convertRequired(Type type)
{
if (type.IsGenericType)
{
Type genericTypeDefinition = type.GetGenericTypeDefinition();

return genericTypeDefinition.Equals(typeof(Nullable<>));
}
else
{
return false;
}
}

全部的代码是这样的,肯定改的不好,如有问题请提出:
public IQueryable<T> GetQuery(T entity)
{
IQueryable<T> query = DataContext.GetTable<T>().AsQueryable();

//
//Get all properties
//
PropertyInfo[] properties = typeof(T).GetProperties();

//
//Add every readable property to query
//
foreach (PropertyInfo property in properties)
{
object value = property.GetValue(entity, null);

//
//SQL Server does not support comparison of TEXT, NTEXT, XML and IMAGE
//
Attribute attr =
Attribute.GetCustomAttribute(property, typeof(ColumnAttribute));
if (attr != null)
{
string dbType = (attr as ColumnAttribute).DbType.ToUpper();
if (dbType.Contains("TEXT") || dbType.Contains("NTEXT") ||
dbType.Contains("XML") || dbType.Contains("IMAGE"))
{
continue;
}
}

if (argumentChecker.Check(value))
{
//
//Build Expression
//
ParameterExpression param =
Expression.Parameter(typeof(T), "entity");
Expression left = Expression.Property(param, property);
//
//If convert needed
//
if (convertRequired(property.PropertyType))
{
left = Expression.Convert(left, value.GetType());
}
Expression right =
Expression.Constant(value);
Expression filter =
Expression.Equal(left, right);
Expression<Func<T, bool>> predicate =
Expression.Lambda<Func<T, bool>>(filter, param);

query = query.Where(predicate);
}
}

return query;
}

private bool convertRequired(Type type)
{
if (type.IsGenericType)
{
Type genericTypeDefinition = type.GetGenericTypeDefinition();

return genericTypeDefinition.Equals(typeof(Nullable<>));
}
else
{
return false;
}
}
  回复  引用    

#15楼  2007-09-13 11:56 张洪庆 [未注册用户]
谢谢 [int08h] 我的问题解决了. 谢谢.

  回复  引用    

#16楼 [楼主] 2007-09-13 18:25 宋国安      
@int08h
谢谢你的回答。关于你提到的问题,我正在调查。
  回复  引用  查看    

#17楼 [楼主] 2007-09-13 18:35 宋国安      
@int08h
先换成这个。再等我的消息。看看是设计时就是这样的还是产品的问题。

Expression right = Expression.Constant(i.MyInt,typeof(int?));

  回复  引用  查看    

#18楼  2007-09-13 23:49 int08h [未注册用户]
是这样的,我并不知道一定是int?,也可能是long?,float?...,所以你所给的这个写法我怕是行不通的,我现在的方法就是用Expression.Convert转一下,代码在上面写了...
  回复  引用    

#19楼 [楼主] 2007-09-14 08:49 宋国安      
@int08h
不好意思,没有注意到你后面自己已经回答了这个问题。这个问题的根本在于

int? k = 5;
k.GetType() 返回的是System.Int32而不是nullable。所以,可以用下面的方案解决。反射可以返回nullable的类型。

Expression right = Expression.Constant(p.GetValue(obj, null),p.PropertyType);

关于你提到的几个类型,我到真的没有测试过。感谢你的测试。
  回复  引用  查看    

#20楼  2007-09-14 09:21 int08h [未注册用户]
使用你的方法已经解决了问题,非常感谢
  回复  引用    

#21楼  2007-09-17 13:54 小菲 [未注册用户]
出现了“不能添加其键已在使用中的实体”的错误,是怎么回事啊,谢谢
  回复  引用    

#22楼  2007-09-17 14:17 laver520 [未注册用户]
EMMSDatabaseDataContext data = new EMMSDatabaseDataContext("************");
var dst = from p in data.InspectionSafeties
select p;

Error 1 Could not find an implementation of the query pattern for source type 'System.Data.Linq.Table<EMMS.Entity.InspectionSafety>'. 'Select' not found. Are you missing a reference to 'System.Core.dll' or a using directive for 'System.Linq'? D:\wwwroot\EMMSDesktop\EMMS.Facade\Inspection\frmRoadSafety.cs 127 34 EMMS.Facade

怎么出现这个错误,请大家看看怎么解决
  回复  引用    

#23楼  2007-09-17 17:32 小菲 [未注册用户]
我的小软件现在有38个表,相互关联,用一个 LINQ to SQL 类来表达;有点担心稳定性和性能,请问作者有什么看法啊,谢谢
  回复  引用    

#24楼 [楼主] 2007-09-18 10:19 宋国安      
@laver520
你没有添加引用吧?'System.Core.dll' 和 System.Data.Linq.DLL
  回复  引用  查看    

#25楼 [楼主] 2007-09-18 10:22 宋国安      
@小菲
出现了“不能添加其键已在使用中的实体”的错误. 就是说你的数据已经存在于数据库中。你是不是在attach时出现了这个情况。

第二个问题。不会有稳定性的问题。一个类可以表达38个表???所有的可能数据库的情况都已经覆盖过了。你可以大胆使用。
  回复  引用  查看    

#26楼  2007-09-19 08:35 小菲 [未注册用户]
如果不能上网,如何安装.net framework 3.5 beta2 啊? 有的网是严格要求不能连接internet的
  回复  引用    

#27楼  2007-09-19 18:54 int08h [未注册用户]
把整个VS2008 BETA2拖下来就行了
  回复  引用    

#28楼 [楼主] 2007-09-21 23:22 宋国安      
@小菲
.net framework 3.5 beta2是有单独的安装程序的.
  回复  引用  查看    

#29楼  2007-09-28 11:01 小菲 [未注册用户]
@宋大侠:
.net framework 3.5 beta2哪有单独安装程序啊? 网上的都是只有2.9M左右,安装的时候还要连接到网上下载一些东西才能安装;而公安网是不能连接互连网的,怎么办啊?
  回复  引用    

#30楼 [楼主] 2007-09-28 11:20 宋国安      
@小菲
晕呀。居然是个女警官呀。。倒,先数落一下自己有没有干啥坏事。哈哈。

.net 3.5需要先安装.net 3.0 才可以。所以,你在家里,把整个orcas beta2拽下来的了。然后,在wcu\dotNetFramework目录下,你可以找到一大堆.net framework,把它们复制到你工作的机器上,安装。完毕。
  回复  引用  查看    

#31楼  2007-09-28 11:47 小菲 [未注册用户]
谢谢,不要意思,太苯了;工作很多年了,项目几个人在做,我负责协调一下;另外想安装sql server 2005,但安装后发现菜单里只有“配置”,新建不了数据库
  回复  引用    

#32楼 [楼主] 2007-09-28 12:02 宋国安      
@小菲
你sql server并没有真的安装上去。
  回复  引用  查看    

#33楼  2007-09-28 12:15 小菲 [未注册用户]
我的安装过程是这样的:运行sql server 2005 cd1 选择"服务器组件、工具、联机丛书和示例(C)",然后就开始安装....不是这样吗
  回复  引用    

#34楼 [楼主] 2007-09-28 13:22 宋国安      
@小菲
安装出错的常见问题是你选的版本和你操作系统不兼容。比如,你选了企业版,操作系统那就该是server版的才可以。标准版才可以装在xp上。
  回复  引用  查看    

#35楼  2007-09-28 14:45 小菲 [未注册用户]
@宋大虾
操作系统是windows server 2003 enterprise edition(sp2)
sql server 2005的安装界面标识的是sql server 2005 enterprise edition;
这样匹配吗? windows server 2003应该安装什么版本的sqlserver2005?谢谢
  回复  引用    

#36楼  2007-09-28 15:09 小菲 [未注册用户]
安装后,只有sql server configuration manager
  回复  引用    

#37楼 [楼主] 2007-09-28 17:02 宋国安      
@小菲
是的。你并没有把sql server引擎安装上。你的版本是匹配的。请找team中懂这个的人来处理吧。我也说不清楚为什么你没装上。或许和你的设置或安装过程中选项有关。
  回复  引用  查看    

#38楼  2007-12-08 09:42 vv [未注册用户]
class 中有一int属性,new class此属性为0,而有时会查找数量为0.
这个怎么处理。
  回复  引用    

#39楼  2007-12-08 16:19 vv [未注册用户]
@int08h 的代码出错,是什么原因。

Error 1 The type or namespace name 'T' could not be found (are you missing a using directive or an assembly reference?)
我using的空间
using System.Linq;
using System.Reflection;
using System;
using System.Linq.Expressions;
using System.Collections.Generic;
using System.Data.Linq;
  回复  引用    

--引用--------------------------------------------------
vv: class 中有一int属性,new class此属性为0,而有时会查找数量为0.
这个怎么处理。
--------------------------------------------------------
非主键的值类型字段类型都是Nullable的,new class后属性不是0而是null。
Nullable的查找方式上面都已经实现了。
  回复  引用    

--引用--------------------------------------------------
vv: class 中有一int属性,new class此属性为0,而有时会查找数量为0.
这个怎么处理。
--------------------------------------------------------
如果该字段本身在数据库中不可以为空,那么你也可以改映射,把映射改成Nullable。
  回复  引用