[转载]Lucene学习总结之五:Lucene段合并(merge)过程分析 - 觉先 - 博客园

mikel阅读(994)

[转载]Lucene学习总结之五:Lucene段合并(merge)过程分析 – 觉先 – 博客园.

一、段合并过程总论

IndexWriter中与段合并有关的成员变量有:

  • HashSet<SegmentInfo> mergingSegments = new HashSet<SegmentInfo>(); //保存正在合并的段,以防止合并期间再次选中被合并。
  • MergePolicy mergePolicy = new LogByteSizeMergePolicy(this);//合并策略,也即选取哪些段来进行合并。
  • MergeScheduler mergeScheduler = new ConcurrentMergeScheduler();//段合并器,背后有一个线程负责合并。
  • LinkedList<MergePolicy.OneMerge> pendingMerges = new LinkedList<MergePolicy.OneMerge>();//等待被合并的任务
  • Set<MergePolicy.OneMerge> runningMerges = new HashSet<MergePolicy.OneMerge>();//正在被合并的任务

和段合并有关的一些 参数有:

  • mergeFactor:当大小几乎相当的段的数量达到此值的时候,开始合并。
  • minMergeSize: 所有大小小于此值的段,都被认为是大小几乎相当,一同参与合并。
  • maxMergeSize:当一个段的大小大于此值的时候,就不再 参与合并。
  • maxMergeDocs:当一个段包含的文档数大于此值的时候,就不再参与合并。

段合并 一般发生在添加完一篇文档的时候,当一篇文档添加完后,发现内存已经达到用户设定的ramBufferSize,则写入文件系统,形成一个新的段。新段的 加入可能造成差不多大小的段的个数达到mergeFactor,从而开始了合并的过程。

合并过程最重要的是两部分:

  • 一 个是选择哪些段应该参与合并,这一步由MergePolicy来决定。
  • 一个是将选择出的段合并成新段的过程,这一步由 MergeScheduler来执行。段的合并也主要包括:
    • 对正向信息的合并,如存储域,词向量,标准化因子等。
    • 对 反向信息的合并,如词典,倒排表。

在总论中,我们重点描述合并策略对段的选择以及反向信息的合并。

1.1、合并策略对段的选择

在LogMergePolicy中,选择可以合并的段的基本 逻辑是这样的:

  • 选择的可以合并的段都是在硬盘上的,不再存在内存中的段,也不是像早期的版本一样每添加一个Document 就生成一个段,然后进行内存中的段合并,然后再合并到硬盘中。
  • 由于从内存中flush到硬盘上是按照设置的内存大小来 DocumentsWriter.ramBufferSize触发的,所以每个刚flush到硬盘上的段大小差不多,当然不排除中途改变内存设置,接下来 的算法可以解决这个问题。
  • 合并的过程是尽量按照合并几乎相同大小的段这一原则,只有大小相当的mergeFacetor个段出现的 时候,才合并成一个新的段。
  • 在硬盘上的段基本应该是大段在前,小段在后,因为大段总是由小段合并而成的,当小段凑够 mergeFactor个的时候,就合并成一个大段,小段就被删除了,然后新来的一定是新的小段。
  • 比如 mergeFactor=3,开始来的段大小为10M,当凑够3个10M的时候,0.cfs, 1.cfs, 2.cfs则合并成一个新的段3.cfs,大小为30M,然后再来4.cfs, 5.cfs, 6.cfs,合并成7.cfs,大小为30M,然后再来8.cfs, 9.cfs, a.cfs合并成b.cfs, 大小为30M,这时候又凑够了3个30M的,合并成90M的c.cfs,然后又来d.cfs, e.cfs, f.cfs合并成10.cfs,大小为30M,然后11.cfs大小为10M,这时候硬盘上的段为:c.cfs(90M) 10.cfs(30M),11.cfs(10M)。

所以LogMergePolicy对合并段的选择过程如下:

  • 将所有的段按照生成的顺序,将段的大小以mergeFactor为底取对数,放入数组中,作为选择的标准。

幻灯片1

  • 从头开始,选择一个值最大的段,然后 将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为是大小差不多的段,属于同一阶梯,此处称为第一阶梯。
  • 然 后从后向前寻找第一个属于第一阶梯的段,从start到此段之间的段都被认为是属于这一阶梯的。也包括之间生成较早但大小较小的段,因为考虑到以下几点:
    • 防止较早生成的段由于人工flush或者人工调整ramBufferSize,因而很小,却破坏了基本从大到小的规则。
    • 如 果运行较长时间后,致使段的大小参差不齐,很难合并相同大小的段。
    • 也防止一个段由于较小,而不断的都有大的段生成从而始终不能参与 合并。
  • 第一阶梯总共4个段,小于mergeFactor因而不合并,接着start=end从而选择下一阶梯。

幻灯片2

  • 从start开始,选择一个值最大的 段,然后将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为属于同一阶梯,此处称为第二阶梯。
  • 然后从后向 前寻找第一个属于第二阶梯的段,从start到此段之间的段都被认为是属于这一阶梯的。
  • 第二阶梯总共4个段,小于 mergeFactor因而不合并,接着start=end从而选择下一阶梯。

幻灯片3

  • 从start开始,选择一个值最大的 段,然后将此段的值减去0.75(LEVEL_LOG_SPAN) ,之间的段被认为属于同一阶梯,此处称为第三阶梯。
  • 由于最大的 段减去0.75后为负的,因而从start到此段之间的段都被认为是属于这一阶梯的。
  • 第三阶梯总共5个段,等于 mergeFactor,因而进行合并。

幻灯片4

  • 第三阶梯的五个段合并成一个较大的 段。
  • 然后从头开始,依然先考察第一阶梯,仍然是4个段,不合并。
  • 然后是第二阶梯,因为有了新生成的段,并且 大小足够属于第二阶梯,从而第二阶梯有5个段,可以合并。

幻灯片5

  • 第二阶段的五个段合并成一个较大的 段。
  • 然后从头开始,考察第一阶梯,因为有了新生成的段,并且大小足够属于第一阶梯,从而第一阶梯有5个段,可以合并。

幻灯片6

  • 第一阶梯的五个段合并成一个大的段。

幻灯片7

1.2、反向信息的合并

反向信息的合并包括两部分:

  • 对字典的合并,词典中的Term是按照字典顺序排序的,需要对词典中的Term进行重新 排序
  • 对于相同的Term,对包含此Term的文档号列表进行合并,需要对文档号重新编号。

对词典的合 并需要找出两个段中相同的词,Lucene是通过一个称为match的SegmentMergeInfo类型的数组以及称为queue的 SegmentMergeQueue实现的,SegmentMergeQueue是继承于 PriorityQueue<SegmentMergeInfo>,是一个优先级队列,是按照字典顺序排序的。 SegmentMergeInfo保存要合并的段的词典及倒排表信息,在SegmentMergeQueue中用来排序的key是它代表的段中的第一个 Term。

我们来举一个例子来说明合并词典的过程,以便后面解析代码的时候能够很好的理解:

  • 假设要合并五个 段,每个段包含的Term也是按照字典顺序排序的,如下图所示。
  • 首先把五个段全部放入优先级队列中,段在其中也是按照第一个 Term的字典顺序排序的,如下图。

01

  • 从优先级队列中弹出第一个 Term(“a”)相同的段到match数组中,如下图。
  • 合并这些段的第一个Term(“a”)的倒排表,并把此Term和它的倒 排表一同加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

02

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

03

  • 从优先级队列中弹出第一个 Term(“b”)相同的段到match数组中。
  • 合并这些段的第一个Term(“b”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

04

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

05

  • 从优先级队列中弹出第一个 Term(“c”)相同的段到match数组中。
  • 合并这些段的第一个Term(“c”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

06

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

07

  • 从优先级队列中弹出第一个 Term(“d”)相同的段到match数组中。
  • 合并这些段的第一个Term(“d”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

08

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

09

  • 从优先级队列中弹出第一个 Term(“e”)相同的段到match数组中。
  • 合并这些段的第一个Term(“e”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

10

  • 将match数组中还有Term的段 重新放入优先级队列中,这些段也是按照第一个Term的字典顺序排序。

11

  • 从优先级队列中弹出第一个 Term(“f”)相同的段到match数组中。
  • 合并这些段的第一个Term(“f”)的倒排表,并把此Term和它的倒排表一同 加入新生成的段中。
  • 对于match数组中的每个段取下一个Term

12

  • 合并完毕。

二、段合并的详细过程

2.1、将缓存写入新的段

IndexWriter在添加文档的时候调用函数addDocument(Document doc, Analyzer analyzer),包含如下步骤:

  • doFlush = docWriter.addDocument(doc, analyzer);//DocumentsWriter添加文档,最后返回是否进行向硬盘写入
    • return state.doFlushAfter || timeToFlushDeletes();//这取决于timeToFlushDeletes

timeToFlushDeletes返回return (bufferIsFull || deletesFull()) && setFlushPending(),而在Lucene索引过程分析(2)的DocumentsWriter的缓存管理部分提到,当 numBytesUsed+deletesRAMUsed > ramBufferSize的时候bufferIsFull设为true,也即当使用的内存大于ramBufferSize的时候,则由内存向硬盘写入。 ramBufferSize可以用IndexWriter.setRAMBufferSizeMB(double mb)设定。

  • if (doFlush) flush(true, false, false);//如果内存中缓存满了,则写入硬盘
    • if (doFlush(flushDocStores, flushDeletes) && triggerMerge)  maybeMerge();//doFlush将缓存写入硬盘,此过程在Lucene索引过程分析(4)中关闭IndexWriter一节已经描述。

当缓存写入硬盘,形成了新的段后,就有可能触发一次段合并,所以调用maybeMerge()

IndexWriter.maybeMerge()

–> maybeMerge(false);

–> maybeMerge(1, optimize);

–> updatePendingMerges(maxNumSegmentsOptimize, optimize);

–> mergeScheduler.merge(this);

IndexWriter.updatePendingMerges(int maxNumSegmentsOptimize, boolean optimize)主要负责找到可以合并的段,并生产段合并任务对象,并向段合并器注册这个任务。

ConcurrentMergeScheduler.merge(IndexWriter) 主要负责进行段的合并。

2.2、选择合并段,生成合并任务

IndexWriter.updatePendingMerges(int maxNumSegmentsOptimize, boolean optimize)主要包括两部分:

  • 选择能够合并 段:MergePolicy.MergeSpecification spec = mergePolicy.findMerges(segmentInfos);
  • 向段合并器注册合并任务,将任务加到 pendingMerges中:
    • for(int i=0;i<spec.merges.size();i++)
      • registerMerge(spec.merges.get(i));

2.2.1、 用合并策略选择合并段

默认的段合并策略是LogByteSizeMergePolicy,其选择合并段由 LogMergePolicy.findMerges(SegmentInfos infos) 完成,包含以下过程:

(1) 生成levels数组,每个段一项。然后根据每个段的大小,计算每个项的值,levels[i]和段的大小的关系为 Math.log(size)/Math.log(mergeFactor),代码如下:

final int numSegments = infos.size();

float[] levels = new float[numSegments];

final float norm = (float) Math.log(mergeFactor);

for(int i=0;i<numSegments;i++) {

final SegmentInfo info = infos.info(i);

long size = size(info);

levels[i] = (float) Math.log(size)/norm;

}

(2) 由于段基本是按照由大到小排列的,而且合并段应该大小差不多的段中进行。我们把大小差不多的段称为属于同一阶梯,因而此处从第一个段开始找属于相同阶梯的 段,如果属于此阶梯的段数量达到mergeFactor个,则生成合并任务,否则继续向后寻找下一阶梯。

//计算最低阶梯值,所有小于此值的都属于最低阶梯

final float levelFloor = (float) (Math.log(minMergeSize)/norm);

MergeSpecification spec = null;

int start = 0;

while(start < numSegments) {

//找到levels数组的最大值,也即当 前阶梯中的峰值

float maxLevel = levels[start];

for(int i=1+start;i<numSegments;i++) {

final float level = levels[i];

if (level > maxLevel)

maxLevel = level;

}

//计算出此阶梯的谷值,也即最大值减去0.75,之间的都属于此阶梯。如果峰值小于最低阶梯值,则所有此阶梯的段都属于最低阶梯。如果峰值大于最低阶梯 值,谷值小于最低阶梯值,则设置谷值为最低阶梯值,以保证所有小于最低阶梯值的段都属于最低阶梯。

float levelBottom;

if (maxLevel < levelFloor)

levelBottom = -1.0F;

else {

levelBottom = (float) (maxLevel – LEVEL_LOG_SPAN);

if (levelBottom < levelFloor && maxLevel >= levelFloor)

levelBottom = levelFloor;

}

float levelBottom = (float) (maxLevel – LEVEL_LOG_SPAN);

//从最后一个段向左找,当然段越来越大,找到第一个大于此阶梯的谷值的段,从start的段开始,一直到upto这个段,都属于此阶梯了。尽管upto 左面也有的段由于内存设置原因,虽形成较早,但是没有足够大,也作为可合并的一员考虑在内了,将被并入一个大的段,从而保证了基本上左大右小的关系。从 upto这个段向右都是比此阶梯小的多的段,应该属于下一阶梯。

int upto = numSegments-1;

while(upto >= start) {

if (levels[upto] >= levelBottom) {

break;

}

upto–;

}

//从start段开始,数 mergeFactor个段,如果不超过upto段,说明此阶梯已经足够mergeFactor个了,可以合并了。当然如果此阶梯包含太多要合并的段,也 是每mergeFactor个段进行一次合并,然后再依次数mergeFactor段进行合并,直到此阶梯的段合并完毕。

int end = start + mergeFactor;

while(end <= 1+upto) {

boolean anyTooLarge = false;

for(int i=start;i<end;i++) {

final SegmentInfo info = infos.info(i);

//如果一个段的大小超过maxMergeSize或者一个段包含的文档 数量超过maxMergeDocs则不再合并。

anyTooLarge |= (size(info) >= maxMergeSize || sizeDocs(info) >= maxMergeDocs);

}

if (!anyTooLarge) {

if (spec == null)

spec = new MergeSpecification();

// 如果确认要合并,则从start到end生成一个段合并任务OneMerge.

spec.add(new OneMerge(infos.range(start, end), useCompoundFile));

}

//刚刚合并的是从start到end共mergeFactor和段,此阶梯还有更多的段,则再依次 数mergeFactor个段。

start = end;

end = start + mergeFactor;

}

//从start到upto是此阶梯的 所有的段,已经选择完毕,下面选择更小的下一个阶梯的段

start = 1+upto;

}

选择的结果保存在MergeSpecification中,结构如下:

spec    MergePolicy$MergeSpecification  (id=25)
merges    ArrayList<E>  (id=28)
elementData    Object[10]  (id=39)
[0]    MergePolicy$OneMerge  (id=42)
aborted    false
error    null
increfDone    false
info    null
isExternal    false
maxNumSegmentsOptimize    0
mergeDocStores    false
mergeGen    0
optimize    false
readers    null
readersClone    null
registerDone    false
segments    SegmentInfos  (id=50)
capacityIncrement    0
counter    0
elementCount    3
elementData    Object[10]  (id=54)
[0]    SegmentInfo  (id=62)
delCount    0
delGen    -1
diagnostics    HashMap<K,V>  (id=67)
dir    SimpleFSDirectory  (id=69)
docCount    1062
docStoreIsCompoundFile    false
docStoreOffset    0
docStoreSegment    “_0”
files    ArrayList<E>  (id=73)
hasProx    true
hasSingleNormFile    true
isCompoundFile    1
name    “_0”
normGen    null
preLockless    false
sizeInBytes    15336467
[1]    SegmentInfo  (id=64)
delCount    0
delGen    -1
diagnostics    HashMap<K,V>  (id=79)
dir    SimpleFSDirectory  (id=69)
docCount    1068
docStoreIsCompoundFile    false
docStoreOffset    1062
docStoreSegment    “_0”
files    ArrayList<E>  (id=80)
hasProx    true
hasSingleNormFile    true
isCompoundFile    1
name    “_1”
normGen    null
preLockless    false
sizeInBytes    15420953
[2]    SegmentInfo  (id=65)
delCount    0
delGen    -1
diagnostics    HashMap<K,V>  (id=86)
dir    SimpleFSDirectory  (id=69)
docCount    1068
docStoreIsCompoundFile    false
docStoreOffset    2130
docStoreSegment    “_0”
files    ArrayList<E>  (id=88)
hasProx    true
hasSingleNormFile    true
isCompoundFile    1
name    “_2”
normGen    null
preLockless    false
sizeInBytes    15420953
generation    0
lastGeneration    0
modCount    1
pendingSegnOutput    null
userData    Collections$EmptyMap  (id=57)
version    1267460515437
useCompoundFile    true
modCount    1
size    1

2.2.2、注册段合并任务

注册段合并任务由 IndexWriter.registerMerge(MergePolicy.OneMerge merge)完成:

(1) 如果选择出的段正在被合并,或者不存在,则退出。

final int count = merge.segments.size();

boolean isExternal = false;

for(int i=0;i<count;i++) {

final SegmentInfo info = merge.segments.info(i);

if (mergingSegments.contains(info))

return false;

if (segmentInfos.indexOf(info) == -1)

return false;

if (info.dir != directory)

isExternal = true;

}

(2) 将合并任务加入pendingMerges:pendingMerges.add(merge);

(3) 将要合并的段放入mergingSegments以防正在合并又被选为合并段。

for(int i=0;i<count;i++)
mergingSegments.add(merge.segments.info(i));

2.3、段合并器进行段合并

段合并器默认为 ConcurrentMergeScheduler,段的合并工作由 ConcurrentMergeScheduler.merge(IndexWriter) 完成,它包含while(true)的循环,在循环中不断做以下事情:

  • 得到下一个合并任 务:MergePolicy.OneMerge merge = writer.getNextMerge();
  • 初始化合并任 务:writer.mergeInit(merge);
    • 将删除文档写入硬盘:applyDeletes();
    • 是 否合并存储域:mergeDocStores = false。按照Lucene的索引文件格式(2)中段的元数据信息(segments_N)中提到 的,IndexWriter.flush(boolean triggerMerge, boolean flushDocStores, boolean flushDeletes)中第二个参数flushDocStores会影响到是否单独或是共享存储。其实最终影响的是 DocumentsWriter.closeDocStore()。每当flushDocStores为false时,closeDocStore不被调 用,说明下次添加到索引文件中的域和词向量信息是同此次共享一个段的。直到flushDocStores为true的时候,closeDocStore被 调用,从而下次添加到索引文件中的域和词向量信息将被保存在一个新的段中,不同此次共享一个段。如2.1节中说的那样,在addDocument中,如果 内存中缓存满了,则写入硬盘,调用的是flush(true, false, false),也即所有的存储域都存储在共享的域中(_0.fdt),因而不需要合并存储域。
    • 生成新的段:merge.info = new SegmentInfo(newSegmentName(),…)
    • 将新的段加入mergingSegments
  • 如果已经有足够多的段合并线程,则等待while (mergeThreadCount() >= maxThreadCount) wait();
  • 生成新的段合并线程:
    • merger = getMergeThread(writer, merge);
    • mergeThreads.add(merger);
  • 启动段合并线程:merger.start();

段合并线程的类型为 MergeThread,MergeThread.run()包含while(truy)循环,在循环中做以下事情:

  • 合并当 前的任务:doMerge(merge);
  • 得到下一个段合并任务:merge = writer.getNextMerge();

ConcurrentMergeScheduler.doMerge(OneMerge) 最终调用IndexWriter.merge(OneMerge) ,主要做以下事情:

  • 初始化合并任 务:mergeInit(merge);
  • 进行合并:mergeMiddle(merge);
  • 完成合并任 务:mergeFinish(merge);
    • 从mergingSegments中移除被合并的段和合并新生成的段:
      • for(int i=0;i<end;i++) mergingSegments.remove(sourceSegments.info(i));
      • mergingSegments.remove(merge.info);
    • 从runningMerges中移除此合并任务:runningMerges.remove(merge);

IndexWriter.mergeMiddle(OneMerge)主要做以下几件事情:

  • 生成用于合并段的对象SegmentMerger merger = new SegmentMerger(this, mergedName, merge);
  • 打开Reader指向要合并的段:
merge.readers = new SegmentReader[numSegments];

merge.readersClone = new SegmentReader[numSegments];

for (int i = 0; i < numSegments; i++) {

final SegmentInfo info = sourceSegments.info(i);

// Hold onto the “live” reader; we will use this to

// commit merged deletes

SegmentReader reader = merge.readers[i] = readerPool.get(info, merge.mergeDocStores,MERGE_READ_BUFFER_SIZE,-1);

// We clone the segment readers because other

// deletes may come in while we’re merging so we

// need readers that will not change

SegmentReader clone = merge.readersClone[i] = (SegmentReader) reader.clone(true);

merger.add(clone);

}

  • 进行段合 并:mergedDocCount = merge.info.docCount = merger.merge(merge.mergeDocStores);
  • 合并生成的段生成为 cfs:merger.createCompoundFile(compoundFileName);

SegmentMerger.merge(boolean) 包含以下几部分:

  • 合并域:mergeFields()
  • 合并词典和倒排 表:mergeTerms();
  • 合并标准化因子:mergeNorms();
  • 合并词向 量:mergeVectors();

下面依次分析者几部分。

2.3.1、合并存储域

合并存储域主要包含两部分:一部分是合并fnm信息,也即域元数据信息,一部分是合并fdt,fdx信息,也即域数据信息。

(1) 合并fnm信息

  • 首先生成新的域元数据信息:fieldInfos = new FieldInfos();
  • 依次用reader读取每个合并段的域元数据信息,加入上述对象
for (IndexReader reader : readers) {

SegmentReader segmentReader = (SegmentReader) reader;

FieldInfos readerFieldInfos = segmentReader.fieldInfos();

int numReaderFieldInfos = readerFieldInfos.size();

for (int j = 0; j < numReaderFieldInfos; j++) {

FieldInfo fi = readerFieldInfos.fieldInfo(j);

//在通常情况下,所有的段中的文档都包 含相同的域,比如添加文档的时候,每篇文档都包含”title”,”description”,”author”,”time”等,不会为某一篇文档添加 或减少与其他文档不同的域。但也不排除特殊情况下有特殊的文档有特殊的域。因而此处的add是无则添加,有则更新。

fieldInfos.add(fi.name, fi.isIndexed, fi.storeTermVector,

fi.storePositionWithTermVector, fi.storeOffsetWithTermVector,

!reader.hasNorms(fi.name), fi.storePayloads,

fi.omitTermFreqAndPositions);

}

}

  • 将域元数据信息fnm写入文件:fieldInfos.write(directory, segment + “.fnm”);

(2) 合并段数据信息fdt, fdx

在合并段的数据信息的时候,有两种情况:

  • 情况一:通常情况,要合并的段和新生成段包含的域的名称,顺序都是一样的,这样就可以把要合并的段的fdt信息直接拷贝到新生成段的最后,以提 高合并效率。
  • 情况二:要合并的段包含特殊的文档,其包含的域多于或者少于新生成段的域,这样就不能够直接拷贝,而是一篇文档一篇文 档的添加。这样合并效率大大降低,因而不鼓励添加文档的时候,不同的文档使用不同的域。

具体过程如下:

  • 首先检查要合并的各个段,其包含域的名称,顺序是否同新生成段的一致,也即是否属于第一种情 况:setMatchingSegmentReaders();
private void setMatchingSegmentReaders() {

int numReaders = readers.size();

matchingSegmentReaders = new SegmentReader[numReaders];

//遍历所有的要合并的段

for (int i = 0; i < numReaders; i++) {

IndexReader reader = readers.get(i);

if (reader instanceof SegmentReader) {

SegmentReader segmentReader = (SegmentReader) reader;

boolean same = true;

FieldInfos segmentFieldInfos = segmentReader.fieldInfos();

int numFieldInfos = segmentFieldInfos.size();

//依次比较要合并的段和新生成的段的段名,顺序是否 一致。

for (int j = 0; same && j < numFieldInfos; j++) {

same = fieldInfos.fieldName(j).equals(segmentFieldInfos.fieldName(j));

}

//最后生成matchingSegmentReaders数组,如果此数组的第i项不是null,则 说明第i个段同新生成的段名称,顺序完全一致,可以采取情况一得方式。如果此数组的第i项是null,则说明第i个段包含特殊的域,则采取情况二的方式。

if (same) {

matchingSegmentReaders[i] = segmentReader;

}

}

}

}

  • 生成存储域的写对象:FieldsWriter fieldsWriter = new FieldsWriter(directory, segment, fieldInfos);
  • 依次遍历所有的要合并的段,按照上 述两种情况,使用不同策略进行合并
int idx = 0;

for (IndexReader reader : readers) {

final SegmentReader matchingSegmentReader = matchingSegmentReaders[idx++];

FieldsReader matchingFieldsReader = null;

//如果 matchingSegmentReader!=null,表示此段属于情况一,得到matchingFieldsReader

if (matchingSegmentReader != null) {

final FieldsReader fieldsReader = matchingSegmentReader.getFieldsReader();

if (fieldsReader != null && fieldsReader.canReadRawDocs()) {

matchingFieldsReader = fieldsReader;

}

}

//根据此段是否包含删除的文档采取不同的策略

if (reader.hasDeletions()) {

docCount += copyFieldsWithDeletions(fieldsWriter, reader, matchingFieldsReader);

} else {

docCount += copyFieldsNoDeletions(fieldsWriter,reader, matchingFieldsReader);

}

}

  • 合并包含删除文档的段
private int copyFieldsWithDeletions(final FieldsWriter fieldsWriter, final IndexReader reader,

final FieldsReader matchingFieldsReader)

throws IOException, MergeAbortedException, CorruptIndexException {

int docCount = 0;

final int maxDoc = reader.maxDoc();

//matchingFieldsReader!=null, 说明此段属于情况一, 则可以直接拷贝。

if (matchingFieldsReader != null) {

for (int j = 0; j < maxDoc;) {

if (reader.isDeleted(j)) {

// 如果文档被删除,则跳过此文档。

++j;

continue;

}

int start = j, numDocs = 0;

do {

j++;

numDocs++;

if (j >= maxDoc) break;

if (reader.isDeleted(j)) {

j++;

break;

}

} while(numDocs < MAX_RAW_MERGE_DOCS);

//从要合并的段中从第start篇文档开始,依次读取numDocs篇文档的文档长度到rawDocLengths中。

IndexInput stream = matchingFieldsReader.rawDocs(rawDocLengths, start, numDocs);

//用fieldsStream.copyBytes(…)直接将fdt信息从要合并的段拷贝到新生成的段,然后将上面读出的rawDocLengths 转换成为每篇文档在fdt中的偏移量,写入fdx文件。

fieldsWriter.addRawDocuments(stream, rawDocLengths, numDocs);

docCount += numDocs;

checkAbort.work(300 * numDocs);

}

} else {

//matchingFieldsReader==null,说明此段属于情况二,必须每篇文档依次添加。

for (int j = 0; j < maxDoc; j++) {

if (reader.isDeleted(j)) {

// 如果文档被删除,则跳过此文档。

continue;

}

//同addDocument 的过程中一样,重新将文档添加一遍。

Document doc = reader.document(j);

fieldsWriter.addDocument(doc);

docCount++;

checkAbort.work(300);

}

}

return docCount;

}

  • 合 并不包含删除文档的段:除了跳过删除的文档的部分,同上述过程一样。
  • 关闭存储域的写对 象:fieldsWriter.close();

2.3.2、合并标准化因子

合并标准化因子的过程比较简单,基本就是对每一个域,用指向合并段的reader读出标准化因子,然后再写入新生成的段。

private void mergeNorms() throws IOException {

byte[] normBuffer = null;

IndexOutput output = null;

try {

int numFieldInfos = fieldInfos.size();

//对于每一个域

for (int i = 0; i < numFieldInfos; i++) {

FieldInfo fi = fieldInfos.fieldInfo(i);

if (fi.isIndexed && !fi.omitNorms) {

if (output == null) {

//指向新生成的段的nrm文件的写入流

output = directory.createOutput(segment + “.” + IndexFileNames.NORMS_EXTENSION);

//写nrm文件头

output.writeBytes(NORMS_HEADER,NORMS_HEADER.length);

}

//对于每一个合并段的reader

for ( IndexReader reader : readers) {

int maxDoc = reader.maxDoc();

if (normBuffer == null || normBuffer.length < maxDoc) {

// the buffer is too small for the current segment

normBuffer = new byte[maxDoc];

}

//读出此段的nrm信息。

reader.norms(fi.name, normBuffer, 0);

if (!reader.hasDeletions()) {

//如果没有文档被删除则写入新生成的段。

output.writeBytes(normBuffer, maxDoc);

} else {

//如果有文档删除则跳过删除的文档写入新生成的段。

for (int k = 0; k < maxDoc; k++) {

if (!reader.isDeleted(k)) {

output.writeByte(normBuffer[k]);

}

}

}

checkAbort.work(maxDoc);

}

}

}

} finally {

if (output != null) {

output.close();

}

}

}

2.3.3、合并词向量

合并词向量的过程同合并存储域的过程非常相似,也包括两种情况:

  • 情况一:通常情况,要合并的段和新生成段包含的域的名称,顺序都是一样的,这样就可以把要合并的段的词向量信息直接拷贝到新生成段的 最后,以提高合并效率。
  • 情况二:要合并的段包含特殊的文档,其包含的域多于或者少于新生成段的域,这样就不能够直接拷贝,而是一篇 文档一篇文档的添加。这样合并效率大大降低,因而不鼓励添加文档的时候,不同的文档使用不同的域。

具体过程如下:

  • 生成词向量的写对象:TermVectorsWriter termVectorsWriter = new TermVectorsWriter(directory, segment, fieldInfos);
  • 依次遍历所有的要合并的 段,按照上述两种情况,使用不同策略进行合并
int idx = 0;

for (final IndexReader reader : readers) {

final SegmentReader matchingSegmentReader = matchingSegmentReaders[idx++];

TermVectorsReader matchingVectorsReader = null;

//如果matchingSegmentReader!=null,表示此段属于情况一,得到matchingFieldsReader

if (matchingSegmentReader != null) {

TermVectorsReader vectorsReader = matchingSegmentReader.getTermVectorsReaderOrig();

if (vectorsReader != null && vectorsReader.canReadRawDocs()) {

matchingVectorsReader = vectorsReader;

}

}

//根据此段是否包含删除的文档采取不同的策略

if (reader.hasDeletions()) {

copyVectorsWithDeletions(termVectorsWriter, matchingVectorsReader, reader);

} else {

copyVectorsNoDeletions(termVectorsWriter, matchingVectorsReader, reader);

}

}

  • 合并包含 删除文档的段
private void copyVectorsWithDeletions(final TermVectorsWriter termVectorsWriter, final TermVectorsReader matchingVectorsReader, final IndexReader reader)

throws IOException, MergeAbortedException {

final int maxDoc = reader.maxDoc();

//matchingFieldsReader!=null,说明此段属于情况一, 则可以直接拷贝。

if (matchingVectorsReader != null) {

for (int docNum = 0; docNum < maxDoc;) {

if (reader.isDeleted(docNum)) {

// 如果文档被删除,则跳过此文档。

++docNum;

continue;

}

int start = docNum, numDocs = 0;

do {

docNum++;

numDocs++;

if (docNum >= maxDoc) break;

if (reader.isDeleted(docNum)) {

docNum++;

break;

}

} while(numDocs < MAX_RAW_MERGE_DOCS);

//从要合并的段中从第start篇文档开始,依次读取 numDocs篇文档的tvd到rawDocLengths中,tvf到rawDocLengths2。

matchingVectorsReader.rawDocs(rawDocLengths, rawDocLengths2, start, numDocs);

//用tvd.copyBytes(…)直接将tvd信息从要合并的段拷贝到新生成的 段,然后将上面读出的rawDocLengths转换成为每篇文档在tvd文件中的偏移量,写入tvx文件。用tvf.copyBytes(…)直接将 tvf信息从要合并的段拷贝到新生成的段,然后将上面读出的rawDocLengths2转换成为每篇文档在tvf文件中的偏移量,写入tvx文件。

termVectorsWriter.addRawDocuments(matchingVectorsReader, rawDocLengths, rawDocLengths2, numDocs);

checkAbort.work(300 * numDocs);

}

} else {

//matchingFieldsReader==null,说明此段属于情况二,必须每篇文档依次添加。

for (int docNum = 0; docNum < maxDoc; docNum++) {

if (reader.isDeleted(docNum)) {

// 如果文档被删除,则跳过此文档。

continue;

}

//同addDocument的过程中一样,重新将文档添加一遍。

TermFreqVector[] vectors = reader.getTermFreqVectors(docNum);

termVectorsWriter.addAllDocVectors(vectors);

checkAbort.work(300);

}

}

}

  • 合并不包含删除文档的段:除了跳过删除的文档的部分,同上述过程一样。
  • 关闭词向量的写对 象:termVectorsWriter.close();

2.3.4、合并词典和倒排表

以上都是合并正向信息,相对过程比较清晰。而合并词典和倒排表就不这么简单了,因为在词典中,Lucene要求按照字典顺序排序,在倒排表中,文 档号要按照从小到大顺序排序排序,在每个段中,文档号都是从零开始编号的。

所以反向信息的合并包括两部分:

  • 对 字典的合并,需要对词典中的Term进行重新排序
  • 对于相同的Term,对包含此Term的文档号列表进行合并,需要对文档号重新编 号。

后者相对简单,假设如果第一个段的编号是0~N,第二个段的编号是0~M,当两个段合并成一个段的时候,第一个段的编号 依然是0~N,第二个段的编号变成N~N+M就可以了,也即增加一个偏移量(前一个段的文档个数)。

对词典的合并需要找出两个段中相同的 词,Lucene是通过一个称为match的SegmentMergeInfo类型的数组以及称为queue的SegmentMergeQueue实现 的,SegmentMergeQueue是继承于PriorityQueue<SegmentMergeInfo>,是一个优先级队列,是按 照字典顺序排序的。SegmentMergeInfo保存要合并的段的词典及倒排表信息,在SegmentMergeQueue中用来排序的key是它代 表的段中的第一个Term。

在总论部分,举了一个例子表明词典和倒排表合并的过程。

下面让我们深入代码看一看具体的实 现:

(1) 生成优先级队列,并将所有的段都加入优先级队列。

//在Lucene索引过程分析(4)中提到 过,FormatPostingsFieldsConsumer 是用来写入倒排表信息的。

//FormatPostingsFieldsWriter.addField(FieldInfo field)用于添加索引域信息,其返回FormatPostingsTermsConsumer用于添加词信息。

//FormatPostingsTermsConsumer.addTerm(char[] text, int start)用于添加词信息,其返回FormatPostingsDocsConsumer用于添加freq信息

//FormatPostingsDocsConsumer.addDoc(int docID, int termDocFreq)用于添加freq信息,其返回FormatPostingsPositionsConsumer用于添加prox信息

//FormatPostingsPositionsConsumer.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength)用于添加prox信息

FormatPostingsFieldsConsumer consumer = new FormatPostingsFieldsWriter(state, fieldInfos);

// 优先级队列

queue = new SegmentMergeQueue(readers.size());

//对于每一个段

final int readerCount = readers.size();

for (int i = 0; i < readerCount; i++) {

IndexReader reader = readers.get(i);

TermEnum termEnum = reader.terms();

//生成SegmentMergeInfo对象,termEnum就是此段的词典及倒排表。

SegmentMergeInfo smi = new SegmentMergeInfo(base, termEnum, reader);

//base就是下一个段的文档号偏移量,等于此段的文档数目。

base += reader.numDocs();

if (smi.next()) //得到段的第一个Term

queue.add(smi); //将此段放入优先级队列。

else

smi.close();

}

(2) 生成match数组

SegmentMergeInfo[] match = new SegmentMergeInfo[readers.size()];

(3) 合并词典

//如果队列不为空,则合并尚未结束

while (queue.size() > 0) {

int matchSize = 0;

//取出优先级队列的第一个段,放到match数组中

match[matchSize++] = queue.pop();

Term term = match[0].term;

SegmentMergeInfo top = queue.top();

//如果优先级队列的最顶端和已经弹出的match中的段的第一个Term相同,则全部弹出。

while (top != null && term.compareTo(top.term) == 0) {

match[matchSize++] =  queue.pop();

top =  queue.top();

}

if (currentField != term.field) {

currentField = term.field;

if (termsConsumer != null)

termsConsumer.finish();

final FieldInfo fieldInfo = fieldInfos.fieldInfo(currentField);

//FormatPostingsFieldsWriter.addField(FieldInfo field)用于添加索引域信息,其返回FormatPostingsTermsConsumer用于添加词信息。

termsConsumer = consumer.addField(fieldInfo);

omitTermFreqAndPositions = fieldInfo.omitTermFreqAndPositions;

}

//合并match数组中的所有的段的第一个Term的倒排表信息,并写入新生成的段。

int df = appendPostings(termsConsumer, match, matchSize);

checkAbort.work(df/3.0);

while (matchSize > 0) {

SegmentMergeInfo smi = match[—matchSize];

//如果match中的段还有下一个Term,则放回优先级队列,进行下一轮的循环。

if (smi.next())

queue.add(smi);

else

smi.close();

}

}

(4) 合并倒排表

private final int appendPostings(final FormatPostingsTermsConsumer termsConsumer, SegmentMergeInfo[] smis, int n)

throws CorruptIndexException, IOException {

//FormatPostingsTermsConsumer.addTerm(char[] text, int start)用于添加词信息,其返回FormatPostingsDocsConsumer用于添加freq信息

//将match数组中段的第一个Term添加到新生成的段中。

final FormatPostingsDocsConsumer docConsumer = termsConsumer.addTerm(smis[0].term.text);

int df = 0;

for (int i = 0; i < n; i++) {

SegmentMergeInfo smi = smis[i];

//得到要合并的段的位置信息(prox)

TermPositions postings = smi.getPositions();

//此段的文档号偏移量

int base = smi.base;

//在要合并的段中找到Term的倒排表位置。

postings.seek(smi.termEnum);

//不断得到下一篇文档号

while (postings.next()) {

df++;

int doc = postings.doc();

//文档号都要加上偏移量

doc += base;

//得到词频信息(frq)

final int freq = postings.freq();

//FormatPostingsDocsConsumer.addDoc(int docID, int termDocFreq)用于添加freq信息,其返回FormatPostingsPositionsConsumer用于添加prox信息

final FormatPostingsPositionsConsumer posConsumer = docConsumer.addDoc(doc, freq);

//如果位置信息需要保存

if (!omitTermFreqAndPositions) {

for (int j = 0; j < freq; j++) {

//得到位置信息(prox)以及payload信息

final int position = postings.nextPosition();

final int payloadLength = postings.getPayloadLength();

if (payloadLength > 0) {

if (payloadBuffer == null || payloadBuffer.length < payloadLength)

payloadBuffer = new byte[payloadLength];

postings.getPayload(payloadBuffer, 0);

}

//FormatPostingsPositionsConsumer.addPosition(int position, byte[] payload, int payloadOffset, int payloadLength)用于添加prox信息

posConsumer.addPosition(position, payloadBuffer, 0, payloadLength);

}

posConsumer.finish();

}

}

}

docConsumer.finish();

return df;

}

[转载]5分钟快速建立项目SVN版本控制

mikel阅读(974)

[转载]5分钟快速建立项目版本控制 – Face Code,Brain bloom – 博客园.

无论是个人进行单独编码还是团体开发项目,项目的版本控制都是很重要的。就我所知的版本控制方式有两种。

  1. 最简单的版本控制就是保留软件不同版本的数份copy,并且适当编号。许多大型开发案都是使用这种简单技巧。虽然这种方法能用,但是很没效率。一 是因为保存的数份copy几乎完全一样,也因为这种方法要高度依靠开发者的自我纪律,而常导致错误。
  2. 使用版本控制工具。常用的windows平台下的版本控制工具有svn,cvs,vss。

如果您还没有用过版本控制工具构建项目管理,本文将带你快速入门,本文使用的工具是基于svn的。

  • 必备软件(这些软件目前是比较新的版本)
  1. tortoiseSVN 1.65
  2. Visual SVN Server 2.11(Visual SVN Server是自带Subversion和Apache的)
  3. Visual SVN 1.77, Crack 破解(Visual Studio插件)
  4. SVN 电子书(可选,如果想深入了解SVN,可以一读)

如果以上文件不能下载,请访问http://cid-d61a87db430d0cab.skydrive.live.com/browse.aspx/.Public/svn这 个地址,下载文件都放在这里了。

  • 安装过程

tortoiseSVN和Visual SVN的安装过程我就不赘述了,下面主要讲Visual SVN Server的安装过程。
01 02

下面的步骤就是next和install了。到此,软件环境就准备好了。

  • 如何创建一个使用版本控制的Hello World项目

1.建立SVN服务器版本库(Create Repository)

    • 创建用户

      05
      07
    • 创建版本库

      03 04
    • 分配权限

      08
      09

2.把项目导入到服务器版本库中(Import project to repository)

10 11 12 13 14 15

3. 客户端签出项目(check out the project)

16 17

签出以后,可以看到每个文件前都有绿色的小图标

18
19

[转载]表达式树基础

mikel阅读(1149)

[转载][翻译]表达式树基础 – 甜番薯 – 博客园.

原文来自Charlie CalvertExpression Tree Basics

表达式树基础

刚接触LINQ的人往往觉得表达式树很不容易 理解。通过这篇文章我希望大家看到它其实并不像想象中那么难。您只要有普通的LINQ知识便可以轻松理解本文。
表达式树提供一个将可执行代码转换成数据的方 法。如果你要在执行代码之前修改或转换此代码,那么它是非常有价值的。尤其是当你要将C#代码—-如LINQ查询表达式转换成其他代码在另一个程序 —-如SQL数据库里操作它。
但是我在这里颠倒顺序,在文章最后你很容易发现为什么将代码转换到数据中去很有用。首 先我需要提供一点背景知识。让我们开始看看相关的创建表达式树的简单语法。
表达式树的语法


考虑下面简单的 Lambda表达式:

Func<int, int, int> function = (a,b) => a + b;
这个语句包含三个部分:

  1. 一个声明: Func<int, int, int> function
  2. 一个等号: =
  3. 一个lambda表达式: (a,b) => a + b;


变量 function指向两个数字相加的原生可执行代码。上面三步的lambda表达式表示一个简短的如下的手写方法:

public int function(int a, int b)
{
return a + b;
}


上面的方法或 lambda表达式都可以这样调用:

int c = function(3, 5);


当方法调用后,变量c将 被设成3+5,即8。

上面声明中第一步委托类型Func是在System命名空间中为我们定义好的:

public delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);
这个代码看上去很复杂,但它在这里只是用来帮我们定义变量function,变量 function赋值为非常简单的两个数字相加的lambda表达式。即使你不懂委托和泛型,你仍然应该清楚这是一个声明可执行代码变量引用的方法。在这 个例子里它指向一个非常简单的可执行代码。
将代码转换到数据中
在上一节,你看到怎么声明一个指向原生可执行 代码的变量。表达式树不是可执行代码,它是一种数据结构。那么我们怎么从表达式的原生代码转换成表达式树?怎么从代码转换成数据?
LINQ提供一个简单语法用来将代码转换到名 叫表达式树的数据结构。首先添加using语句引入Linq.Expressions命名空间:
using System.Linq.Expressions;


现在我们可以创建一个表 达式树:

Expression<Func<int, int, int>> expression = (a,b) => a + b;
跟上个例子一样的lambda表达式用来转换到类型为 Expression<T>的表达式树。标识expression不是可执行代码;它是一个名叫表达式树的数据结构。
Visual Studio 2008的samples包含一个叫ExpressionTreeVisualizer的程序。它可以用来呈现表达式树。图1你可 以看到一个展示上面简单表达式语句的对话框截图。注意,对话框上面部分显示的是lambda表达式,下面是用TreeView控件显示的其组成部分。

ExpressionTree


图1:
VS2008 C# Samples中的ExpressionTreeVisualizer创建一个表达式树的象征性的输出

编写代码来探索表达式树
我们的例子是一个Expression<TDelegate>。 Expression<TDelegate>类有四个属性:
  • Body: 得到表达式的主体。
  • Parameters: 得到lambda表达式的参数.
  • NodeType: 获取树的节点的ExpressionType。共45种不同值,包含所有表达式节点各种 可能的类型,例如返回常量,例如返回参数,例如取两个值的小值(<),例如取两个值的大值(>),例如将值相加(+),等等。
  • Type: 获取表达式的一个静态类型。在这个例子里,表达式的类型是Func<int, int, int>。
如果我们折叠图1的树节 点,Expression<TDelegate>的四个属性便显示得很清楚:
ExpressionTreeProperties
图2:将树节点折叠起来,你可以很容易的看到 Expression<TDelegate>类的四个主要属性。
你可以使用这四个属性开始探索表达式树。例如,你可以通过这样找到参数的名称:
Console.WriteLine(“参数1: {0}, 参数2: {1}”, expression.Parameters[0], expression.Parameters[1]);
这句代码输出值ab
参数1: a, 参数2: b
这个很容易在图1的ParameterExpression节点找到。
让我们在接下来的代码探索表达式的Body,在这个例子里是(a + b):
BinaryExpression body = (BinaryExpression)expression.Body;
ParameterExpression left = (ParameterExpression)body.Left;
ParameterExpression right = (ParameterExpression)body.Right;
Console.WriteLine(expression.Body);
Console.WriteLine(” 表达式左边部分: “ + “{0}{4} 节点类型: {1}{4} 表达式右边部分: {2}{4} 类型: {3}{4}”, left.Name, body.NodeType, right.Name, body.Type, Environment.NewLine);

这段代码产生如下输入:

(a + b)
表达式左边部 分: a
节点类型:  Add
表达式右边部分: b
类型: System.Int32

同样,你会发现很容易在图1的Body节点中 找到这些信息。

通过探索表达式树,我们可以分析表达式的各个部分发现它的组成。你可以看见,我们的 表达式的所有元素都展示为像节点这样的数据结构。表达式树是代码转换成的数据。
编译一个表达式:将数据转换回代码
如果我们可以将代码转换到数据,那么我们也应该能将数据转换回代码。这里是让编译器 将表达式树转换到可执行代码的简单代码。
int result = expression.Compile()(3, 5);
Console.WriteLine(result);
这段代码会输出值8,跟本文最初声明的lambda函数的执行结果一样。
IQueryable<T>和表达式树
现在至少你有一个抽象的概念理解表达式树,现在是时候回来理解其在LINQ中的关键 作用了,尤其是在LINQ to SQL中。花点时间考虑这个标准的LINQ to SQL查询表达式:
var query = from c in db.Customers
where c.City == “Nantes”
select new { c.City, c.CompanyName };
你可能知道,这里LINQ表达式返回的变量query是IQueryable类型。 这里是IQueryable类型的定义:
public interface IQueryable : IEnumerable
{
Type ElementType { get; }
Expression Expression { get; }
IQueryProvider Provider { get; }
}
你可以看见,IQueryable包含一个类型为Expression的属 性,Expression是Expression<T>的基类。IQueryable的实例被设计成拥有一个相关的表达式树。它是一个等同于 查询表达式中的可执行代码的数据结构。
花点时间考虑图3。你可能需要点击它使图片原尺寸显示。这是本节开始的查询表达式的 表达式树的可视化显示。此图使用ExpressionTreeVisualizer创建,就像我使用它在图1创建基础的 lambda表达式树一样。
LinqToSqlExpressionTree01
图3:此复杂的表达式树由上面的样例LINQ to SQL查询表达式生成。(点击图片看查看大图)

为什么要将LINQ to SQL查询表达式转换成表达式树呢?

你已经学习了表达式树是一个用来表示可执行代 码的数据结构。但到目前为止我们还没有回答一个核心问题,那就是为什么我们要做这样的转换。这个问题是我们在本文开始时提出来的,现在是时候回答了。

一个LINQ to SQL查询不是在你的C#程序里执行的。相反,它被转换成SQL,通过网络发送,最后在数据库服务器上执行。换句话说,下面的代码实际上从来不会在你的程 序里执行:

var query = from c in db.Customers

where c.City == “Nantes”

select new { c.City, c.CompanyName };

它首先被转换成下面的SQL语句然后在服务器 上执行:

SELECT [t0].[City], [t0].[CompanyName]

FROM [dbo].[Customers] AS [t0]
WHERE [t0].[City] = @p0
从查询表达式的代码转换成SQL查询语句 —-它可以通过字符串形式被发送到其他程序。在这里,这个程序恰好是SQL Server数据库。像这样将数据结构转换到SQL显然比直接从原生IL或可执行代码转换到SQL要容易得多。这有些夸大问题的难度,只要试想转换0和1 的序列到SQL!

现在是时候将你的查询表达式转换成SQL了, 描述查询的表达式树是分解并解析了的,就像我们在上一节分解我们的简单的lambda表达式树一样。当然,解析LINQ to SQL表达式树的算法比我们用的那个要复杂得多,但规则是一样的。一旦解析了表达式树的各部分,那么LINQ开始斟酌以最好的方式生成返回被请求的数据的 SQL语句。

表达式树被创建是为了制造一个像将查询表达式 转换成字符串以传递给其他程序并在那里执行这样的转换任务。就是这么简单。没有巨大奥秘,不需要挥舞魔杖。只是简单的:把代码,转换成数据,然后分析数据 发现其组成部分,最后转换成可以传递到其他程序的字符串。

由于查询来自编译器封装的抽象的数据结构,编 译器可以获取任何它想要的信息。它不要求执行查询要在特定的顺序,或用特定的方式。相反,它可以分析表达式树,寻找你要做的是什么,然后再决定怎么去做。至少在理论上,我们可 以自由的考虑各种因素,比如网络状况,数据库负载,结果集是否有效,等等。在实际中LINQ to SQL不考虑所有这些因素,但它理论上可以自由的做几乎所有想做的事。此外,人们可以通过表达式树将自己编写的代码,分析并转换成跟LINQ to SQL提供的完全不同的东西。

IQueryable<T>和 IEnumerable<T>
正如你可能知道的,LINQ to Objects的查询表达式返回IEnumerable<T>而不是IQueryable<T>。为什么LINQ to Objects使用IEnumerable<T>而LINQ to SQL使用IQueryable<T>?
这里是IEnumerable<T>的定义:
public interface IEnumerable<T> : IEnumerable
{
IEnumerator<T> GetEnumerator();
}
正如你看到的,IEnumerable<T>并不包含类型为 Expression的属性。这指出LINQ to Objects和LINQ to SQL的根本区别。后者大量使用了表达式树,但LINQ to Objects很少使用。
为什么表达式树不是LINQ to Objects的标准部分?虽然答案不一定会马上出现,但这是很有意义的一旦你发现这个问题。
考虑这个简单LINQ to Objects查询表达式:
List<int> list = new List<int>() { 1, 2, 3 };
var query = from number in list
where number < 3
select number;
这个LINQ查询返回在我们的list中比3小的数字;就是说,这里返回数字1和 2。显然没有必要将查询转换成字符串来顺序传递给其他程序并获取正确的结果。相反,可以直接转换查询表达式为可执行的.NET代码。这里并不需要将它转换 成字符串或对它执行任何其他复杂操作。
可是这有点理论化,在实际中某些特殊情况下其分隔线可能有些模糊,总体上讲规则相当 简单:
  • 如果代码可以在程序里执行那么可以使用名为 IEnumerable<T>的简单类型完成任务
  • 如果你需要将查询表达式转换成将传递到其他程序的字符串,那么应该使用 IQueryable<T>和表达式树。
LINQ to Amazon这样的项目需要将查询表达式转换成web service调用执行外部程序,通常使用IQueryable<T>和表达式树。LINQ to Amazon将它的查询表达式转换成数据,通过web service传递给另一个甚至不是C#写的程序。将C#代码转换成到某些能传递到web service的东西时,表达式树内在的抽象是非常有用的。要在程序内执行的代码,仍然可以经常使用而抛开表达式树。例如下面的查询使用 IEnumerable<T>,因为它调用到当前程序的.NET反射API:

var query = from method in typeof(System.Linq.Enumerable).GetMethods()

orderby method.Name

group method by method.Name into g

select new { Name = g.Key, Overloads = g.Count() };

概要

本文覆盖了表达式树的一些基本情况。通过将代 码转换成数据,这些数据结构揭示并描绘表达式的组成部分。从最小的概念上讲,理解表达式树是相当简单的。它获取可执行表达式并获取其组成部分放入树形数据 结构。例如,我们检测这个简单的表达式:

(a,b) => a + b;

通过研究来源于这个表达式的树,你能看到创建 树的基本规则,见图1。

你同样可以看到表达式树在LINQ to SQL里扮演非常重要的角色。尤其,他们是LINQ to SQL查询表达式用来获取逻辑的数据抽象。解析并分析此数据得到SQL语句,然后发送到服务器。

LINQ使查询C#语言的一个普通类即有类型 检查也有智能感知。其代码是类型检查和智能感知的,必须使用正确的C#语法,它能直接转换到可执行代码,就像任何其他C#代码一样被转换和执行。表达式树 使将可执行代码转换成能传递到服务器的SQL语句相对容易。

查询返回 IEnumerable<T>优于IQueryable<T>表示不使用表达式树。作为一般性规则,可以这么说:LINQ查询在 程序内执行时不需要表达式树,当代码在程序外执行时可以利用表达式树。
Download the source.

还可看看这篇文章:Using the Expression Tree Visualizer.

[转载]Apache中 RewriteCond 规则参数介绍

mikel阅读(1003)

[转载]Apache中 RewriteCond 规则参数介绍 文章分类:服务器设置 52web.com — 为网站开发者加油!.

Apache模块 mod_rewrite 提供了一个基于正则表达式分析器的重写引擎来实时重写URL请求。它支持每个完整规则可以拥有不限数量的子规则以及附加条件规则的灵活而且强大的URL操 作机制。此URL操作可以依赖于各种测试,比如服务器变量、环境变量、HTTP头、时间标记,甚至各种格式的用于匹配URL组成部分的查找数据库。

此模块可以操作URL的所有部分(包括路径信息部分),在服务器级的(httpd.conf)和目录级的(.htaccess)配置都有效,还可以生成最终请求字 符串。此重写操作的结果可以是内部子处理,也可以是外部请求的转向,甚至还可以是内部代理处理。

这里着重介绍一下 RewriteCond 的规则以及参数说明。RewriteCond指令定义了规则生效的条件,即在一个RewriteRule指令之前可以有一个或多个RewriteCond 指令。条件之后的重写规则仅在当前URI与Pattern匹配并且满足此处的条件(TestString能够与CondPattern匹配)时才会起作 用。

【说明 】定义重写发生的条件
【语法】 RewriteCond TestString CondPattern [flags]
【作用域】 server config, virtual host, directory, .htaccess
【覆盖项】 FileInfo
【状态】 扩展(E)
【模块】 mod_rewrite

TestString是一个纯文本的字符串,但是还可以包含下列可扩展的成分:
1、RewriteRule反向引用 ,引用方法是:$N (0 <= N <= 9)引用当前(带有若干RewriteRule指令的)RewriteCond中的与Pattern匹配的分组成分(圆括号!)。
2、RewriteCond反向引用 ,引用方法是:%N (1 <= N <= 9)引用当前若干RewriteCond条件中最后符合的条件中的分组成分(圆括号!)。
3、RewriteMap扩展 ,引用方法是:${mapname:key|default} 细节请参见RewriteMap 指令
4、服务器变量 ,引用方法是:%{NAME_OF_VARIABLE} NAME_OF_VARIABLE可以是下表列出的字符串之一:

HTTP头 连接与请求
HTTP_USER_AGENT
HTTP_REFERER
HTTP_COOKIE
HTTP_FORWARDED
HTTP_HOST
HTTP_PROXY_CONNECTION
HTTP_ACCEPT
REMOTE_ADDR
REMOTE_HOST
REMOTE_PORT
REMOTE_USER
REMOTE_IDENT
REQUEST_METHOD
SCRIPT_FILENAME
PATH_INFO
QUERY_STRING
AUTH_TYPE
服务器自身 日期和时间 其它
DOCUMENT_ROOT
SERVER_ADMIN
SERVER_NAME
SERVER_ADDR
SERVER_PORT
SERVER_PROTOCOL
SERVER_SOFTWARE
TIME_YEAR
TIME_MON
TIME_DAY
TIME_HOUR
TIME_MIN
TIME_SEC
TIME_WDAY
TIME
API_VERSION
THE_REQUEST
REQUEST_URI
REQUEST_FILENAME
IS_SUBREQ
HTTPS

这些变量都对应于类似命名的HTTP MIME头、Apache服务器的C变量、Unix系统中的struct tm字段,其中的大多数在其他的手册或者CGI规范中都有说明。 其中为mod_rewrite所特有的变量如下:

IS_SUBREQ
如果正在处理的请求是一个子请求,它将包含字符串”true”,否则就是”false”。模块为了解析URI中的附加文件,可能会产生子请求。
API_VERSION
这是正在使用中的Apache模块API(服务器和模块之间内部接口)的版本, 其定义位于include/ap_mmn.h中。此模块API版本对应于正在使用的Apache的版本(比如在Apache 1.3.14的发行版中这个值是19990320:10)。 通常,对它感兴趣的是模块的开发者。
THE_REQUEST
这是由浏览器发送的完整的HTTP请求行(比如:”GET /index.html HTTP/1.1″)。它不包含任何浏览器发送的其它头信息。
REQUEST_URI
这是在HTTP请求行中所请求的资源(比如上述例子中的”/index.html”)。
REQUEST_FILENAME
这是与请求相匹配的完整的本地文件系统的文件路径名。
HTTPS
如果连接使用了SSL/TLS,它将包含字符串”on”,否则就是”off”(无论mod_ssl是否已经加载,该变量都可以安全的使用)。

其它注意事项:
1、SCRIPT_FILENAME和REQUEST_FILENAME包含的值是相同的——即Apache服务器内部的request_rec结构中的 filename字段。 第一个就是大家都知道的CGI变量名,而第二个则是REQUEST_URI(request_rec结构中的uri字段)的一个副本。
2、特殊形式:%{ENV:variable} ,其中的variable可以是任意环境变量。它是通过查找Apache内部结构或者(如果没找到的话)由Apache服务器进程通过getenv()得 到的。
3、特殊形式:%{SSL:variable} ,其中的variable可以是一个SSL环境变量的名字,无论mod_ssl模块是否已经加载都可以使用(未加载时为空字符串)。比如:% {SSL:SSL_CIPHER_USEKEYSIZE}将会被替换为128。
4、特殊形式:%{HTTP:header} ,其中的header可以是任意HTTP MIME头的名称。它总是可以通过查找HTTP请求而得到。比如:%{HTTP:Proxy-Connection}将被替换为Proxy- Connection:HTTP头的值。
5、预设形式:%{LA-U:variable} ,variable的最终值在执行一个内部(基于URL的)子请求后确定。 当需要使用一个目前未知但是会在之后的过程中设置的变量的时候,就可以使用这个方法。例如,需要在服务器级配置(httpd.conf文件)中根据 REMOTE_USER变量进行重写, 就必须使用%{LA-U:REMOTE_USER}。因为此变量是由URL重写(mod_rewrite)步骤之后的认证步骤设置的。 但是另一方面,因为mod_rewrite是通过API修正步骤来实现目录级(.htaccess文件)配置的, 而认证步骤先于API修正步骤,所以可以用%{REMOTE_USER}。
6、预设形式:%{LA-F:variable} ,variable的最终值在执行一个内部(基于文件名的)子请求后确定。 大多数情况下和上述的LA-U是相同的。

CondPattern是条件模式,即一个应用于当前TestString实例的正则表达式。TestString将被首先计算,然后再与 CondPattern匹配。

注意:CondPattern是一个perl兼容的正则表达式,但是还有若干增补:
1、可以在CondPattern串的开头使用’!'(惊叹号)来指定不匹配
2、CondPatterns有若干特殊的变种。除了正则表达式的标准用法,还有下列用法:

<CondPattern‘(词典顺序的小于)
将CondPattern视为纯字符串,与TestString按词典顺序进行比较。如果TestString小于CondPattern则为真。
>CondPattern‘(词典顺序的大于)
将CondPattern视为纯字符串,与TestString按词典顺序进行比较。如果TestString大于CondPattern则为真。
=CondPattern‘(词典顺序的等于)
将CondPattern视为纯字符串,与TestString按词典顺序进行比较。如果TestString等于CondPattern(两个字符串逐 个字符地完全相等)则为真。如果CondPattern是””(两个双引号),则TestString将与空字符串进行比较。
-d‘(目录)
将TestString视为一个路径名并测试它是否为一个存在的目录。
-f‘(常规文件)
将TestString视为一个路径名并测试它是否为一个存在的常规文件。
-s‘(非空的常规文件)
将TestString视为一个路径名并测试它是否为一个存在的、尺寸大于0的常规文件。
-l‘(符号连接)
将TestString视为一个路径名并测试它是否为一个存在的符号连接。
-x‘(可执行)
将TestString视为一个路径名并测试它是否为一个存在的、具有可执行权限的文件。该权限由操作系统检测。
-F‘(对子请求存在的文件)
检查TestString是否为一个有效的文件,而且可以在服务器当前的访问控制配置下被访问。它使用一个内部子请求来做检查,由于会降低服务器的性能, 所以请谨慎使用!
-U‘(对子请求存在的URL)
检查TestString是否为一个有效的URL,而且可以在服务器当前的访问控制配置下被访问。它使用一个内部子请求来做检查,由于会降低服务器的性 能,所以请谨慎使用!

注意:所有这些测试都可以用惊叹号作前缀(‘!’)以实现测试条件的反转。
3、还可以在CondPattern之后追加特殊的标记[flags]作为 RewriteCond指令的第三个参数。flags是一个以逗号分隔的以下标记的列表:

nocase|NC‘(忽 略大小写)
它使测试忽略大小写,扩展后的TestString和CondPattern中’A-Z’ 和’a-z’是没有区别的。此标记仅用于TestString和CondPattern的比较,而对文件系统和子请求的检查不起作用。
ornext|OR‘(或下一条件)
它以OR方式组合若干规则的条件,而不是隐含的AND。典型的例子如下:

RewriteCond %{REMOTE_HOST} ^host1.* [OR]
RewriteCond %{REMOTE_HOST} ^host2.* [OR]
RewriteCond %{REMOTE_HOST} ^host3.*
RewriteRule … 针对这3个主机的规则集 …如果不用这个标记,你就必须要书写三次条件/规则对。

举例
如果要按请求头中的”User-Agent:”重写一个站点的主页,可以这样写:

RewriteCond  %{HTTP_USER_AGENT}  ^Mozilla.*
RewriteRule  ^/$                 /homepage.max.html  [L]

RewriteCond  %{HTTP_USER_AGENT}  ^Lynx.*
RewriteRule  ^/$                 /homepage.min.html  [L]

RewriteRule  ^/$                 /homepage.std.html  [L]

解释:如果你使用的浏览器识别标志是’Mozilla’,则你将得到内容最大化的主页(含有Frames等等)。如果你使用的是(基于终端的)Lynx, 则你得到的是内容最小化的主页(不含table等等)。如果上述条件都不满足(使用的是其他浏览器),则你得到的是一个标准的主页。

[转载]发布FCKeditor语法高亮插件

mikel阅读(1006)

[转载]发布FCKeditor语法高亮插件 – Kend’s Blog.

语法高亮的插件已经使用一段时间了,感觉没什么大问题。现在也有网友问我什么时候可以下载,今天有点空,就把它打包放上来了。

使用方法:

1、在您的FCKeditor的配置文件中(一般为fckconfig.js或custom.config.js)

配置其中的FCKConfig.ToolbarSets,添加HighLighter。

如:

FCKConfig.ToolbarSets[“review”] = [

[&apos;HighLighter&apos;,&apos;Bold&apos;,&apos;Italic&apos;,&apos;Underline&apos;,&apos;StrikeThrough&apos;,&apos;Link&apos;,&apos;Unlink&apos;,&apos;Image&apos;,&apos;Rule&apos;,&apos;Smiley&apos;,&apos;TextColor&apos;,&apos;BGColor&apos;]

];

当然,你可以放到别的工具栏,不过记得注意大小写。

2、根据你指定的plugin目录,注册plugin

如:

// 代码语法高亮插件

FCKConfig.Plugins.Add( &apos;highlighter&apos;, &apos;zh-cn,en&apos; ) ;

3、OK,你会发现你的FCK工具栏的图片多了一个带有”ab”字母,黄底的图标。你就可以使用语法高亮显示功能了。

点击下载语法高亮的插件

通过

[转载]在Asp.net MVC使用thickbox

mikel阅读(1198)

[转载]在Asp.net MVC使用thickbox实现调用页面的Ajax更新 – 打造可持续发展的事业 – 博客园.

MVC模式中,通常都是Controller处理请求并生成数据,选 择一个合适的View来显示结果给用户。虽然ASP.NET MVC已经有非常丰富的ActionResult来满足不同情况下的需求,但是有了Ajax的利器,我们希望有更流畅的交互方式。我们希望能够在一个界面完成列表的显示,编辑或者新增,并完成 刷新。借助JQuery的一个扩展thickbox我们能够达到这个效果。

本文的灵感来自于一篇博文:MVC AJAX Form with Ajax.BeginForm() and JQuery Thickboxhttp://geekswithblogs.net/michelotti/archive/2009/08/31/mvc-ajax-form-with-ajax.beginform-and-jquery-thickbox.aspx

和该文的差异在于thickbox中提交后,他更新的是thickbox中的内容,而不是加载thickbox的页面(即thickbox后 面的那个页面)。

我演示的场景如下

点击“新建角色”弹出一个新建的角色thickBox输入窗口,填入相 应的信息后,将利用Ajax更新角色列表

假设你已经非常了解ASP.NET MVC了。

这里我们不讨论MVC中的Model层, 假设Model数据可以通过Service层获取。

在这个例子里,我们需要3ViewRoleList.aspxRoleNameList.ascxRoleCreate.ascx。后两个PartialView

RoleController需要处理以下几个请求:

[get] RoleNameList():显示Role列表,返回RoleNameList.ascx这个PartialView.

[get] RoleCreate():显示Create的输入界面RoleCreate.ascx等待用户输入相关的信息。

[post]ReleCreate(Role role):处理Create的提交,并返回RoleNameList.ascx

看看代码
<%@ Page Title=”” Language=”C# MasterPageFile=”~/Views/Shared/Site.Master” Inherits=”System.Web.Mvc.ViewPage %>

<asp:Content ID=”Content1″ ContentPlaceHolderID=”TitleContent” runat=”server”>

RoleList

asp:Content>

<asp:Content ID=”Content2″ ContentPlaceHolderID=”MainContent” runat=”server”>

<div id=”rolenamelist” class=”LeftPanel”>

<% Html.RenderPartial(“RoleNameList”, Model.allRoles); %>

div>

<div class=”RightPanel”>

div>

<div class=”clear”>

asp:Content>

RoleNameList.ascx

<%@ Control Language=”C# Inherits=”System.Web.Mvc.ViewUserControl>” %>

<h2>

系统角色h2>

<ul>

<% foreach (var item in Model)

{ %>

<li>

<%=Html.ActionLink (“RoleEdit”, new { id = item.Id })%>

<%=Html.ActionLink“RoleDetail”, new { id = item.Id })%>

<%=Html.ActionLink (“RoleDelete”, new { id = item.Id })%>

  

<%= Html.Encode(item.name) %>

li>

<% } %>

ul>

<div id=”SamDiv”>

div>

<p>

<%= Html.ActionLink新建角色”, “RoleCreate”, new { height = 200, width = 300, modal = true }, new { @class = “thickbox” })%>

p>

其中RoleList.aspx

其中将最后一个ActionLinkclass设置为thickboxthickbox将 会自动以ajax的方式请求到相应链接的内容并显示在一个类似于弹出对话 框的层里面。

结合参考的文档,我曾经天真地想,是不是在RoleCreate.ascx,将UpdateTargetId置为父窗口的相应DIV ID即可。如是

<%@ Control Language=”C# Inherits=”System.Web.Mvc.ViewUserControl<RealMVC.Data.DO_RoleInfo>” %>

<% using (Ajax.BeginForm(“RoleCreate”,“Role” , new AjaxOptions { HttpMethod = “post”, InsertionMode = InsertionMode.Replace, UpdateTargetId = “rolenamelist” }))

{%>

<fieldset>

<legend>创建新的角色</legend>

<div class=”editor-label”>

<%= Html.LabelFor(model => model.name)%>

</div>

<div class=”editor-field”>

<%= Html.TextBoxFor(model => model.name)%>

<%= Html.ValidationMessageFor(model => model.name)%>

</div>

<div class=”editor-label”>

<%= Html.LabelFor(model => model.comment)%>

</div>

<div class=”editor-field”>

<%= Html.TextBoxFor(model => model.comment)%>

<%= Html.ValidationMessageFor(model => model.comment)%>

</div>

<p>

<input type=”submit” value=”Create” />

</p>

</fieldset>

<% } %>

你可以试一下,这时候会出现问题,thickbox不会关闭,更谈不上更新后面的页面。

如是我试图将AjaxOptions的OnBegin设置为tb_remove试图先关闭thickbox但是产生 错误,无法正确执行AjaxForm产生的代码。究其原因可能是tb_remove返回了false,浏览器终止了后续的请求,于是我修改了 thickbox代码。

右边是我修改之后的代码。

这个时候RoleCreate.ascx代码如下:

<% using (Ajax.BeginForm(“RoleCreate”, “Role”, new AjaxOptions { HttpMethod = “post”, InsertionMode = InsertionMode.Replace, UpdateTargetId = “rolenamelist”, OnBegin = “tb_remove_return_true” }))

再附上RoleController中对这个Ajax请求的处理方法:

[AcceptVerbs(HttpVerbs.Post)]

public ActionResult RoleCreate(DO_RoleInfo role)

{

try

{

// TODO: Add insert logic here

RoleService.Save(role);

if(Request.IsAjaxRequest())

return PartialView(“RoleNameList”,RoleService.ListAllRoles());

else

return RedirectToAction(“RoleList”);

}

catch

{

return View();

}

}

总结一下:

l 先阅读MVC AJAX Form with Ajax.BeginForm() and jQuery Thickboxhttp://geekswithblogs.net/michelotti/archive/2009/08/31/mvc-ajax-form-with-ajax.beginform-and-jquery-thickbox.aspx

l 创建链接ActionLink,将class设置为thickbox,以便在thickbox中显示RoleCreate View。

l CreateView中使用AjaxForm,设置OnBegin调用tb_remove来关闭thickbox

l 关键是重写一个tb_remove使其返回一个true而不是false

[转载]ASP.NET MVC 2 RC 2 发布

mikel阅读(888)

[转载]ASP.NET MVC 2 RC 2 发布 – TNT2(SZW)’s Blog @ .net – 博客园.

下载地址:http://www.microsoft.com/downloads/details.aspx?FamilyID=7aba081a-19b9-44c4-a247-3882c8f749e3&displaylang=en

目前次版本只支持的IDE只有VS2008(.NET3.5),针对VS 2010的RC版本将会在不久后提供下载。这次RC2的升级主要修复了一些bug,添加了一些API以及改进了了一些而方法。

ASP.NET MVC 2 RC 1到RC 2变化主要如下:

1、在RC1中新增的ASP.NET MVC 2 validation(对ViewData中Model数据验证)功能,现在已 经由 model-validation 取代了 input-validation 。也就是说,如果你使用 model binding (将Model属性自动绑定到View中,并自动绑定从View中Post回Controller的值),那么所有的属性都将被验证一遍,而不仅仅是变更 后的数据。

(注:个人认为这在某些情况下反而会带来不便,比如当你只想绑定一个模型中部分属性,以此作为一个暂时的“容器”的时候,可能会使 ModelState.IsValid变成false。不知是否可以有选择性地使用新的特性)

2、新的“强类型HTML(<input>标签)的辅助方法”,现在支持了Lambda表达式,从而可以使你使用到数组(array)或者集 合索引(collection indexes)。例如现在可以这么做:Html.EditorFor(m=>m.Orders[i]) ,这段代码将会生成以序列i为name、具体数组中的值为value的<input>标签。

(注:这一点在DropDownList中将极为有用。)

3、重新调整过的Html.EditorFor() 和 Html.DisplayFor() 扩展方法,在默认情况下将自动对应简单属性。这使得自动构造Form表单变为更加简单了。

(注:终于有点像某些“前辈”MVC的Form了,希望这种改进可以来得更猛烈些)

4、model验证所使用的客户端脚本中的id现在更清晰了。在RC1中,id会像这样:form0_ prefix,现在id改为了以属性名称为前缀,如:unitPrice_validationMessage

(注:这点变化可以使前台开发人员对页面的控制更加完整)

5、Html.ValidationSummary() 方法增加了一个参数,可以控制汇总信息显示的级别,是整个Model级别,或是单个属性级别。

(注:变的更人性化了,有的页面我们只需要让它显示一个tip就可以了,不需要把所有的都重复列出来)

6、AccountController中使用默认的ASP.NET MVC Web Application模板更加整洁了。

7、脚手架(scaffolding)功能现在提供了在Controller中的Delete方法,以及对的View页面(模板)。

8、JQuery 1.4.1被包含到了项目中,取代了原先的1.3.2版本。同步更新的还有用来智能提示的vsdoc文件。

9、其他一些很有意义的改进,比如第2点中所说的“强类型HTML(<input>标签)的辅助方法”现在更快了。

次版本的源代码可以在这里下载到:http://aspnet.codeplex.com/Release/ProjectReleases.aspx?ReleaseId=39978

http://szw.cnblogs.com/
研究、探讨ASP.NET
转载请注明出处和作者,谢谢!

[转载]企业应用下的业务组件开发实践

mikel阅读(1208)

[转载]企业应用下的业务组件开发实践 – Anders小明的Blog – 博客园.

作者:  Anders小明


什么是企业应用下的业务组件

首先,这是一个组件,这 意味着它需要在容器里运行,因此不包括任何中间件服务,同时以一定结构(文件结构或者压缩格式)组成,被容器识别;其次,这是一个业务组件,即提供的是应 用服务,而非技术服务;第三,这是企业应用,在业务上包括功能和服务(Service,当前最时髦的说法,你可以理解为API),技术上(以J2EE来讲)包括:UI资 源(JSFJSPJSCSS等)、应用程序(Java)资源和配置文件、数据库表定义、初始化数据和存储过程。

为什么要企业应用下的业务组件

组件技术从提出到现在已 经有20多年了,为什么要提企业应用业务组件?因为现有的组件技术不支持 企业应用环境下的组件要求,J2EEEJB不支持,.NETDLL也不支持。

如前所述,一个企业应用 通常包括了交互界面、应用代码以及数据库结构,而不论是EJB还是DLL只支持应用代码,都不包括交互界面和数据库结构。

如果说EJB不是,那么J2EEEAR或者WAR是否算 是一个组件?答案也不是,EAR或者WAR部署的是一个企业应用,请注意EJB规范中明确说:The Enterprise JavaBeans architecture is a architecture for the development and deployment of component-based (distributed) business applications(EJB 2.x3.x唯一的区别 是2.xdistributed),它们有自己的应用域,彼此相互隔离(简单的看,它们有各自独立的会话管理)。.NET也是有自己的应用域概念。

更进一步,基于应用的部 署导致了三个隔离问题:交互(界面)隔离程序访问隔离数据隔 离(请注意这三个问题分别对应了企业应用业务组件的三个技术内容)。交互隔离导致了企业用户必须访问不同界面,代码访问隔离导致了点对 点的集成以及诸如性能、事务和异步处理等各种非功能性问题,而数据隔离导致了数据有效性、一致性等等问题。所有这些都进而导致了维护的问题。

为了解决这些问题,大厂 商们都提出了各种解决方案:Portal来解决交互隔离问题,通过ESB来解决代码访问隔离问题,以及通过所谓的信息服务(Information Service)来解决数据隔离问题。

那么OSGi技术或者SCA技 术能否满足要求?答案是目前不能,OSGi最初开发的目的就不是为了企业 应用,只是这几年开始成熟,并向企业应用方向发展。09年推出的企业版 (草案)刚刚提出针对程序访问问题的方案,如远程服务、事务管理等;交互隔离问题上规范并没有提出相应方案,只有EclipseEquinox提 出了界面的扩展点机制,但这也不能解决B/S环境的问题;而数据隔离问题 就没有任何方案。SCA从一开始就是面向企业应用,不过不解决交互隔离和 数据隔离问题。

此外,对于行业ISV来说,除企业用户面临的这些种种问题,还面临着其它问题。企业用户毕竟只是面对自己的需求,行业ISV却面临着多个企业用户的需求,面临定制化带来的维护问题,特别是业务和技术的隔离问题(即如何保持 构建业务组件的所使用技术的平稳升级)。

组件的容器

既然要企业应用下的业务 组件,而现有的组件技术又无法支撑,那么就需要一个新的组件容器了(当然,作为一个普通开发人员,我们无法新建一个公开标准的组件体系,也独立维护一个私 有的)。新的组件容器完全使用现有的中间件技术,并加上一些新的内容,包括如下:

  1. 组 件框架,识别组件,以及组件(文件)结构和各个技术工件。
  2. 技术框架,提供业务无关的技术支持,以便于技术的平稳升级切换。
  3. 运行容器,采用现有中间件技术,包括Tomcat、应用服务器和数据库服务等;
  4. 工具,包括打包以及部署工具等。

关于数据隔离问题,在EIP中提到了各种解决方案,这里采用的共享数据库方式,即各个组件都共用一个数据库,各个组件只提供数 据库定义和初时数据(如同EJB/OSGi一样,运行时环境由容器提 供)。

组件的关系

组件的关系分为两种:依 赖和联动。依赖关系在已有的组件技术上已经广为认知,而联动则是新创造的(肯定不是第一个创建的,只不过不同人有不同的叫法)。

联动和依赖的区别是:如 果有组件B和组件A联动,则组件B可以 在没有组件A的情况下运行,并提供相应功能。

针对三种不同技术工件 (即三个隔离问题)呈现不同特点,如下:

1. UI资源(交互隔 离问题),依赖是指UI资源的嵌入、引用和替换,联动是指UI资源的新增。

2. 应用程序(程序访 问隔离),依赖是指API/模型依赖,联动是指消息(传统消息和JMS消息)以及SPI实 现。其中,无论是依赖或者联动都涉及到相应的非功能性需求,包括:异步、事务控制和服务时限等。

3. 数据库资源(数据 隔离),依赖是指外键关联和级联操作,无明显的联动关系。

这里,需要关注应用程序 的依赖和联动

1. SPIAPI存在业务不匹配问题。

虽然组件A依赖组件B,但是 不代表组件B提供的服务完全匹配组件A的要求。有时组件A所 需要的数据需要组件B的多个API组成,为了开发方便或者组件A所需要的性能问题,可能会在组件B新写一个接口给组件A使用,注意该接口不是组件BAPI,该接口仅适用于组件A

2. 尽可能的使用SPI集成方式

SPI集成方式是相对于API集成方式,API集 成方式就是,组件B直接调用其它组件的API及其模型。SPI集 成方式(类似于依赖倒置),组件B定义其所需要的接口及其模型,由组件A或者胶水层代码来实现。

这个点对于行业ISV尤为明显。对于企业用户来说,依赖是明确的,组件A依赖/联动于组件B,但对于行业ISV, 则面临着定制化问题,虽然组件A依赖/联动于组件B,但是 在某个定制化项目中,由于客户已有系统C,而需要组件A依赖/联动于客户 已有系统C。此时采用SPI方式。

在开源世界里采用SPI方式更是广泛。很多框架为了兼容(同一功能)不同实现的类库,都是先定义框架所需接口,并同时提供 不同类库的胶水代码。

不论是EJB/OSGi/SCA都没有对SPI集 成方式的支持。

3. 依赖和联动的非功 能性需求。

事实上,非功能性需求都 是在集成时才存在的。以事务管理为例,除了及其少数的例子外,大部分事务只能在处理流程才被决定(注意,EJB在这方面着是定义在API上的,这样的设计是不适应需求的),而组件AAPI在用例1中需要被 异步调用,而在用例2中需要被同步调用是常见的。即便是OSGi规范,也在这方面没有任何处理。

组件的定制化

定制化问题只针对于行业ISV有效,对于企业用户来说,除非是那种跨国企业在面临不同国度的业务模式、法律监管和会计制度等差 异,存在定制化需要,即使如此,ISV和企业用户对于同一问题的解决方式 也是不同的。

既然我们已经将原有的应 用采用组件化方式开发,那么应用的定制化问题就转化为组件的定制化问题。同样,应用的定制化手段也就转化为组件的定制化手段。

组件框架

罗罗嗦嗦的说了半天,有人就说了:这不就是把UIJava和数据 库三个东东一打包,然后说这就是一个企业应用下的业务组件,有啥新意呢,不就是模块化开发嘛,一直一来大家都是就是这么搞的嘛,何必搞个怪名词来忽悠。

是的,就是把UIJava和数据库 三个东东整合在一起,组件容器说提到的技术框架很多的开发队伍都有一套,运行容器更是有无数开源商业的,打包部署工具更是写了无数。

这确确实实就是我们常说 的模块化开发。但是模块化开发不同于组件开发,模块化开发只是在逻辑上做了切分,物理上(开发出的系统代码)通常并没有真正意义上的隔离,一切都只是在文 档中。

我们需要一点干货,只有 实实在在的组件框架才能组件化开发真正落地的(如同OSGi框架那样): 我们需要一个类似于Equinox的界面扩展框架来支持UI资源的依赖和联动;我们需要一个集成框架来支持应用程序的依赖和联动,解决所面临的种种问题(业务 不匹配、SPI集成以及各种非功能性需求);我们需要一个打包部署工具 (类似Spring DM)提供部署UI资源、应用程序和数据库定义资源(Spring DM提供了基于Web资 源的部署能力)。

其它问题

对于采用J2EEB/S环境的组 件应用还面临一个问题,即现有Servlet规范只允许一个web.xml,不支持组件各自定义私有的FilterServlet, 不过这个问题不是很严重,在现有技术框架已经支持一份简单的web.xml, 而新的Servlet规范已经允许多个web.xml

分布式部署以及集群部署 问题,这其实不是个问题。基于应用的我们有很多手段和技术,那么基于组件的也一样有办法。

[转载]ASP.NET的SEO:目录

mikel阅读(1149)

[转载]ASP.NET的SEO:目录 – 自由飞 – 博客园.

ASP.NET 的SEO:基础知识

ASP.NET 的SEO:Global.asax和HttpModule中的RewritePath()方法——友好的URL

ASP.NETSEO:正则表达式

ASP.NET 的SEO:服务器控件背后——SEO友好的Html和JavaScript

ASP.NET的SEO:使用.ashx文件——排除重复内容

ASP.NET 的SEO:HTTP报头状态码—内容重定向

ASP.NET 的SEO:Linq to XML—网站地图和RSS Feed

ASP.NETSEO:SEO Hack

这 个系列可以算是我的一个读书笔记—WROX红皮书系列之《搜索引擎优化高级编程》(Professional Search Engine Optimization with ASP.NET:A Developer’s Guide to SEO)。我觉得蛮不错的,第一是比较系统和权威;第二是不同于一般的SEO的理论介绍,它着重于asp.net技术的实现!推荐一下。另外,因为是老外 的书,所以所谓的搜索引擎,其实没有包括百度,主要针对的是Google和Yahoo。但博客中也有很多知识只是我自己的理解,欢迎大家指正讨论。

SEO, 搜索引擎优化,简单的理解,就是一种让网站能尽可能的被搜索引擎收录而且排名靠前的技术。可能很多程序员并不是很看重或者了解,但对于无数的草根站长(包 括垃圾站长,呵呵)和众多希望进行网络营销的中小企业而言,SEO是简直是一个入门的基本功。但关于SEO的基础知识,我也就点到即止,因为类似的文章网 上太多了。而且是在博客园里面,我会把重点放在技术层面上。

所涉及的技术其实比较“底层”,对于直接学习 ASP.NET2.0甚至是3.5的同学来说,应该还是很有帮助的,如:
1. 应用程序生命周期事件,如Global.asax和HttpModule;

3. 正则表达式;(以上都关系到URLRewrite)

4. Http报头信息中的状态码:404、301、500等;(主要用于站点重定向)

5. XML文件生成;(关系到网站地图,RSS)

6. HttpHandler;(动态的生产验证码、Robert.txt文件,用于排除重复内容)