[C#]BlogEngine.Net架构与源代码分析系列part1:开篇介绍

mikel阅读(1025)

  最近我要开始这个系列了,这是我的第一个系列。关于BlogEngine.Net我想说的是,它设计的真的很棒,代码简洁但是功能很多,真是麻雀虽小,五 脏俱全啊,而且具有了很多Web2.0的特征,甚至它的每一行代码都值得我们去研究一下,它的开发团队很棒。实际上很多国外的个人Blog都是采用 BlogEngine.Net加上自定义皮肤实现的,如果您是一个Blog的开发者,这更是您的必备参考!

  很多兄弟都推荐BlogEngine.Net看一看,甚至www.ASP.NET也 把它放在了很重要的位置。前一阶段我仔细的把它的源代码阅读了一遍,看完以后兴奋的很冲动,心想居然还有这么好的玩意,这个开源项目设计的真的不错。实际 上前不久的一个Podcast项目我就是采用了类似BlogEngine.Net这种架构开发的,感觉还是很不错的。最近利用空闲的时间做了一下总结,准 备写一个关于BlogEngine.Net系列文章,其实我早就想写一个系列文章,但是一直没有好的想法,对于我比较熟悉的方面发现园子里的兄弟都给写完 了,但是关于BlogEngine.Net的文章似乎很少,所以我决定写这个系列,还希望园子中的兄弟们多支持一下啊。

  开篇声明

  本系列文章介绍的是BlogEngine.Net 1.4.5版本,这是官方前不久发布的一个版本。大家可以在http://www.codeplex.com/blogengine 下载最新的源代码和相应的说明文档。此外,您还可以在它的官方站点http://www.dotnetblogengine.net/上了解更多的安装和一些扩展开发等问题,还可以下载一些开发者已经做好的皮肤。讲解代码使用C#,基于.Net Framework2.0。

  BlogEngine.Net简介

  BlogEngine.NET是一个开源的.NET博客项目。整个项目采用C#开发,它的结构比较简单,但是扩展起来很容易,它的复杂程度较低,易于定制。扩展特性主要体现在以下三个方面:
    1.Widget小工具
    2.Extension扩展功能
    3.自定义个性化Theme

  最初它是一个单人博客,很容易将它实现成多人博客。codeplex上有一个案例就是基于BlogEngine.Net的多人博客。BlogEngine.Net的主要特性:
    1.很容易被安装,只要把文件上传到Web服务器就可以运行。因为它默认采用XML存储数据。
    2.具有很多Blog的新特性并提供了开放接口。例如Ajax评论,支持TrackBack等。
    3.具有很多Web2.0特性,例如OpenSearch, XFN tags, tag cloud等。
    4.自定义主题,您可以自己开发很多主题,类似博客园的主题。
    5.可以配置自己的数据源,例如XML,SQL Server,SQLite等。

  这个系列文章我将从BlogEngine.Net的架构入手,对于每个相对独立的部分进行一下代码分析并作出相应的总结,对于一些部分我会给出我个人的评价,对于一些比较好的细节部分我也会深入探讨。如果大家有一些反馈我还会及时调整。

  暂定目录

  下面是我初步定出的一个目录结构,也反映了系列文章的主要路线,请大家参考,这个目录可能在写的过程中会随时进行调整并加入已经完成文章的链接:

  1.BlogEngine.Net架构与源代码分析系列part1:开篇介绍

  2.BlogEngine.Net架构与源代码分析系列part2:业务对象–共同的父类BusinessBase

  3.BlogEngine.Net架构与源代码分析系列part3:数据存储–基于Provider模式的实现

  4.BlogEngine.Net架构与源代码分析系列part4:Blog全局设置–BlogSettings

  5.BlogEngine.Net架构与源代码分析系列part5:对象搜索–IPublishable与Search

  6.BlogEngine.Net架构与源代码分析系列part6:开放API–MetaWeblog与BlogImporter

  7.BlogEngine.Net架构与源代码分析系列part7:Web2.0特性–Pingback&Trackback

  8.BlogEngine.Net架构与源代码分析系列part8:开发扩展(上)–Extension与管理上的实现

  9.BlogEngine.Net架构与源代码分析系列part9:开发扩展(中)–Widget小工具

  10.BlogEngine.Net架构与源代码分析系列part10:开发扩展(下)–自定义Theme

  11.BlogEngine.Net架构与源代码分析系列part11:页面共同的基类–BlogBasePage

  12.BlogEngine.Net架构与源代码分析系列part12:实现分析(上)–HttpHandlers与HttpModules

  13.BlogEngine.Net架构与源代码分析系列part13:实现分析(下)–网站页面上值得参考的部分

  14.BlogEngine.Net架构与源代码分析系列part14:总结篇

  我写这个系列文章的目的有三个。首先将好的东西分享给大家,其次让我更加深入的研究BlogEngine.Net提高自己,最后作为一个备忘录存储在博客园上。

   分享是一种美。

版权声明
作者:Thriving.country
出处:http://thriving-country.cnblogs.com/
本文版权归作者和博客园共同所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接.

[DBMS]Managing Hierarchical Data in MySQL

mikel阅读(798)

Managing Hierarchical Data in MySQL

Introduction

Most users at one time or another have dealt with hierarchical data in a SQL database and no doubt learned that the management of hierarchical data is not what a relational database is intended for. The tables of a relational database are not hierarchical (like XML), but are simply a flat list. Hierarchical data has a parent-child relationship that is not naturally represented in a relational database table.

For our purposes, hierarchical data is a collection of data where each item has a single parent and zero or more children (with the exception of the root item, which has no parent). Hierarchical data can be found in a variety of database applications, including forum and mailing list threads, business organization charts, content management categories, and product categories. For our purposes we will use the following product category hierarchy from an fictional electronics store:

These categories form a hierarchy in much the same way as the other examples cited above. In this article we will examine two models for dealing with hierarchical data in MySQL, starting with the traditional adjacency list model.

The Adjacency List Model

Typically the example categories shown above will be stored in a table like the following (I'm including full Create and Insert statements so you can follow along):

Create TABLE category(
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
parent INT DEFAULT NULL);
Insert INTO category
VALUES(1,'ELECTRONICS',NULL),(2,'TELEVISIONS',1),(3,'TUBE',2),
(4,'LCD',2),(5,'PLASMA',2),(6,'PORTABLE ELECTRONICS',1),
(7,'MP3 PLAYERS',6),(8,'FLASH',7),
(9,'CD PLAYERS',6),(10,'2 WAY RADIOS',6);
Select * FROM category orDER BY category_id;
+-------------+----------------------+--------+
| category_id | name                 | parent |
+-------------+----------------------+--------+
|           1 | ELECTRONICS          |   NULL |
|           2 | TELEVISIONS          |      1 |
|           3 | TUBE                 |      2 |
|           4 | LCD                  |      2 |
|           5 | PLASMA               |      2 |
|           6 | PORTABLE ELECTRONICS |      1 |
|           7 | MP3 PLAYERS          |      6 |
|           8 | FLASH                |      7 |
|           9 | CD PLAYERS           |      6 |
|          10 | 2 WAY RADIOS         |      6 |
+-------------+----------------------+--------+
10 rows in set (0.00 sec)

In the adjacency list model, each item in the table contains a pointer to its parent. The topmost element, in this case electronics, has a NULL value for its parent. The adjacency list model has the advantage of being quite simple, it is easy to see that FLASH is a child of mp3 players, which is a child of portable electronics, which is a child of electronics. While the adjacency list model can be dealt with fairly easily in client-side code, working with the model can be more problematic in pure SQL.

Retrieving a Full Tree

The first common task when dealing with hierarchical data is the display of the entire tree, usually with some form of indentation. The most common way of doing this is in pure SQL is through the use of a self-join:

Select t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
Where t1.name = 'ELECTRONICS';
+-------------+----------------------+--------------+-------+
| lev1        | lev2                 | lev3         | lev4  |
+-------------+----------------------+--------------+-------+
| ELECTRONICS | TELEVISIONS          | TUBE         | NULL  |
| ELECTRONICS | TELEVISIONS          | LCD          | NULL  |
| ELECTRONICS | TELEVISIONS          | PLASMA       | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS  | FLASH |
| ELECTRONICS | PORTABLE ELECTRONICS | CD PLAYERS   | NULL  |
| ELECTRONICS | PORTABLE ELECTRONICS | 2 WAY RADIOS | NULL  |
+-------------+----------------------+--------------+-------+
6 rows in set (0.00 sec)

Finding all the Leaf Nodes

We can find all the leaf nodes in our tree (those with no children) by using a LEFT JOIN query:

Select t1.name FROM
category AS t1 LEFT JOIN category as t2
ON t1.category_id = t2.parent
Where t2.category_id IS NULL;
+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+

Retrieving a Single Path

The self-join also allows us to see the full path through our hierarchies:

Select t1.name AS lev1, t2.name as lev2, t3.name as lev3, t4.name as lev4
FROM category AS t1
LEFT JOIN category AS t2 ON t2.parent = t1.category_id
LEFT JOIN category AS t3 ON t3.parent = t2.category_id
LEFT JOIN category AS t4 ON t4.parent = t3.category_id
Where t1.name = 'ELECTRONICS' AND t4.name = 'FLASH';
+-------------+----------------------+-------------+-------+
| lev1        | lev2                 | lev3        | lev4  |
+-------------+----------------------+-------------+-------+
| ELECTRONICS | PORTABLE ELECTRONICS | MP3 PLAYERS | FLASH |
+-------------+----------------------+-------------+-------+
1 row in set (0.01 sec)

The main limitation of such an approach is that you need one self-join for every level in the hierarchy, and performance will naturally degrade with each level added as the joining grows in complexity.

Limitations of the Adjacency List Model

Working with the adjacency list model in pure SQL can be difficult at best. Before being able to see the full path of a category we have to know the level at which it resides. In addition, special care must be taken when deleting nodes because of the potential for orphaning an entire sub-tree in the process (delete the portable electronics category and all of its children are orphaned). Some of these limitations can be addressed through the use of client-side code or stored procedures. With a procedural language we can start at the bottom of the tree and iterate upwards to return the full tree or a single path. We can also use procedural programming to delete nodes without orphaning entire sub-trees by promoting one child element and re-ordering the remaining children to point to the new parent.

The Nested Set Model

What I would like to focus on in this article is a different approach, commonly referred to as the Nested Set Model. In the Nested Set Model, we can look at our hierarchy in a new way, not as nodes and lines, but as nested containers. Try picturing our electronics categories this way:

Notice how our hierarchy is still maintained, as parent categories envelop their children.We represent this form of hierarchy in a table through the use of left and right values to represent the nesting of our nodes:

Create TABLE nested_category (
category_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL
);
Insert INTO nested_category
VALUES(1,'ELECTRONICS',1,20),(2,'TELEVISIONS',2,9),(3,'TUBE',3,4),
(4,'LCD',5,6),(5,'PLASMA',7,8),(6,'PORTABLE ELECTRONICS',10,19),
(7,'MP3 PLAYERS',11,14),(8,'FLASH',12,13),
(9,'CD PLAYERS',15,16),(10,'2 WAY RADIOS',17,18);
Select * FROM nested_category orDER BY category_id;
+-------------+----------------------+-----+-----+
| category_id | name                 | lft | rgt |
+-------------+----------------------+-----+-----+
|           1 | ELECTRONICS          |   1 |  20 |
|           2 | TELEVISIONS          |   2 |   9 |
|           3 | TUBE                 |   3 |   4 |
|           4 | LCD                  |   5 |   6 |
|           5 | PLASMA               |   7 |   8 |
|           6 | PORTABLE ELECTRONICS |  10 |  19 |
|           7 | MP3 PLAYERS          |  11 |  14 |
|           8 | FLASH                |  12 |  13 |
|           9 | CD PLAYERS           |  15 |  16 |
|          10 | 2 WAY RADIOS         |  17 |  18 |
+-------------+----------------------+-----+-----+

We use lft and rgt because left and right are reserved words in MySQL, see http://dev.mysql.com/doc/mysql/en/reserved-words.html for the full list of reserved words.

So how do we determine left and right values? We start numbering at the leftmost side of the outer node and continue to the right:

This design can be applied to a typical tree as well:

When working with a tree, we work from left to right, one layer at a time, descending to each node's children before assigning a right-hand number and moving on to the right. This approach is called the modified preorder tree traversal algorithm.

Retrieving a Full Tree

We can retrieve the full tree through the use of a self-join that links parents with nodes on the basis that a node's lft value will always appear between its parent's lft and rgt values:

Select node.name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND parent.name = 'ELECTRONICS'
ORDER BY node.lft;
+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| TELEVISIONS          |
| TUBE                 |
| LCD                  |
| PLASMA               |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
| CD PLAYERS           |
| 2 WAY RADIOS         |
+----------------------+

Unlike our previous examples with the adjacency list model, this query will work regardless of the depth of the tree. We do not concern ourselves with the rgt value of the node in our BETWEEN clause because the rgt value will always fall within the same parent as the lft values.

Finding all the Leaf Nodes

Finding all leaf nodes in the nested set model even simpler than the LEFT JOIN method used in the adjacency list model. If you look at the nested_category table, you may notice that the lft and rgt values for leaf nodes are consecutive numbers. To find the leaf nodes, we look for nodes where rgt = lft + 1:

Select name
FROM nested_category
Where rgt = lft + 1;
+--------------+
| name         |
+--------------+
| TUBE         |
| LCD          |
| PLASMA       |
| FLASH        |
| CD PLAYERS   |
| 2 WAY RADIOS |
+--------------+

Retrieving a Single Path

With the nested set model, we can retrieve a single path without having multiple self-joins:

Select parent.name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'FLASH'
ORDER BY parent.lft;
+----------------------+
| name                 |
+----------------------+
| ELECTRONICS          |
| PORTABLE ELECTRONICS |
| MP3 PLAYERS          |
| FLASH                |
+----------------------+

Finding the Depth of the Nodes

We have already looked at how to show the entire tree, but what if we want to also show the depth of each node in the tree, to better identify how each node fits in the hierarchy? This can be done by adding a COUNT function and a GROUP BY clause to our existing query for showing the entire tree:

Select node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| ELECTRONICS          |     0 |
| TELEVISIONS          |     1 |
| TUBE                 |     2 |
| LCD                  |     2 |
| PLASMA               |     2 |
| PORTABLE ELECTRONICS |     1 |
| MP3 PLAYERS          |     2 |
| FLASH                |     3 |
| CD PLAYERS           |     2 |
| 2 WAY RADIOS         |     2 |
+----------------------+-------+

We can use the depth value to indent our category names with the CONCAT and REPEAT string functions:

Select CONCAT( REPEAT(' ', COUNT(parent.name) - 1), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+

Of course, in a client-side application you will be more likely to use the depth value directly to display your hierarchy. Web developers could loop through the tree, adding <li></li> and <ul></ul> tags as the depth number increases and decreases.

Depth of a Sub-Tree

When we need depth information for a sub-tree, we cannot limit either the node or parent tables in our self-join because it will corrupt our results. Instead, we add a third self-join, along with a sub-query to determine the depth that will be the new starting point for our sub-tree:

Select node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
Select node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
orDER BY node.lft
)AS sub_tree
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| FLASH                |     2 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+

This function can be used with any node name, including the root node. The depth values are always relative to the named node.

Find the Immediate Subordinates of a Node

Imagine you are showing a category of electronics products on a retailer web site. When a user clicks on a category, you would want to show the products of that category, as well as list its immediate sub-categories, but not the entire tree of categories beneath it. For this, we need to show the node and its immediate sub-nodes, but no further down the tree. For example, when showing the PORTABLE ELECTRONICS category, we will want to show MP3 PLAYERS, CD PLAYERS, and 2 WAY RADIOS, but not FLASH.

This can be easily accomplished by adding a HAVING clause to our previous query:

Select node.name, (COUNT(parent.name) - (sub_tree.depth + 1)) AS depth
FROM nested_category AS node,
nested_category AS parent,
nested_category AS sub_parent,
(
Select node.name, (COUNT(parent.name) - 1) AS depth
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.name = 'PORTABLE ELECTRONICS'
GROUP BY node.name
orDER BY node.lft
)AS sub_tree
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
AND sub_parent.name = sub_tree.name
GROUP BY node.name
HAVING depth <= 1
ORDER BY node.lft;
+----------------------+-------+
| name                 | depth |
+----------------------+-------+
| PORTABLE ELECTRONICS |     0 |
| MP3 PLAYERS          |     1 |
| CD PLAYERS           |     1 |
| 2 WAY RADIOS         |     1 |
+----------------------+-------+

If you do not wish to show the parent node, change the HAVING depth <= 1 line to HAVING depth = 1.

Aggregate Functions in a Nested Set

Let's add a table of products that we can use to demonstrate aggregate functions with:

Create TABLE product(
product_id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(40),
category_id INT NOT NULL
);
Insert INTO product(name, category_id) VALUES('20" TV',3),('36" TV',3),
('Super-LCD 42"',4),('Ultra-Plasma 62"',5),('Value Plasma 38"',5),
('Power-MP3 5gb',7),('Super-Player 1gb',8),('Porta CD',9),('CD To go!',9),
('Family Talk 360',10);
Select * FROM product;
+------------+-------------------+-------------+
| product_id | name              | category_id |
+------------+-------------------+-------------+
|          1 | 20" TV            |           3 |
|          2 | 36" TV            |           3 |
|          3 | Super-LCD 42"     |           4 |
|          4 | Ultra-Plasma 62"  |           5 |
|          5 | Value Plasma 38"  |           5 |
|          6 | Power-MP3 128mb   |           7 |
|          7 | Super-Shuffle 1gb |           8 |
|          8 | Porta CD          |           9 |
|          9 | CD To go!         |           9 |
|         10 | Family Talk 360   |          10 |
+------------+-------------------+-------------+

Now let's produce a query that can retrieve our category tree, along with a product count for each category:

Select parent.name, COUNT(product.name)
FROM nested_category AS node ,
nested_category AS parent,
product
Where node.lft BETWEEN parent.lft AND parent.rgt
AND node.category_id = product.category_id
GROUP BY parent.name
ORDER BY node.lft;
+----------------------+---------------------+
| name                 | COUNT(product.name) |
+----------------------+---------------------+
| ELECTRONICS          |                  10 |
| TELEVISIONS          |                   5 |
| TUBE                 |                   2 |
| LCD                  |                   1 |
| PLASMA               |                   2 |
| PORTABLE ELECTRONICS |                   5 |
| MP3 PLAYERS          |                   2 |
| FLASH                |                   1 |
| CD PLAYERS           |                   2 |
| 2 WAY RADIOS         |                   1 |
+----------------------+---------------------+

This is our typical whole tree query with a COUNT and GROUP BY added, along with a reference to the product table and a join between the node and product table in the Where clause. As you can see, there is a count for each category and the count of subcategories is reflected in the parent categories.

Adding New Nodes

Now that we have learned how to query our tree, we should take a look at how to update our tree by adding a new node. Let's look at our nested set diagram again:

If we wanted to add a new node between the TELEVISIONS and PORTABLE ELECTRONICS nodes, the new node would have lft and rgt values of 10 and 11, and all nodes to its right would have their lft and rgt values increased by two. We would then add the new node with the appropriate lft and rgt values. While this can be done with a stored procedure in MySQL 5, I will assume for the moment that most readers are using 4.1, as it is the latest stable version, and I will isolate my queries with a LOCK TABLES statement instead:

LOCK TABLE nested_category WRITE;
Select @myRight := rgt FROM nested_category
Where name = 'TELEVISIONS';
Update nested_category SET rgt = rgt + 2 Where rgt > @myRight;
Update nested_category SET lft = lft + 2 Where lft > @myRight;
Insert INTO nested_category(name, lft, rgt) VALUES('GAME CONSOLES', @myRight + 1, @myRight + 2);
UNLOCK TABLES;
We can then check our nesting with our indented tree query:
Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
+-----------------------+

If we instead want to add a node as a child of a node that has no existing children, we need to modify our procedure slightly. Let's add a new FRS node below the 2 WAY RADIOS node:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft FROM nested_category
Where name = '2 WAY RADIOS';
Update nested_category SET rgt = rgt + 2 Where rgt > @myLeft;
Update nested_category SET lft = lft + 2 Where lft > @myLeft;
Insert INTO nested_category(name, lft, rgt) VALUES('FRS', @myLeft + 1, @myLeft + 2);
UNLOCK TABLES;

In this example we expand everything to the right of the left-hand number of our proud new parent node, then place the node to the right of the left-hand value. As you can see, our new node is now properly nested:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  GAME CONSOLES        |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

Deleting Nodes

The last basic task involved in working with nested sets is the removal of nodes. The course of action you take when deleting a node depends on the node's position in the hierarchy; deleting leaf nodes is easier than deleting nodes with children because we have to handle the orphaned nodes.

When deleting a leaf node, the process if just the opposite of adding a new node, we delete the node and its width from every node to its right:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
Where name = 'GAME CONSOLES';
Delete FROM nested_category Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt - @myWidth Where rgt > @myRight;
Update nested_category SET lft = lft - @myWidth Where lft > @myRight;
UNLOCK TABLES;

And once again, we execute our indented tree query to confirm that our node has been deleted without corrupting the hierarchy:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   MP3 PLAYERS         |
|    FLASH              |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

This approach works equally well to delete a node and all its children:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
Where name = 'MP3 PLAYERS';
Delete FROM nested_category Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt - @myWidth Where rgt > @myRight;
Update nested_category SET lft = lft - @myWidth Where lft > @myRight;
UNLOCK TABLES;

And once again, we query to see that we have successfully deleted an entire sub-tree:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+-----------------------+
| name                  |
+-----------------------+
| ELECTRONICS           |
|  TELEVISIONS          |
|   TUBE                |
|   LCD                 |
|   PLASMA              |
|  PORTABLE ELECTRONICS |
|   CD PLAYERS          |
|   2 WAY RADIOS        |
|    FRS                |
+-----------------------+

The other scenario we have to deal with is the deletion of a parent node but not the children. In some cases you may wish to just change the name to a placeholder until a replacement is presented, such as when a supervisor is fired. In other cases, the child nodes should all be moved up to the level of the deleted parent:

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt - lft + 1
FROM nested_category
Where name = 'PORTABLE ELECTRONICS';
Delete FROM nested_category Where lft = @myLeft;
Update nested_category SET rgt = rgt - 1, lft = lft - 1 Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt - 2 Where rgt > @myRight;
Update nested_category SET lft = lft - 2 Where lft > @myRight;
UNLOCK TABLES;

In this case we subtract two from all elements to the right of the node (since without children it would have a width of two), and one from the nodes that are its children (to close the gap created by the loss of the parent's left value). Once again, we can confirm our elements have been promoted:

Select CONCAT( REPEAT( ' ', (COUNT(parent.name) - 1) ), node.name) AS name
FROM nested_category AS node,
nested_category AS parent
Where node.lft BETWEEN parent.lft AND parent.rgt
GROUP BY node.name
ORDER BY node.lft;
+---------------+
| name          |
+---------------+
| ELECTRONICS   |
|  TELEVISIONS  |
|   TUBE        |
|   LCD         |
|   PLASMA      |
|  CD PLAYERS   |
|  2 WAY RADIOS |
|   FRS         |
+---------------+

Other scenarios when deleting nodes would include promoting one of the children to the parent position and moving the child nodes under a sibling of the parent node, but for the sake of space these scenarios will not be covered in this article.

Final Thoughts

While I hope the information within this article will be of use to you, the concept of nested sets in SQL has been around for over a decade, and there is a lot of additional information available in books and on the Internet. In my opinion the most comprehensive source of information on managing hierarchical information is a book called Joe Celko's Trees and Hierarchies in SQL for Smarties, written by a very respected author in the field of advanced SQL, Joe Celko. Joe Celko is often credited with the nested sets model and is by far the most prolific author on the subject. I have found Celko's book to be an invaluable resource in my own studies and highly recommend it. The book covers advanced topics which I have not covered in this article, and provides additional methods for managing hierarchical data in addition to the Adjacency List and Nested Set models.

In the References / Resources section that follows I have listed some web resources that may be of use in your research of managing hierarchal data, including a pair of PHP related resources that include pre-built PHP libraries for handling nested sets in MySQL. Those of you who currently use the adjacency list model and would like to experiment with the nested set model will find sample code for converting between the two in the Storing Hierarchical Data in a Database resource listed below.

[DBMS]一种理想的在关系数据库中存储树型结构数据的方法

mikel阅读(1029)

在各种基于关系数据库的应用系统开发中,我们往往需要存储树型结构的数据,目前有很多流行的方法,如邻接列表模型(The Adjacency List Model),在此基础上也有很多人针对不同的需求做了相应的改进,但总是在某些方面存在的各种各样的缺陷。
    那么理想中的树型结构应具备哪些特点呢?数据存储冗余小、直观性强;方便返回整个树型结构数据;可以很轻松的返回某一子树(方便分层加载);快整获以某节 点的祖谱路径;插入、删除、移动节点效率高等等。带着这些需求我查找了很多资料,发现了一种理想的树型结构数据存储及操作算法,改进的前序遍历树模型 (The Nested Set Model)。

一、数据

    在本文中,举一个在线食品店树形图的例子。这个食品店通过类别、颜色和品种来组织食品。树形图如下:

二、邻接列表模型(The Adjacency List Model)

在这种模型下,上述数据在关系数据库的表结构数据通常如下图所示:

由于该模型比较简单,在此不再详细介绍其算法,下面列出它的一些不足:

    在大多数编程语言中,他运行很慢,效率很差。这主要是“递归”造成的。我们每次查询节点都要访问数据库。每次数据库查询都要花费一些时间,这让函数处理庞 大的树时会十分慢。造成这个函数不是太快的第二个原因可能是你使用的语言。不像Lisp这类语言,大多数语言不是针对递归函数设计的。对于每个节点造成这 个函数不是太快的第二个原因可能是你使用的语言。不像Lisp这类语言,大多数语言不是针对递归函数设计的。对于每个节点,函数都要调用他自己,产生新的 实例。这样,对于一个4层的树,你可能同时要运行4个函数副本。对于每个函数都要占用一块内存并且需要一定的时间初始化,这样处理大树时递归就很慢了。

三、改进的前序遍历树模型(The Nested Set Model)

原理:

    我们先把树按照水平方式摆开。从根节点开始(“Food”),然后他的左边写上1。然后按照树的顺序(从上到下)给“Fruit”的左边写上2。这样,你 沿着树的边界走啊走(这就是“遍历”),然后同时在每个节点的左边和右边写上数字。最后,我们回到了根节点“Food”在右边写上18。下面是标上了数字 的树,同时把遍历的顺序用箭头标出来了。

 

    我们称这些数字为左值和右值(如,“Food”的左值是1,右值是18)。正如你所见,这些数字按时了每个节点之间的关系。因为“Red”有3和6两个 值,所以,它是有拥有1-18值的“Food”节点的后续。同样的,我们可以推断所有左值大于2并且右值小于11的节点,都是有2-11的“Fruit” 节点的后续。这样,树的结构就通过左值和右值储存下来了。这种数遍整棵树算节点的方法叫做“改进前序遍历树”算法。

表结构设计:

常用的操作:

下面列出一些常用操作的SQL语句

返回完整的树(Retrieving a Full Tree)

Select node.name
  
FROM nested_category node, nested_category parent
 
Where node.lft BETWEEN parent.lft AND parent.rgt
   
AND parent.name = 'electronics'
 
ORDER BY node.lft

返回某结点的子树(Find the Immediate Subordinates of a Node)

Select V.*
  
FROM (Select node.name,
               (
COUNT(parent.name)  (AVG(sub_tree.depth) + 1)) depth
          
FROM nested_category node,
               nested_category parent,
               nested_category sub_parent,
               (
Select V.*
                  
FROM (Select node.name, (COUNT(parent.name)  1) depth
                          
FROM nested_category node, nested_category parent
                         
Where node.lft BETWEEN parent.lft AND parent.rgt
                           
AND node.name = 'portable electronics'
                         
GROUP BY node.name) V,
                       nested_category T
                 
Where V.name = T.name
                 
ORDER BY T.lft) sub_tree
         
Where node.lft BETWEEN parent.lft AND parent.rgt
           
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
           
AND sub_parent.name = sub_tree.name
         
GROUP BY node.name) V,
       nested_category T
 
Where V.name = T.name
   
and V.depth <= 1
   
and V.depth > 0
 
ORDER BY T.Lft

返回某结点的祖谱路径(Retrieving a Single Path)

Select parent.name
  
FROM nested_category node, nested_category parent
 
Where node.lft BETWEEN parent.lft AND parent.rgt
   
AND node.name = 'flash'
 
ORDER BY node.lft

返回所有节点的深度(Finding the Depth of the Nodes)

Select V.*
  
FROM (Select node.name, (COUNT(parent.name)  1) depth
          
FROM nested_category node, nested_category parent
         
Where node.lft BETWEEN parent.lft AND parent.rgt
         
GROUP BY node.name) V,
       nested_category T
 
Where V.name = T.name
 
ORDER BY T.Lft

返回子树的深度(Depth of a Sub-Tree)

Select V.*
  
FROM (Select node.name,
               (
COUNT(parent.name)  (AVG(sub_tree.depth) + 1)) depth
          
FROM nested_category node,
               nested_category parent,
               nested_category sub_parent,
               (
Select V.*
                  
FROM (Select node.name, (COUNT(parent.name)  1) depth
                          
FROM nested_category node, nested_category parent
                         
Where node.lft BETWEEN parent.lft AND parent.rgt
                           
AND node.name = 'portable electronics'
                         
GROUP BY node.name) V,
                       nested_category T
                 
Where V.name = T.name
                 
ORDER BY T.lft) sub_tree
         
Where node.lft BETWEEN parent.lft AND parent.rgt
           
AND node.lft BETWEEN sub_parent.lft AND sub_parent.rgt
           
AND sub_parent.name = sub_tree.name
         
GROUP BY node.name) V,
       nested_category T
 
Where V.name = T.name
 
ORDER BY T.Lft

返回所有的叶子节点(Finding all the Leaf Nodes)

Select name FROM nested_category Where rgt = lft + 1

插入节点(Adding New Nodes)

LOCK TABLE nested_category WRITE;
Select @myRight := rgt FROM nested_category Where name = 'TELEVISIONS';
Update nested_category SET rgt = rgt + 2 Where rgt > @myRight;
Update nested_category SET lft = lft + 2 Where lft > @myRight;
Insert INTO nested_category
  (name, lft, rgt)
VALUES
  (
'GAME CONSOLES'@myRight + 1@myRight + 2);
UNLOCK TABLES;

删除节点(Deleting Nodes)

LOCK TABLE nested_category WRITE;
Select @myLeft := lft, @myRight := rgt, @myWidth := rgt  lft + 1
  
FROM nested_category
 
Where name = 'GAME CONSOLES';
Delete FROM nested_category Where lft BETWEEN @myLeft AND @myRight;
Update nested_category SET rgt = rgt  @myWidth Where rgt > @myRight;
Update nested_category SET lft = lft  @myWidth Where lft > @myRight;
UNLOCK TABLES;

 


[参考资料]

http://dev.mysql.com/tech-resources/articles/hierarchical-data.html

http://www.nirvanastudio.org/php/hierarchical-data-database.html

[正则表达式]非常经典的正则表达式

mikel阅读(676)

前言:
       半年前我对正则表达式产生了兴趣,在网上查找过不少资料,看过不少的教程,最后在使用一个正则表达式工具RegexBuddy时发现他的教程写的非常好,可以说是我目前见过最好的正则表达式教程。于是一直想把他翻译过来。这个愿望直到这个五一长假才得以实现,结果就有了这篇文章。关于本文的名字,使用深入浅出似乎已经太俗。但是通读原文以后,觉得只有用深入浅出才能准确的表达出该教程给我的感受,所以也就不能免俗了。
       本文是Jan GoyvaertsRegexBuddy写的教程的译文,版权归原作者所有,欢迎转载。但是为了尊重原作者和译者的劳动,请注明出处!谢谢!

 

1.      什么是正则表达式

基本说来,正则表达式是一种用来描述一定数量文本的模式。Regex代表Regular Express。本文将用<<regex>>来表示一段具体的正则表达式。

一段文本就是最基本的模式,简单的匹配相同的文本。

 

2.      不同的正则表达式引擎

正则表达式引擎是一种可以处理正则表达式的软件。通常,引擎是更大的应用程序的一部分。在软件世界,不同的正则表达式并不互相兼容。本教程会集中讨论Perl 5 类型的引擎,因为这种引擎是应用最广泛的引擎。同时我们也会提到一些和其他引擎的区别。许多近代的引擎都很类似,但不完全一样。例如.NET正则库,JDK正则包。

 

3.      文字符号

最基本的正则表达式由单个文字符号组成。如<<a>>,它将匹配字符串中第一次出现的字符“a”。如对字符串“Jack is a boy”。“J”后的“a”将被匹配。而第二个“a”将不会被匹配。

正则表达式也可以匹配第二个“a”,这必须是你告诉正则表达式引擎从第一次匹配的地方开始搜索。在文本编辑器中,你可以使用“查找下一个”。在编程语言中,会有一个函数可以使你从前一次匹配的位置开始继续向后搜索。

类似的,<<cat>>会匹配“About cats and dogs”中的“cat”。这等于是告诉正则表达式引擎,找到一个<<c>>,紧跟一个<<a>>,再跟一个<<t>>

要注意,正则表达式引擎缺省是大小写敏感的。除非你告诉引擎忽略大小写,否则<<cat>>不会匹配“Cat”。

 

·        特殊字符

对于文字字符,有11个字符被保留作特殊用途。他们是:

[ ] " ^ $ . | ? * + ( )

这些特殊字符也被称作元字符。

如果你想在正则表达式中将这些字符用作文本字符,你需要用反斜杠“"”对其进行换码 (escape)。例如你想匹配“1+1=2”,正确的表达式为<<1"+1=2>>.

需要注意的是,<<1+1=2>>也是有效的正则表达式。但它不会匹配“1+1=2”,而会匹配“123+111=234”中的“111=2”。因为“+”在这里表示特殊含义(重复1次到多次)。

在编程语言中,要注意,一些特殊的字符会先被编译器处理,然后再传递给正则引擎。因此正则表达式<<1"+2=2>>C++中要写成“1""+1=2”。为了匹配“C:"temp”,你要用正则表达式<<C:""temp>>。而在C++中,正则表达式则变成了“C:""""temp”。

 

·        不可显示字符

可以使用特殊字符序列来代表某些不可显示字符:

<<"t>>代表Tab(0x09)

<<"r>>代表回车符(0x0D)

<<"n>>代表换行符(0x0A)

要注意的是Windows中文本文件使用“"r"n”来结束一行而Unix使用“"n”。

 

4.      正则表达式引擎的内部工作机制

知道正则表达式引擎是如何工作的有助于你很快理解为何某个正则表达式不像你期望的那样工作。

有两种类型的引擎:文本导向(text-directed)的引擎和正则导向(regex-directed)的引擎。Jeffrey Friedl把他们称作DFANFA引擎。本文谈到的是正则导向的引擎。这是因为一些非常有用的特性,如“惰性”量词(lazy quantifiers)和反向引用(backreferences),只能在正则导向的引擎中实现。所以毫不意外这种引擎是目前最流行的引擎。

你可以轻易分辨出所使用的引擎是文本导向还是正则导向。如果反向引用或“惰性”量词被实现,则可以肯定你使用的引擎是正则导向的。你可以作如下测试:将正则表达式<<regex|regex not>>应用到字符串“regex not”。如果匹配的结果是regex,则引擎是正则导向的。如果结果是regex not,则是文本导向的。因为正则导向的引擎是“猴急”的,它会很急切的进行表功,报告它找到的第一个匹配

 

·        正则导向的引擎总是返回最左边的匹配

这是需要你理解的很重要的一点:即使以后有可能发现一个“更好”的匹配,正则导向的引擎也总是返回最左边的匹配。

当把<<cat>>应用到“He captured a catfish for his cat”,引擎先比较<<c>>和“H”,结果失败了。于是引擎再比较<<c>>和“e”,也失败了。直到第四个字符,<<c>>匹配了“c”。<<a>>匹配了第五个字符。到第六个字符<<t>>没能匹配“p”,也失败了。引擎再继续从第五个字符重新检查匹配性。直到第十五个字符开始,<<cat>>匹配上了“catfish”中的“cat”,正则表达式引擎急切的返回第一个匹配的结果,而不会再继续查找是否有其他更好的匹配。

 

 

5.      字符集

字符集是由一对方括号“[]”括起来的字符集合。使用字符集,你可以告诉正则表达式引擎仅仅匹配多个字符中的一个。如果你想匹配一个“a”或一个“e”,使用<<[ae]>>。你可以使用<<gr[ae]y>>匹配graygrey。这在你不确定你要搜索的字符是采用美国英语还是英国英语时特别有用。相反,<<gr[ae]y>>将不会匹配graaygraey。字符集中的字符顺序并没有什么关系,结果都是相同的。

你可以使用连字符“”定义一个字符范围作为字符集。<<[0-9]>>匹配09之间的单个数字。你可以使用不止一个范围。<<[0-9a-fA-F] >>匹配单个的十六进制数字,并且大小写不敏感。你也可以结合范围定义与单个字符定义。<<[0-9a-fxA-FX]>>匹配一个十六进制数字或字母X。再次强调一下,字符和范围定义的先后顺序对结果没有影响。

 

·        字符集的一些应用

查找一个可能有拼写错误的单词,比如<<sep[ae]r[ae]te>> <<li[cs]en[cs]e>>

查找程序语言的标识符,<<A-Za-z_][A-Za-z_0-9]*>>(*表示重复0或多次)

查找C风格的十六进制数<<0[xX][A-Fa-f0-9]+>>(+表示重复一次或多次)

 

·        取反字符集

在左方括号“[”后面紧跟一个尖括号“^”,将会对字符集取反。结果是字符集将匹配任何不在方括号中的字符。不像“.”,取反字符集是可以匹配回车换行符的。

需要记住的很重要的一点是,取反字符集必须要匹配一个字符。<<q[^u]>>并不意味着:匹配一个q,后面没有u跟着。它意味着:匹配一个q,后面跟着一个不是u的字符。所以它不会匹配“Iraq”中的q,而会匹配“Iraq is a country”中的q和一个空格符。事实上,空格符是匹配中的一部分,因为它是一个“不是u的字符”。

如果你只想匹配一个q,条件是q后面有一个不是u的字符,我们可以用后面将讲到的向前查看来解决。

 

·        字符集中的元字符

需要注意的是,在字符集中只有4 字符具有特殊含义。它们是:“] " ^ –”。“]”代表字符集定义的结束;“"”代表转义;“^”代表取反;“”代表范围定义。其他常见的元字符在字符集定义内部都是正常字符,不需要转义。例如,要搜索星号*或加号+,你可以用<<[+*]>>。当然,如果你对那些通常的元字符进行转义,你的正则表达式一样会工作得很好,但是这会降低可读性。

在字符集定义中为了将反斜杠“"”作为一个文字字符而非特殊含义的字符,你需要用另一个反斜杠对它进行转义。<<[""x]>>将会匹配一个反斜杠和一个X。“]^-”都可以用反斜杠进行转义,或者将他们放在一个不可能使用到他们特殊含义的位置。我们推荐后者,因为这样可以增加可读性。比如对于字符“^”,将它放在除了左括号“[”后面的位置,使用的都是文字字符含义而非取反含义。如<<[x^]>>会匹配一个x^<<[]x]>>会匹配一个“]”或“x”。<<[-x]>><<[x-]>>都会匹配一个“”或“x”。

 

·        字符集的简写

因为一些字符集非常常用,所以有一些简写方式。

<<"d>>代表<<[0-9]>>;

<<"w>>代表单词字符。这个是随正则表达式实现的不同而有些差异。绝大多数的正则表达式实现的单词字符集都包含了<<A-Za-z0-9_]>>

<<"s>>代表“白字符”。这个也是和不同的实现有关的。在绝大多数的实现中,都包含了空格符和Tab符,以及回车换行符<<"r"n>>

字符集的缩写形式可以用在方括号之内或之外。<<"s"d>>匹配一个白字符后面紧跟一个数字。<<["s"d]>>匹配单个白字符或数字。<<["da-fA-F]>>将匹配一个十六进制数字。

取反字符集的简写

<<["S]>> = <<[^"s]>>

<<["W]>> = <<[^"w]>>

<<["D]>> = <<[^"d]>>

·        字符集的重复

如果你用“?*+”操作符来重复一个字符集,你将会重复整个字符集。而不仅是它匹配的那个字符。正则表达式<<[0-9]+>>会匹配837以及222

如果你仅仅想重复被匹配的那个字符,可以用向后引用达到目的。我们以后将讲到向后引用。

 

 

6.      使用?*+ 进行重复

?:告诉引擎匹配前导字符0次或一次。事实上是表示前导字符是可选的。

+:告诉引擎匹配前导字符1次或多次

*:告诉引擎匹配前导字符0次或多次

<[A-Za-z][A-Za-z0-9]*>匹配没有属性的HTML标签,“<”以及“>”是文字符号。第一个字符集匹配一个字母,第二个字符集匹配一个字母或数字。

我们似乎也可以用<[A-Za-z0-9]+>。但是它会匹配<1>。但是这个正则表达式在你知道你要搜索的字符串不包含类似的无效标签时还是足够有效的。

 

·        限制性重复

许多现代的正则表达式实现,都允许你定义对一个字符重复多少次。词法是:{min,max}minmax都是非负整数。如果逗号有而max被忽略了,则max没有限制。如果逗号和max都被忽略了,则重复min次。

因此{0,}*一样,{1}+ 的作用一样。

你可以用<<"b[1-9][0-9]{3}"b>>匹配1000~9999之间的数字("b”表示单词边界)<<"b[1-9][0-9]{2,4}"b>>匹配一个在100~99999之间的数字。

 

·        注意贪婪性

假设你想用一个正则表达式匹配一个HTML标签。你知道输入将会是一个有效的HTML文件,因此正则表达式不需要排除那些无效的标签。所以如果是在两个尖括号之间的内容,就应该是一个HTML标签。

许多正则表达式的新手会首先想到用正则表达式<< <.+> >>,他们会很惊讶的发现,对于测试字符串,“This is a <EM>first</EM> test”,你可能期望会返回<EM>,然后继续进行匹配的时候,返回</EM>

但事实是不会。正则表达式将会匹配“<EM>first</EM>”。很显然这不是我们想要的结果。原因在于“+”是贪婪的。也就是说,“+”会导致正则表达式引擎试图尽可能的重复前导字符。只有当这种重复会引起整个正则表达式匹配失败的情况下,引擎会进行回溯。也就是说,它会放弃最后一次的“重复”,然后处理正则表达式余下的部分。

和“+”类似,“?*”的重复也是贪婪的。

 

·        深入正则表达式引擎内部

让我们来看看正则引擎如何匹配前面的例子。第一个记号是“<”,这是一个文字符号。第二个符号是“.”,匹配了字符“E”,然后“+”一直可以匹配其余的字符,直到一行的结束。然后到了换行符,匹配失败(.”不匹配换行符)。于是引擎开始对下一个正则表达式符号进行匹配。也即试图匹配“>”。到目前为止,“<.+”已经匹配了“<EM>first</EM> test”。引擎会试图将“>”与换行符进行匹配,结果失败了。于是引擎进行回溯。结果是现在“<.+”匹配“<EM>first</EM> tes”。于是引擎将“>”与“t”进行匹配。显然还是会失败。这个过程继续,直到“<.+”匹配“<EM>first</EM”,“>”与“>”匹配。于是引擎找到了一个匹配“<EM>first</EM>”。记住,正则导向的引擎是“急切的”,所以它会急着报告它找到的第一个匹配。而不是继续回溯,即使可能会有更好的匹配,例如“<EM>”。所以我们可以看到,由于“+”的贪婪性,使得正则表达式引擎返回了一个最左边的最长的匹配。

 

·        用懒惰性取代贪婪性

一个用于修正以上问题的可能方案是用“+”的惰性代替贪婪性。你可以在“+”后面紧跟一个问号“?”来达到这一点。“*”,“{}”和“?”表示的重复也可以用这个方案。因此在上面的例子中我们可以使用“<.+?>”。让我们再来看看正则表达式引擎的处理过程。

再一次,正则表达式记号“<”会匹配字符串的第一个“<”。下一个正则记号是“.”。这次是一个懒惰的“+”来重复上一个字符。这告诉正则引擎,尽可能少的重复上一个字符。因此引擎匹配“.”和字符“E”,然后用“>”匹配“M”,结果失败了。引擎会进行回溯,和上一个例子不同,因为是惰性重复,所以引擎是扩展惰性重复而不是减少,于是“<.+”现在被扩展为“<EM”。引擎继续匹配下一个记号“>”。这次得到了一个成功匹配。引擎于是报告“<EM>”是一个成功的匹配。整个过程大致如此。

 

·        惰性扩展的一个替代方案

我们还有一个更好的替代方案。可以用一个贪婪重复与一个取反字符集:“<[^>]+>”。之所以说这是一个更好的方案在于使用惰性重复时,引擎会在找到一个成功匹配前对每一个字符进行回溯。而使用取反字符集则不需要进行回溯。

最后要记住的是,本教程仅仅谈到的是正则导向的引擎。文本导向的引擎是不回溯的。但是同时他们也不支持惰性重复操作。

 

7.      使用“.”匹配几乎任意字符

在正则表达式中,“.”是最常用的符号之一。不幸的是,它也是最容易被误用的符号之一。

.”匹配一个单个的字符而不用关心被匹配的字符是什么。唯一的例外是新行符。在本教程中谈到的引擎,缺省情况下都是不匹配新行符的。因此在缺省情况下,“.”等于是字符集[^"n"r](Window)[^"n]( Unix)的简写。

这个例外是因为历史的原因。因为早期使用正则表达式的工具是基于行的。它们都是一行一行的读入一个文件,将正则表达式分别应用到每一行上去。在这些工具中,字符串是不包含新行符的。因此“.”也就从不匹配新行符。

现代的工具和语言能够将正则表达式应用到很大的字符串甚至整个文件上去。本教程讨论的所有正则表达式实现都提供一个选项,可以使“.”匹配所有的字符,包括新行符。在RegexBuddy, EditPad ProPowerGREP等工具中,你可以简单的选中“点号匹配新行符”。在Perl中,“.”可以匹配新行符的模式被称作“单行模式”。很不幸,这是一个很容易混淆的名词。因为还有所谓“多行模式”。多行模式只影响行首行尾的锚定(anchor),而单行模式只影响“.”。

其他语言和正则表达式库也采用了Perl的术语定义。当在.NET Framework中使用正则表达式类时,你可以用类似下面的语句来激活单行模式:Regex.Match(“string”,”regex”,RegexOptions.SingleLine)

 

 

·        保守的使用点号“.

点号可以说是最强大的元字符。它允许你偷懒:用一个点号,就能匹配几乎所有的字符。但是问题在于,它也常常会匹配不该匹配的字符。

我会以一个简单的例子来说明。让我们看看如何匹配一个具有“mm/dd/yy”格式的日期,但是我们想允许用户来选择分隔符。很快能想到的一个方案是<<"d"d."d"d."d"d>>。看上去它能匹配日期“02/12/03”。问题在于02512703也会被认为是一个有效的日期。

<<"d"d[-/.]"d"d[-/.]"d"d>>看上去是一个好一点的解决方案。记住点号在一个字符集里不是元字符。这个方案远不够完善,它会匹配“99/99/99”。而<<[0-1]"d[-/.][0-3]"d[-/.]"d"d>>又更进一步。尽管他也会匹配“19/39/99”。你想要你的正则表达式达到如何完美的程度取决于你想达到什么样的目的。如果你想校验用户输入,则需要尽可能的完美。如果你只是想分析一个已知的源,并且我们知道没有错误的数据,用一个比较好的正则表达式来匹配你想要搜寻的字符就已经足够。

 

8.      字符串开始和结束的锚定

锚定和一般的正则表达式符号不同,它不匹配任何字符。相反,他们匹配的是字符之前或之后的位置。“^”匹配一行字符串第一个字符前的位置。<<^a>>将会匹配字符串“abc”中的a<<^b>>将不会匹配“abc”中的任何字符。

类似的,$匹配字符串中最后一个字符的后面的位置。所以<<c$>>匹配“abc”中的c

 

·        锚定的应用

在编程语言中校验用户输入时,使用锚定是非常重要的。如果你想校验用户的输入为整数,用<<^"d+$>>

用户输入中,常常会有多余的前导空格或结束空格。你可以用<<^"s*>><<"s*$>>来匹配前导空格或结束空格。

 

·        使用“^”和“$”作为行的开始和结束锚定

如果你有一个包含了多行的字符串。例如:“first line"n"rsecond line(其中"n"r表示一个新行符)。常常需要对每行分别处理而不是整个字符串。因此,几乎所有的正则表达式引擎都提供一个选项,可以扩展这两种锚定的含义。“^”可以匹配字串的开始位置(f之前),以及每一个新行符的后面位置("n"rs之间)。类似的,$会匹配字串的结束位置(最后一个e之后),以及每个新行符的前面(e"n"r之间)

.NET中,当你使用如下代码时,将会定义锚定匹配每一个新行符的前面和后面位置:Regex.Match("string", "regex", RegexOptions.Multiline)

应用:string str = Regex.Replace(Original, "^", "> ", RegexOptions.Multiline)–将会在每行的行首插入“> ”。

 

·        绝对锚定

<<"A>>只匹配整个字符串的开始位置,<<"Z>>只匹配整个字符串的结束位置。即使你使用了“多行模式”,<<"A>><<"Z>>也从不匹配新行符。

即使"Z$只匹配字符串的结束位置,仍然有一个例外的情况。如果字符串以新行符结束,则"Z$将会匹配新行符前面的位置,而不是整个字符串的最后面。这个“改进”是由Perl引进的,然后被许多的正则表达式实现所遵循,包括Java.NET等。如果应用<<^[a-z]+$>>到“joe"n”,则匹配结果是“joe”而不是“joe"n”。

 

9.      单词边界

 

元字符<<"b>>也是一种对位置进行匹配的“锚”。这种匹配是0长度匹配。

4种位置被认为是“单词边界”:

1)        在字符串的第一个字符前的位置(如果字符串的第一个字符是一个“单词字符”)

2)        在字符串的最后一个字符后的位置(如果字符串的最后一个字符是一个“单词字符”)

3)        在一个“单词字符”和“非单词字符”之间,其中“非单词字符”紧跟在“单词字符”之后

4)        在一个“非单词字符”和“单词字符”之间,其中“单词字符”紧跟在“非单词字符”后面

 “单词字符”是可以用“"w”匹配的字符,“非单词字符”是可以用“"W”匹配的字符。在大多数的正则表达式实现中,“单词字符”通常包括<<[a-zA-Z0-9_]>>

例如:<<"b4"b>>能够匹配单个的4而不是一个更大数的一部分。这个正则表达式不会匹配“44”中的4

换种说法,几乎可以说<<"b>>匹配一个“字母数字序列”的开始和结束的位置。

 

“单词边界”的取反集为<<"B>>,他要匹配的位置是两个“单词字符”之间或者两个“非单词字符”之间的位置。

 

·        深入正则表达式引擎内部

让我们看看把正则表达式<<"bis"b>>应用到字符串“This island is beautiful”。引擎先处理符号<<"b>>。因为"b0长度 ,所以第一个字符T前面的位置会被考察。因为T是一个“单词字符”,而它前面的字符是一个空字符(void),所以"b匹配了单词边界。接着<<i>>和第一个字符“T”匹配失败。匹配过程继续进行,直到第五个空格符,和第四个字符“s”之间又匹配了<<"b>>。然而空格符和<<i>>不匹配。继续向后,到了第六个字符“i”,和第五个空格字符之间匹配了<<"b>>,然后<<is>>和第六、第七个字符都匹配了。然而第八个字符和第二个“单词边界”不匹配,所以匹配又失败了。到了第13个字符i,因为和前面一个空格符形成“单词边界”,同时<<is>>和“is”匹配。引擎接着尝试匹配第二个<<"b>>。因为第15个空格符和“s”形成单词边界,所以匹配成功。引擎“急着”返回成功匹配的结果。

 

10. 选择符

正则表达式中“|”表示选择。你可以用选择符匹配多个可能的正则表达式中的一个。

如果你想搜索文字“cat”或“dog”,你可以用<<cat|dog>>。如果你想有更多的选择,你只要扩展列表<<cat|dog|mouse|fish>>

选择符在正则表达式中具有最低的优先级,也就是说,它告诉引擎要么匹配选择符左边的所有表达式,要么匹配右边的所有表达式。你也可以用圆括号来限制选择符的作用范围。如<<"b(cat|dog)"b>>,这样告诉正则引擎把(cat|dog)当成一个正则表达式单位来处理。

·        注意正则引擎的“急于表功”性

正则引擎是急切的,当它找到一个有效的匹配时,它会停止搜索。因此在一定条件下,选择符两边的表达式的顺序对结果会有影响。假设你想用正则表达式搜索一个编程语言的函数列表:GetGetValueSetSetValue。一个明显的解决方案是<<Get|GetValue|Set|SetValue>>。让我们看看当搜索SetValue时的结果。

因为<<Get>><<GetValue>>都失败了,而<<Set>>匹配成功。因为正则导向的引擎都是“急切”的,所以它会返回第一个成功的匹配,就是“Set”,而不去继续搜索是否有其他更好的匹配。

和我们期望的相反,正则表达式并没有匹配整个字符串。有几种可能的解决办法。一是考虑到正则引擎的“急切”性,改变选项的顺序,例如我们使用<<GetValue|Get|SetValue|Set>>,这样我们就可以优先搜索最长的匹配。我们也可以把四个选项结合起来成两个选项:<<Get(Value)?|Set(Value)?>>。因为问号重复符是贪婪的,所以SetValue总会在Set之前被匹配。

一个更好的方案是使用单词边界:<<"b(Get|GetValue|Set|SetValue)"b>><<"b(Get(Value)?|Set(Value)?"b>>。更进一步,既然所有的选择都有相同的结尾,我们可以把正则表达式优化为<<"b(Get|Set)(Value)?"b>>

 

 

11. 组与向后引用

把正则表达式的一部分放在圆括号内,你可以将它们形成组。然后你可以对整个组使用一些正则操作,例如重复操作符。

要注意的是,只有圆括号“()”才能用于形成组。“[]”用于定义字符集。“{}”用于定义重复操作。

当用“()”定义了一个正则表达式组后,正则引擎则会把被匹配的组按照顺序编号,存入缓存。当对被匹配的组进行向后引用的时候,可以用“"数字”的方式进行引用。<<"1>>引用第一个匹配的后向引用组,<<"2>>引用第二个组,以此类推,<<"n>>引用第n个组。而<<"0>>则引用整个被匹配的正则表达式本身。我们看一个例子。

假设你想匹配一个HTML标签的开始标签和结束标签,以及标签中间的文本。比如<B>This is a test</B>,我们要匹配<B></B>以及中间的文字。我们可以用如下正则表达式:“<([A-Z][A-Z0-9]*)[^>]*>.*?</"1>

首先,“<”将会匹配“<B>”的第一个字符“<”。然后[A-Z]匹配B[A-Z0-9]*将会匹配0到多次字母数字,后面紧接着0到多个非“>”的字符。最后正则表达式的“>”将会匹配“<B>”的“>”。接下来正则引擎将对结束标签之前的字符进行惰性匹配,直到遇到一个“</”符号。然后正则表达式中的“"1”表示对前面匹配的组“([A-Z][A-Z0-9]*)”进行引用,在本例中,被引用的是标签名“B”。所以需要被匹配的结尾标签为“</B>

你可以对相同的后向引用组进行多次引用,<<([a-c])x"1x"1>>将匹配“axaxa”、“bxbxb”以及“cxcxc”。如果用数字形式引用的组没有有效的匹配,则引用到的内容简单的为空。

一个后向引用不能用于它自身。<<([abc]"1)>>是错误的。因此你不能将<<"0>>用于一个正则表达式匹配本身,它只能用于替换操作中。

后向引用不能用于字符集内部。<<(a)["1b]>>中的<<"1>>并不表示后向引用。在字符集内部,<<"1>>可以被解释为八进制形式的转码。

向后引用会降低引擎的速度,因为它需要存储匹配的组。如果你不需要向后引用,你可以告诉引擎对某个组不存储。例如:<<Get(?:Value)>>。其中“(”后面紧跟的“?:”会告诉引擎对于组(Value),不存储匹配的值以供后向引用。

·        重复操作与后向引用

当对组使用重复操作符时,缓存里后向引用内容会被不断刷新,只保留最后匹配的内容。例如:<<([abc]+)="1>>将匹配“cab=cab”,但是<<([abc])+="1>>却不会。因为([abc])第一次匹配“c”时,“"1”代表“c”;然后([abc])会继续匹配“a”和“b”。最后“"1”代表“b”,所以它会匹配“cab=b”。

应用:检查重复单词当编辑文字时,很容易就会输入重复单词,例如“the the”。使用<<"b("w+)"s+"1"b>>可以检测到这些重复单词。要删除第二个单词,只要简单的利用替换功能替换掉“"1”就可以了。

 

 

·        组的命名和引用

PHPPython中,可以用<<(?P<name>group)>>来对组进行命名。在本例中,词法?P<name>就是对组(group)进行了命名。其中name是你对组的起的名字。你可以用(?P=name)进行引用。

.NET的命名组

.NET framework也支持命名组。不幸的是,微软的程序员们决定发明他们自己的语法,而不是沿用PerlPython的规则。目前为止,还没有任何其他的正则表达式实现支持微软发明的语法。

下面是.NET中的例子:

(?<first>group)(?’second’group)

正如你所看到的,.NET提供两种词法来创建命名组:一是用尖括号“<>”,或者用单引号“’’”。尖括号在字符串中使用更方便,单引号在ASP代码中更有用,因为ASP代码中“<>”被用作HTML标签。

要引用一个命名组,使用"k<name>"k’name’.

当进行搜索替换时,你可以用“${name}”来引用一个命名组。

 

12. 正则表达式的匹配模式

本教程所讨论的正则表达式引擎都支持三种匹配模式:

<</i>>使正则表达式对大小写不敏感,

<</s>>开启“单行模式”,即点号“.”匹配新行符

<</m>>开启“多行模式”,即“^”和“$”匹配新行符的前面和后面的位置。

 

·        在正则表达式内部打开或关闭模式

如果你在正则表达式内部插入修饰符(?ism),则该修饰符只对其右边的正则表达式起作用。(?-i)是关闭大小写不敏感。你可以很快的进行测试。<<(?i)te(?-i)st>>应该匹配TEst,但是不能匹配teSTTEST.

 

13. 原子组与防止回溯

在一些特殊情况下,因为回溯会使得引擎的效率极其低下。

让我们看一个例子:要匹配这样的字串,字串中的每个字段间用逗号做分隔符,第12个字段由P开头。

我们容易想到这样的正则表达式<<^(.*?,){11}P>>。这个正则表达式在正常情况下工作的很好。但是在极端情况下,如果第12个字段不是由P开头,则会发生灾难性的回溯。如要搜索的字串为“1,2,3,4,5,6,7,8,9,10,11,12,13”。首先,正则表达式一直成功匹配直到第12个字符。这时,前面的正则表达式消耗的字串为“1,2,3,4,5,6,7,8,9,10,11,”,到了下一个字符,<<P>>并不匹配“12”。所以引擎进行回溯,这时正则表达式消耗的字串为“1,2,3,4,5,6,7,8,9,10,11”。继续下一次匹配过程,下一个正则符号为点号<<.>>,可以匹配下一个逗号“,”。然而<<>>并不匹配字符“12”中的“1”。匹配失败,继续回溯。大家可以想象,这样的回溯组合是个非常大的数量。因此可能会造成引擎崩溃。

用于阻止这样巨大的回溯有几种方案:

一种简单的方案是尽可能的使匹配精确。用取反字符集代替点号。例如我们用如下正则表达式<<^([^,"r"n]*,){11}P>>,这样可以使失败回溯的次数下降到11次。

另一种方案是使用原子组。

原子组的目的是使正则引擎失败的更快一点。因此可以有效的阻止海量回溯。原子组的语法是<<(?>正则表达式)>>。位于(?>)之间的所有正则表达式都会被认为是一个单一的正则符号。一旦匹配失败,引擎将会回溯到原子组前面的正则表达式部分。前面的例子用原子组可以表达成<<^(?>(.*?,){11})P>>。一旦第十二个字段匹配失败,引擎回溯到原子组前面的<<^>>

 

14. 向前查看与向后查看

Perl 5 引 入了两个强大的正则语法:“向前查看”和“向后查看”。他们也被称作“零长度断言”。他们和锚定一样都是零长度的(所谓零长度即指该正则表达式不消耗被匹 配的字符串)。不同之处在于“前后查看”会实际匹配字符,只是他们会抛弃匹配只返回匹配结果:匹配或不匹配。这就是为什么他们被称作“断言”。他们并不实 际消耗字符串中的字符,而只是断言一个匹配是否可能。

几乎本文讨论的所有正则表达式的实现都支持“向前向后查看”。唯一的一个例外是JavaScript只支持向前查看。

·        肯定和否定式的向前查看

如我们前面提过的一个例子:要查找一个q,后面没有紧跟一个u。也就是说,要么q后面没有字符,要么后面的字符不是u。采用否定式向前查看后的一个解决方案为<<q(?!u)>>。否定式向前查看的语法是<<(?!查看的内容)>>

肯定式向前查看和否定式向前查看很类似:<<(?=查看的内容)>>

如果在“查看的内容”部分有组,也会产生一个向后引用。但是向前查看本身并不会产生向后引用,也不会被计入向后引用的编号中。这是因为向前查看本身是会被抛弃掉的,只保留匹配与否的判断结果。如果你想保留匹配的结果作为向后引用,你可以用<<(?=(regex))>>来产生一个向后引用。

·        肯定和否定式的先后查看

向后查看和向前查看有相同的效果,只是方向相反

否定式向后查看的语法是:<<(?<!查看内容)>>

肯定式向后查看的语法是:<<(?<=查看内容)>>

我们可以看到,和向前查看相比,多了一个表示方向的左尖括号。

例:<<(?<!a)b>>将会匹配一个没有“a”作前导字符的“b”。

值得注意的是:向前查看从当前字符串位置开始对“查看”正则表达式进行匹配;向后查看则从当前字符串位置开始先后回溯一个字符,然后再开始对“查看”正则表达式进行匹配。

 

·        深入正则表达式引擎内部

让我们看一个简单例子。

把正则表达式<<q(?!u)>>应用到字符串“Iraq”。正则表达式的第一个符号是<<q>>。正如我们知道的,引擎在匹配<<q>>以前会扫过整个字符串。当第四个字符“q”被匹配后,“q”后面是空字符(void)。而下一个正则符号是向前查看。引擎注意到已经进入了一个向前查看正则表达式部分。下一个正则符号是<<u>>,和空字符不匹配,从而导致向前查看里的正则表达式匹配失败。因为是一个否定式的向前查看,意味着整个向前查看结果是成功的。于是匹配结果“q”被返回了。

我们在把相同的正则表达式应用到“quit”。<<q>>匹配了“q”。下一个正则符号是向前查看部分的<<u>>,它匹配了字符串中的第二个字符“i”。引擎继续走到下个字符“i”。然而引擎这时注意到向前查看部分已经处理完了,并且向前查看已经成功。于是引擎抛弃被匹配的字符串部分,这将导致引擎回退到字符“u”。

因为向前查看是否定式的,意味着查看部分的成功匹配导致了整个向前查看的失败,因此引擎不得不进行回溯。最后因为再没有其他的“q”和<<q>>匹配,所以整个匹配失败了。

为了确保你能清楚地理解向前查看的实现,让我们把<<q(?=u)i>>应用到“quit”。<<q>>首先匹配“q”。然后向前查看成功匹配“u”,匹配的部分被抛弃,只返回可以匹配的判断结果。引擎从字符“i”回退到“u”。由于向前查看成功了,引擎继续处理下一个正则符号<<i>>。结果发现<<i>>和“u”不匹配。因此匹配失败了。由于后面没有其他的“q”,整个正则表达式的匹配失败了。

 

·        更进一步理解正则表达式引擎内部机制

让我们把<<(?<=a)b>>应用到“thingamabob”。引擎开始处理向后查看部分的正则符号和字符串中的第一个字符。在这个例子中,向后查看告诉正则表达式引擎回退一个字符,然后查看是否有一个“a”被匹配。因为在“t”前面没有字符,所以引擎不能回退。因此向后查看失败了。引擎继续走到下一个字符“h”。再一次,引擎暂时回退一个字符并检查是否有个“a”被匹配。结果发现了一个“t”。向后查看又失败了。

向后查看继续失败,直到正则表达式到达了字符串中的“m”,于是肯定式的向后查看被匹配了。因为它是零长度的,字符串的当前位置仍然是“m”。下一个正则符号是<<b>>,和“m”匹配失败。下一个字符是字符串中的第二个“a”。引擎向后暂时回退一个字符,并且发现<<a>>不匹配“m”。

在下一个字符是字符串中的第一个“b”。引擎暂时性的向后退一个字符发现向后查看被满足了,同时<<b>>匹配了“b”。因此整个正则表达式被匹配了。作为结果,正则表达式返回字符串中的第一个“b”。

·        向前向后查看的应用

我们来看这样一个例子:查找一个具有6位字符的,含有“cat”的单词。

首先,我们可以不用向前向后查看来解决问题,例如:

<< cat"w{3}|"wcat"w{2}|"w{2}cat"w|"w{3}cat>>

足够简单吧!但是当需求变成查找一个具有6-12位字符,含有“cat”,“dog”或“mouse”的单词时,这种方法就变得有些笨拙了。

我们来看看使用向前查看的方案。在这个例子中,我们有两个基本需求要满足:一是我们需要一个6位的字符,二是单词含有“cat”。

满足第一个需求的正则表达式为<<"b"w{6}"b>>。满足第二个需求的正则表达式为<<"b"w*cat"w*"b>>

把两者结合起来,我们可以得到如下的正则表达式:

     <<(?="b"w{6}"b)"b"w*cat"w*"b>>

具体的匹配过程留给读者。但是要注意的一点是,向前查看是不消耗字符的,因此当判断单词满足具有6个字符的条件后,引擎会从开始判断前的位置继续对后面的正则表达式进行匹配。

最后作些优化,可以得到下面的正则表达式:

<<"b(?="w{6}"b)"w{0,3}cat"w*>>

 

15. 正则表达式中的条件测试

条件测试的语法为<<(?ifthen|else)>>。“if”部分可以是向前向后查看表达式。如果用向前查看,则语法变为:<<(?(?=regex)then|else)>>,其中else部分是可选的。

如果if部分为true,则正则引擎会试图匹配then部分,否则引擎会试图匹配else部分。

需要记住的是,向前先后查看并不实际消耗任何字符,因此后面的thenelse部分的匹配时从if测试前的部分开始进行尝试。

 

16. 为正则表达式添加注释

在正则表达式中添加注释的语法是:<<(?#comment)>>

例:为用于匹配有效日期的正则表达式添加注释:

 (?#year)(19|20)"d"d[- /.](?#month)(0[1-9]|1[012])[- /.](?#day)(0[1-9]|[12][0-9]|3[01])

[SQL]SQLServer : EXEC和sp_executesql的区别

mikel阅读(1057)

摘要

1,EXEC的使用

2,sp_executeSQL的使用

       MSSQL为我们提供了两种动态执行SQL语句的命令,分别是EXEC和sp_executesql;通常,sp_executesql则更具有优势,它 提供了输入输出接口,而EXEC没有。还有一个最大的好处就是利用sp_executesql,能够重用执行计划,这就大大提供了执行性能(对于这个我在 后面的例子中会详加说明),还可以编写更安全的代码。EXEC在某些情况下会更灵活。除非您有令人信服的理由使用EXEC,否侧尽量使用 sp_executesql.

1,EXEC的使用

EXEC命令有两种用法,一种是执行一个存储过程,另一种是执行一个动态的批处理。以下所讲的都是第二种用法。

下面先使用EXEC演示一个例子,代码1

DECLARE @TableName VARCHAR(50),@Sql NVARCHAR(MAX),@OrderID INT;
SET @TableName = 'Orders';
SET @OrderID = 10251;
SET @sql = 'Select * FROM '+QUOTENAME(@TableName) +'Where orderID = '+CAST(@OrderID AS VARCHAR(10))+' orDER BY orDERID DESC'
EXEC(@sql);

注:这里的EXEC括号中只允许包含一个字符串变量,但是可以串联多个变量,如果我们这样写EXEC:

EXEC('Select TOP('+ CAST(@TopCount AS VARCHAR(10)) +')* FROM '+QUOTENAME(@TableName) +' orDER BY orDERID DESC');
 
SQL编译器就会报错,编译不通过,而如果我们这样:
EXEC(@sql+@sql2+@sql3);
编译器就会通过;
 
所以最佳的做法是把代码构造到一个变量中,然后再把该变量作为EXEC命令的输入参数,这样就不会受限制了;
 
EXEC不提供接口
这里的接口是指,它不能执行一个包含一个带变量符的批处理,这里乍一听好像不明白,不要紧,我在下面有一个实例,您一看就知道什么意思.
DECLARE @TableName VARCHAR(50),@Sql NVARCHAR(MAX),@OrderID INT;
SET @TableName = 'Orders';
SET @OrderID = 10251;
SET @sql = 'Select * FROM '+QUOTENAME(@TableName) +'Where OrderID = @OrderID orDER BY orDERID DESC'
EXEC(@sql);

关键就在SET @sql这一句话中,如果我们运行这个批处理,编译器就会产生一下错误

Msg 137, Level 15, State 2, Line 1
必须声明标量变量 "@OrderID"。

使用EXEC时,如果您想访问变量,必须把变量内容串联到动态构建的代码字符串中,如:SET @sql = 'Select * FROM '+QUOTENAME(@TableName) +'Where orderID = '+CAST(@OrderID AS VARCHAR(10))+' orDER BY orDERID DESC'

串联变量的内容也存在性能方面的弊端。SQL Server为每一个的查询字符串创建新的执行计划,即使查询模式相同也是这样。为演示这一点,先清空缓存中的执行计划

DBCC FREEPROCCACHE (这个不是本文所涉及的内容,您可以查看MS的MSDN)

http://msdn.microsoft.com/zh-cn/library/ms174283.aspx 

将代码1运行3次,分别对@OrderID 赋予下面3个值,10251,10252,10253。然后使用下面的代码查询

Select cacheobjtype,objtype,usecounts,sql FROM sys.syscacheobjects Where sql NOT LIKE '%cach%' AND sql NOT LIKE '%sys.%' 

点击F5运行,就会出现下面如图所示的查询结果:
dynmicsql1

我们可以看到,每执行一次都要产生一次的编译,执行计划没有得到充分重用。

EXEC除了不支持动态批处理中的输入参数外,他也不支持输出参数。默认情况下,EXEC把查询的输出返回给调用者。例如下面代码返回Orders表中所有的记录数

DECLARE @sql NVARCHAR(MAX)
SET @sql = 'Select COUNT(ORDERID) FROM orders';
EXEC(@sql);

然而,如果你要把输出返回给调用批处理中的变量,事情就没有那么简单了。为此,你必须使用Insert EXEC语法把输出插入到一个目标表中,然后从这表中获取值后赋给该变量,就像这样:

DECLARE @sql NVARCHAR(MAX),@RecordCount INT
SET @sql = 'Select COUNT(ORDERID) FROM orders';
 
Create TABLE #T(TID INT);
Insert INTO #T EXEC(@sql);
SET @RecordCount = (Select TID FROM #T)
Select @RecordCount
Drop TABLE #T

2,sp_executesql的使用

sp_executesql命令在SQL Server中引入的比EXEC命令晚一些,它主要为重用执行计划提供更好的支持。

为了和EXEC作一个鲜明的对比,我们看看如果用代码1的代码,把EXEC换成sp_executesql,看看是否得到我们所期望的结果

DECLARE @TableName VARCHAR(50),@sql NVARCHAR(MAX),@OrderID INT ,@sql2 NVARCHAR(MAX);
SET @TableName = 'Orders ';
SET @OrderID = 10251;
SET @sql = 'Select * FROM '+QUOTENAME(@TableName) + ' Where orderID = '+CAST(@OrderID AS VARCHAR(50)) + ' orDER BY orDERID DESC'
EXEC sp_executesql @sql

注意最后一行;

事实证明可以运行;

sp_executesql提供接口

sp_executesql命令比EXEC命令更灵活,因为它提供一个接口,该接口及支持输入参数也支持输出参数。这功能使你可以创建带参数的查询 字符串,这样就可以比EXEC更好的重用执行计划,sp_executesql的构成与存储过程非常相似,不同之处在于你是动态构建代码。它的构成包括: 代码快,参数声明部分,参数赋值部分。说了这么多,还是看看它的语法吧

EXEC sp_executesql

@stmt = <statement>,–类似存储过程主体

@params = <params>, –类似存储过程参数部分

<params assignment> –类似存储过程调用

@stmt参数是输入的动态批处理,它可以引入输入参数或输出参数,和存储过程的主体语句一样,只不过它是动态的,而存储过程是静态的,不过你也可以在存储过程中使用sp_executesql;

@params参数与定义输入/输出参数的存储过程头类似,实际上和存储过程头的语法完全一样;

@<params assignment> 与调用存储过程的EXEC部分类似。

为了说明sp_executesql对执行计划的管理优于EXEC,我将使用前面讨论EXEC时用到的代码。

   1:  DECLARE @TableName VARCHAR(50),@sql NVARCHAR(MAX),@OrderID INT;
   2:  SET @TableName = 'Orders ';
   3:  SET @OrderID = 10251;
   4:  SET @sql = 'Select * FROM '+QUOTENAME(@TableName) + ' Where orderID = @OID orDER BY orDERID DESC'
   5:  EXEC sp_executesql
   6:      @stmt = @sql,
   7:      @params = N'@OID AS INT ',
   8:      @OID = @OrderID

在调用该代码和检查它生成的执行计划前,先清空缓存中的执行计划;

DBCC FREEPROCCACHE

将上面的动态代码执行3次,每次执行都赋予@OrderID 不同的值,然后查询sys.syscacheobjects表,并注意它的输出,优化器只创建了一个备用计划,而且该计划被重用的3次

Select cacheobjtype,objtype,usecounts,sql FROM sys.syscacheobjects Where sql NOT LIKE '%cache%' AND sql NOT LIKE '%sys.%' AND sql NOT LIKE '%sp_executesql%'
点击F5运行,就会出现如下表所示的结果;
dynmicsql2
sq_executesql的另一个与其接口有关的强大功能是,你可以使用输出参数为调用批处理中的 变量返回值。利用该功能可以避免用临时表返回数据,从而得到更高效的代码和更少的重新编译。定义和使用输出参数的语法与存储过程类似。也就是说,你需要在 声明参数时指定OUTPUT子句。例如,下面的静态代码简单的演示了如何从动态批处理中利用输出参数@p把值返回到外部批处理中的变量@i.
DECLARE @sql AS NVARCHAR(12),@i AS INT;
SET @sql = N' SET @p = 10';
EXEC sp_executesql 
    @stmt = @sql,
    @params = N'@p AS INT OUTPUT',
    @p = @i OUTPUT
Select @i
该代码返回输出10
 
          以上就是EXEC和sp_executesql的主要区别,如果各位看官觉得哪不对或者表达不清楚的,还请多多指出^_^
 
          作者:兴百放
          时间:2008-11-2 22:30
          网址:Http://xbf321.cnblogs.com

[MVC]《ASP.NET MVC案例教程》索引贴

mikel阅读(856)

      本系列文章通过一个虚拟的案例——《MVC公告发布系统》的开发过程,全面展示了ASP.NET MVC的基本使用方法,同时在讨论了这个框架的基本原理。
      这个文章系列的目的就是使朋友们更轻松的入门ASP.NET MVC。
      这个系列会包含的内容有:ASP.NET MVC基本应用、基本原理、路由处理、表单处理、与ASP.NET AJAX结合、与JQuery结合、拦截器、结合Silverlight等。
 

      摘要: 本文将简要介绍这个文章系列的目的、形式及大体内容。并且完成开始学习这个系列前所必要的准备工作。

      摘要: 本文首先一步一步完成Demo的第一个页面——首页。然后根据实现过程,说明一下其中用到的与ASP.NET MVC相关的概念与原理。

      摘要: 本文对ASP.NET MVC的全局运行机理进行一个简要的介绍,以使得朋友们更好的理解后续文章。

      摘要: 本文将完成我们“MVC公告发布系统”的公告发布功能,以此展示在ASP.NET MVC中如何传递处理表单的数据。

[SVN]AnkhSVN有1.02版支持Vs2008

mikel阅读(854)

AnkhSvn 是一个Subversion plugin for Visual Studio.这个软件允许你在您的Microsoft Visual Studio IDE内执行共同的版本控制操作.使用AnkhSVN你不再需要离开你的IDE去执行查看你的源代码状态,更新你Subversion copy和提交.你能在浏览你的知识库和你能插件式插入你的喜欢的不同的工具.

地址


Optimized workflow    


(1)不用离开你的IDE就可操作
(2)立即查看你的工程所有文件的源代码控制状态.
(3)查看协助copy信息,如最后提交者,最后提交数据和知识库URL
(4)能从及时从solution explorer中导入solutions
(5)得到所有Subversion transfer protocols


Pluggable diff/merge


(1)插入你的选种的diff/merge tool.
(2)为大部分共同的merge tools使用command line templates.


Working Copy Explorer


很容易与你的非projects/solution的部分的文件协同工作.
Visual Studio solution explorer中使用相同的命令.


Repository Explorer


很容易浏览 Subversion repository
在Visual Studio Properties window中View remote files and directories 的扩展信息. 
图片
The working copy explorer.

The commit dialog.

The internal diff.

Adding a solution to a repository.

The Visual solution explorer.

[网站]Picnik

mikel阅读(827)

www.picnik.com

这 个站点允许我们将照片,图片等上传至网站之上,然后运用pinik及第三方开发的图像滤镜进行图片的各种编辑和处理,里面的功能超酷,而且,我还发现这个 站点可以运用一些Flash Player10的特效功能,比如pixel bender对图形进行实时处理,感兴趣的开发者可以快去尝试一把!这个站点有繁体中文版本支持!

[Flash]Flash3D编程探秘三

mikel阅读(920)

作者:Yang Zhou
感谢:Crystal

介绍

对学习Flash中的3D编程感兴趣?You come to the right place!在文章中,我将陆续的介绍在Flash中使用Actionsript进行3D编程一些理论和实例。这是一篇初级到中级难度的学习资料,如果你 具有一些基本的数学和几何知识,那对你来说不会太难。虽然例子中运用了非常简单的程序构架和实现方法,但是我还是期望你已经有大量的 Actionscirpt和Flash经验,这样你在使用以及理解Actionscript语言的时候不会有太大的障碍。如果3D这个课题对你来说是比较 新的东西,或者你在阅读中发现很多地方不清楚,那么我建议你参考我写过的另外几篇3D的基础知识的文章。

文章中含有大量的代码和一些算法的实现,并且附带源文件。你会发现我写的代码里有大量的注释,如果有什么不明白的话你可以参考一下注释。你可以完全拷贝我写的代码去使用,但是请务必注明出处。

 

点此下载本节源文件

 

在前面的两节中,你在空间的位置是一成不变的。我们一直是让舞台上的物体来回移动,并没有用到摄像机这个概念,在有些时候我们只需要让物体运动。但是随着你对3D的深入,你会发现那些并不足够

 

当我们讨论3D空间的时候,摄像机理论上代表3D中的一个点,我们从这个点去观看这个空间。为什么要使用摄像机呢?因为我们希望能够透过一个镜头看 到其他所有舞台上的物体,其实程序里摄像机只不过是一组数值来表示你的摄像机在3D空间的参数(比如位置)。想象一下你在广阔的撒哈拉沙漠里越野,又或者 你的朋友小P走向在你的身旁。你的朋友小P慢慢走向你并最终到达离你很近的位置,几下来你们可以交谈了。但是,如果小P站在原地不动,而是你走向他的身 旁,那么对于地面来说,小P是不动的,而你(摄像机)是移动的。在这里你的眼睛就充当了我们的摄像机。看看下面的两个动画文件,再对比一下。左边是小P走 向你,右边是你走向小P。

移动物体和移动摄像机

 

你会发现对于你的眼睛来说,你可以并不走动,只要把小P和周围一切的物体都移动到你的面前,也会达到同样的效果。但是哪一种可行呢?下面我们做两个动画来说明如何在我们的3D使用摄像机。动画效果如下,左面的是移动我们的小P,右边是移动我们的摄像机。

对比移动小P和移动你的摄像机

 

 

制作步骤:

1. 和以前一样,定义原点,设置焦距,创建舞台。

// origin is the center of the view point in 3d space
// everything scale around this point
// these lines of code will shift 3d space origin to the center
var origin = new Object();
origin.x 
= stage.stageWidth/2;
origin.y 
= stage.stageHeight/270;
// focal length of viewer's camera
var focal_length = 400;

2. 下面我们定义一个摄像机物体,它具有3D空间的x,y,z,并且我们给它一个移动方向和初始的在z方向的移动速度。

// setup camera
var camera = new Object();
camera.x 
= 0;
camera.y 
= 0;
camera.z 
= 0;
camera.direction 
= 1;
camera.speed_z 
= 6;

3. 创建一个小球在舞台上。

// create a sphere
// go to library, right click on Sphere, choose linkage
// and check Export for Actionscript
for (var i = 0; i < 1; i++)
{
    var sphere 
= new Sphere();
    sphere.x_3d 
= 30;
    sphere.y_3d 
= 80;
    sphere.z_3d 
= 600;
    
// add all the spherees to the scene object
    scene.addChild(sphere);
}

4. 接下来我们开始写运动的循环函数。每一次执行函数一开始我们要把摄像机的位置在z方向移动一定的量。如果摄像机离小球很近了的话,那么让摄像机回来。

// move the spherees back and forth
function run(e:Event)
{
    
// here we offset the camera position by its moving speed times the direction
    camera.z += camera.speed_z*camera.direction;
    
    
if (camera.z > 600)                    // if the camera is too close to the ball
    {
        camera.z 
= 600;
        camera.direction 
= 1;           // move camera backward
    }
    
else if (camera.z < 0)                // if the camera is too close to the screen
    {
        camera.z 
= 0;
        camera.direction 
= 1;
    }
    
// loop through all the objects on the scene
    for (var i = 0; i < scene.numChildren; i++)
    {
        
// calculate the scale what the object should be
        var scale = focal_length/(focal_length+scene.getChildAt(i).z_3dcamera.z);
        scene.getChildAt(i).x 
= (scene.getChildAt(i).x_3dcamera.x)*scale;
        scene.getChildAt(i).y 
= (scene.getChildAt(i).y_3dcamera.x)*scale;
        
// properly scale the object to look 3d
        scene.getChildAt(i).scaleX = scene.getChildAt(i).scaleY = scale;
    }
}

5. 计算出小球离摄像机的x距离,y距离和z距离,然后得出小球的缩放比率。最后把小球缩放并移动到相应的位置。That's it! 不要忘记添加循环函数执行事件。

this.addEventListener(Event.ENTER_FRAME, run);

注意:

你需要考虑让摄像机的z的指不能小于-1乘焦距,如果z小于这个值,那么公式scale = focal_length/(focal_length+z)得出的缩放比率会是负数,那么物体就会开始向后运动。

一个简单的赛车小游戏制作

下面我们运用摄像机的概念来制作一个简单的赛车小游戏,游戏里你可以使用上下左右键控制小车,COOL。那么我们开始。

简单赛车游戏,键盘上下左右方向键控制

 

1. 定义原点,设置焦距,创建舞台。

// origin is the center of the view point in 3d space
// everything scale around this point
// these lines of code will shift 3d space origin to the center
var origin = new Object();
origin.x 
= stage.stageWidth/2;
origin.y 
= stage.stageHeight/2;
// now create a scene object to hold all the spheres
var scene = new Sprite();
this.addChild(scene);
scene.x 
= origin.x;
scene.y 
= origin.y;
// focal length of viewer's camera
var focal_length = 400;

2. 下面我们定义一个摄像机物体,它具有3D空间的x,y,z,并且我们给它初始的在z方向的移动速度。

// setup camera
var camera = new Object();
camera.x 
= 0;
camera.y 
= 40;                   // make the camera off the ground a little bit
camera.z = 0;
camera.speed_z 
= 0;            // your driving speed

3. 创建两个个场景,一个用来盛放所有的小车,另外一个盛放放有的路边轮胎。然后把它们添加到舞台上。

var tires = new Sprite();                // this sprite holds all the tires
var cars = new Sprite();                // this sprite holds all the cars
var txt_speed = new TextField();    // your dashboard
txt_speed.x = 20;
txt_speed.y 
= 20;
// now add them to the screen
scene.addChild(tires);
scene.addChild(cars);
this.addChild(txt_speed);

4. 定义一些小车的运动状态的变量。

// now here are the variables determine the car's moving state
var move_left = false;
var move_right 
= false;
var speed_up 
= false;
var brake 
= false;

 

5. 那么接下来我们创建40个轮胎并且把前20个放在路的左边,后20个放在路的右边,这样我们给赛道画出一个轮廓。

// now create 40 tires, 20 on the left and 10 on the right
for (var i = 0; i < 40; i++)
{
    var tire 
= new Tire();
    
if (i < 20)
    {
        tire.x_3d 
= 400;
        tire.z_3d 
= i*500;
    }
    
else
    {
        tire.x_3d 
= 400;
        tire.z_3d 
= (i20)*500;
    }
    tire.y_3d 
= 40;
    tires.addChild(tire);
}

6. 创建8个小车,给它们相应的xyz位置(注意要赛车放在赛道上,设置它们的x范围在-230到230之间),给他们不同的起始z位置和速度,最后把它们添加到舞台上。

// create 8 opponent cars
for (var j = 0; j < 8; j++)
{
    var car 
= new Car();
    car.x_3d 
= Math.random()*(230230)+230;        // give them random x position
    car.y_3d = 30;
    car.z_3d 
= 800+(8j)*400;                              // give them speed
    car.speed_z = (8j)*15;
    cars.addChild(car);
}

7. 接下来我们要写一个函数,每一次我们执行这个函数,首先把赛车在z方向移动一定量(赛车相对地面是运动的),然后计算比率,把赛车移动到相应的位置并且缩 放。我把它命名updateCar,还是运用摄像机的理论。分别计算出摄像机与小车的xyz距离,然后把小车缩放和移动。注意小车如果离摄像机太远或者被 甩到摄像机的后面的话,我们可以让它不在屏幕上显示。

// for each of the running cycle, these two functions are called
function updateCar(car)
{
    var x 
= car.x_3dcamera.x;             // calculate the x distance between your camera and car
    var y = car.y_3dcamera.y;            // same we can y distance
    var z = car.z_3dcamera.z;            // and z distance
    
    
if (z < 0 || z > 10000)                   // if car is too far or left behind
        car.visible = false;                    // then do not draw it
    else
        car.visible 
= true;
        
    car.z_3d 
+= car.speed_z;             // move the car
    z = car.z_3dcamera.z;                // recaculate the z distance
    
    var scale 
= focal_length/(focal_length+z);        // caculate the scale what the car should be
    car.x = x*scale;
    car.y 
= y*scale;
    car.scaleX 
= car.scaleY = scale;    // scale it to a proper size
}

8. 轮胎的更新函数updateTire和赛车的更新函数类似,不同的是,轮胎相对地面是静止的,所以这里我们不改变它们的xyz值。只有在轮胎已经到了摄像机后面(轮胎的z小于摄像机的z),我们把这个轮胎重新定位到摄像机前非常远的地方。

function updateTire(tire)
{
    var x 
= tire.x_3dcamera.x;
    var y 
= tire.y_3dcamera.y;
    var z 
= tire.z_3dcamera.z;
    
    
if (z < 0)
    {
        tire.z_3d 
+= 10000;               // if the tire is left behind, then offset it 
        z = tire.z_3dcamera.z;            
    }
    
    var scale 
= focal_length/(focal_length+z);
    tire.x 
= x*scale;
    tire.y 
= y*scale;
    tire.scaleX 
= tire.scaleY = scale;
}

9. 下一步,run函数执行首先把我们的摄像头沿z方向移动,然后调用我们前面写的updateCar和updateTire函数,刷新所有赛车和轮胎的位置和大小。

// here is the function loop
function run(e:Event)
{
    camera.z 
+= camera.speed_z;                            // first move your camera
    
    
for (var i = 0; i < cars.numChildren; i++)              // update all the cars
    {
        updateCar(cars.getChildAt(i));
    }
    
for (var j = 0; j < tires.numChildren; j++)             // and the tires on the side
    {
        updateTire(tires.getChildAt(j));
    }
    
    txt_speed.text 
= int(camera.speed_z) + " MPH";   // show your speed
    txt_speed.setTextFormat(new TextFormat("Verdana"160x444444true));
}

10. 下面是键盘响应事件函数,我写了很多的注释在程序里,你应该很快就能看懂,就不打算详细解说了。功能是当按下左键你的赛车左移;按下上键,赛车开始加速(当然极速是需要你来定义的了)等等。

// keyboard functions
function key_down(e:KeyboardEvent):void
{
    
if (e.keyCode == 37)            // left key
        move_left = true;
    
if (e.keyCode == 39)            // right key
        move_right = true;
    
if (e.keyCode == 38)            // up key
        speed_up = true;
    
if (e.keyCode == 40)            // down key
        brake = true;
}
function key_up(e:KeyboardEvent):
void
{
    
if (e.keyCode == 37)
        move_left 
= false;
    
if (e.keyCode == 39)
        move_right 
= false;
    
if (e.keyCode == 38)
        speed_up 
= false;
    
if (e.keyCode == 40)
        brake 
= false;
}
function keyboard_response(e:Event):
void
{
    
if (move_left)            
    {
        
// move the camera to the left, remember here the fast you go, the fast your steer
        camera.x -= camera.speed_z/6;
        
if (camera.x < 300) camera.x = 300;     // limit your car so it won't go off the road
    }
    
if (move_right)
    {
        camera.x 
+= camera.speed_z/6;
        
if (camera.x > 300) camera.x = 300;        // limit your car so it won't go off the road
    }
    
if (speed_up)
    {
        camera.speed_z 
+= .2;                         // accelerate
        
// limit the car speed in a range
        if (camera.speed_z < 0) camera.speed_z = 0;            
        
else if (camera.speed_z > 120) camera.speed_z = 120;
    }
    
else
    {
        camera.speed_z 
*= .99;                      // if you don't hit the throttle, it will stop soon
    }
    
if (brake)
    {
        camera.speed_z 
-= .3;                       // slow down
        if (camera.speed_z < 0) camera.speed_z = 0;
    }
}

11. 最后,添加循环函数执行和键盘响应事件。如果没问题的话,现在发布运行。成功了!

// now initialize all the necessary event listeners and we are done
this.addEventListener(Event.ENTER_FRAME, run);
this.addEventListener(Event.ENTER_FRAME, keyboard_response);
stage.addEventListener(KeyboardEvent.KEY_DOWN, key_down);
stage.addEventListener(KeyboardEvent.KEY_UP, key_up);
stage.stageFocusRect 
= false;
stage.focus 
= scene;

 

注意:

当你在循环一个数组中所有对象时,你可能会遇到想要删除一个对象的情况(可能你需要把这个对象从这个数组删除,然后添加到另外一个数组中),那么在 这个删除的过程中你要非常的小心,因为数组在你执行删除操作后的长度会改变,那么你如果循环使用数组长度作为max的话,会造成掠过对现在在原删除对象所 在位置的对象的操作。

一种解决办法就是在循环之前定义一个变量然后再执行for循环。

var length = objects.numChildren;

目前为止,我们一直在讨论3D场景的设置。后面我会陆续的介绍如何使用代码实现3D物体的绘制,不过我想在那之前我们还是多看几个3D场景的例子加深你的印象。那么,请不要失去耐心,最终你所关心的内容这里一定会有介绍的。

点此下载本节源文件

作者:Yang Zhou
出处:http://yangzhou1030.cnblogs.com
感谢:Yunqing
本文版权归作者和博客园共有,转载未经作者同意必须保留此段声明。请在文章页面明显位置给出原文连接,作者保留追究法律责任的权利。

[UI] 改进电子商务网站易用性的10个方法

mikel阅读(1082)

    今天消费者网络购物的支出越来越多,可是太多的网站并没有考虑到他们网站和订购流程的易用性,最终导致用户很快就放弃了他们的订购。 这里有10种可以提高你的电子商务网站易用性的方法,可以帮助你最大可能的提高网站的转化率,将用户的"购物篮里的商品"转换为订单。

    1.  用email来标识用户(登陆名) 

       你曾经在网上购物中,用过多少不同的用户名作为账号? 又用过多少不同的emai作为账号?  我敢打赌, 你不仅不同的Email用的很少 ,而且你会发现Email比用户名,更容易记忆。 大多数情况下,用email标识用户比用户名更好。 因为Email容易记忆,更标准,这意味着你不必担心去记忆哪些乱七八糟的字符。他们永远是独一无二的,永远不会像用户名那样出现重名。

     

    2. 将下订单的过程划分成几个大步骤(让用户一次处理一个任务---海带博客

      订购流程通常非常复杂,比较典型的场景可能包括: 用户输入一个配送地址, 选择配送方式, 选择输入支付方式, 最后确认自己的订单。 试图让用户在一个步骤里做完这些事会有问题, 因为用户要输入太多的信息了!

    将这些步骤分解,让用户每次能处理一个步骤, 确保每个步骤需要思考和输入的信息相对较少。

     举个例子,亚马逊(Amazon.com)将订购流程分解成以下步骤:

 

    3.  告诉用户他们在哪儿,将要去那儿?

    (注: 关于网络用户迷失的位置感描述,请参考<Don't make me think>一书---海带博客

     在你的旅行中, 如果你不知道自己已经走了多远, 或者不知道自己还要走多远, 这难道不是一件糟糕的事情吗? 对用户来说, 当他们试图在线买点什么东西,如果他们不知道最终完成订购还需要多少步骤,他们会倍感挫折。 这就是为什么在订购过程中,告诉用户他在什么位置,而且还需要多少步骤, 是多么重要!

The four steps of the Dixon's ordering process: Delivery, Payment, Submit order, Confirmation

    举个例子, Dixons14 显示当前预订步骤,下一步要去的步骤。 作为一种选择,你可以只是在预订步骤中简单的设定步骤号码就可以了,比如: 输入配送地址(步骤1).

     

    4.不要让下订单过程不必要的复杂

    令人惊奇的是, 有太多的电子商务网站把他们的订单流程搞的无比复杂,超过他们实际需要的。据个例子, 用户经常被要求按照月份输入他们的信用卡或者借记卡的有效日期(注: 这里意思是必须输入英文的日期,Jan, Feb, March之类的---海带博客),而不是简单的01,02,03等等。 这就强制用户将他们卡片上显示的数字转换成单词,却无法直接输入数字。

    作为产品设计人员,对每个订购步骤都要仔细考虑,以使其更简单。 举个例子,预订过程中,所有的输入框都必须用户输入吗?(注: 如果不是必须的,请尽量简化。---海带博客) 。通过不断简化流线化订单流程,你可以将用户订购过程中的体验问题降低到最少。

     

    5. 告知用户通常会产生的疑问的答案

      在整个订购流程中,告诉用户通常会遇到的问题和疑问的解决办法是非常重要的。 举个例子,用户通常需要知道他的配送地址最多可允许多长 ?或者当用户需要输入生日的时候,告诉他为什么要收集他的生日信息等等。

    仔细走一遍预订流程并在每个步骤问问你自己: 用户可能会有什么疑问? 在屏幕上给予用户提示,并且提供一个超链接到详细的帮助。

Explanation that Marks & Spencers require users' contact numbers because they may need to contact them

    举个例子,Markts& Spencers 向用户解释了他们为什么需要输入联系人电话。

    6. 高亮必填项


    再也没有比填写完一个订单后,却因为没有填写必填项,不得不返回重新填写更令人恼火的事情了。在填写一个订单中, 必须非常清楚标识出哪些项目是必填的,哪些是可选的。 其实很简单,只需要在必填项前加一个“*”。

Use of the * to denote required fields

    dabs.com 这点做的非常好,可以参考。

    7. 让订购流程可变通。

    通过让流程可变通, 用户不仅会感到更多的控制, 而且也不太可能遇到关键性的问题。 据个例子, 一些电子商务网站,当用户输入地址的时候,自动帮助用户查找邮编。这对于哪些非正式的或者新的地址可能会有问题, 因为这种情况下不会为用户返回邮编。 这就意味着,用户不可能简单直接输入地址就可下订单(我自己就碰到好几次这种情况了)。

    一个好的例子,CD WOW! 已经将可变通放在了他们的订购流程中, 他们允许用户不用注册直接下订单。这就意味着那些对注册感到不适的用户,仍然可以下订单。

     

    8. 让用户感到放心,安全(注:电子商务产品设计的目标就是容易,安全---海带博客

     很多消费者在线购买东西时,仍然不是100%放心。 他们常常担心提交了信用卡账号,却收不到货。因此努力去缓解用户的担忧,让用户感到放心是非常重要的。仔细考虑用户在订购过程中的每一步可能碰到的疑虑,并对这些疑虑进行说明。

    例如Tesco 在他们的订购流程的支付步骤,向用户解释在Tesco购物是安全的。

     

Tesco's safe shopping guarantee mentions their secure server that encrypts credit card information

    9. 让用户下订单前再次确认

    订购的最后一个步骤通常会让用户确认他们的订单。 用户可以看到订单的清单,包括他们需要支付多少钱,什么时候货送到。他们也可以取消或者提交此订单。最好不要拷贝亚马逊的"Amazon's "1-click" ordering system", 因为这样会让用户在没有检查和确认订单细节情况下,直接提交订单。比如配送地址和送货的费用等等。(注:处理配送地址错误的订单,或者因为配送费用导致退货的问题处理,成本非常高。---海带博客)

    在订单提交后,应该显示订单的最终确认信息, 以便让用户知道他的订单是否成功。 订单最终确认信息应该包括:

        * The expected delivery date   期望的送货时间
        * The order number  订单号
        * How to track the order online (if this is possible)  如何在线跟踪订单状态(如果可能)

    10. 订单成功后,发一个确认邮件。

    一旦用户成功提交订单, 需要立刻给用户发送一个确认邮件。 依据Jakob Nielsen's Alertbox, December 8, 2003, 一个确认邮件应该:

    简明扼要

    告诉用户他们想要的, 比如: 订单号。

    应该象公司客服代表一样。

    记住,在线解决问题比致电客服中心成本低得多。在线提示用户,比如告诉用户配送地址应该多长等等,将会大幅度减少客户服务的电话数量。 

 

结论:

本文概述了改进电子商务网站的10个方法, 遵循这些原则不仅让你的电子商务网站更加可用,而且最终也更成功。 当然,通过这些原则,你只能走这么远,这就是为什么进行可用性测试是每个电子商务网站重要部分的原因。 遵循可用性原则并让真实用户进行可用性测试,你会发现你的电子商务网站不仅仅可用,而且更加有效。