[C#]蛙蛙推荐:简化基于数据库的DotNet应用程序开发

mikel阅读(826)

分析

  要做一个基于数据库的应用程序,我们有大量的重复劳动要去做,建表,写增删改查的SQL语句,写与数据库表对应的实体类,写执行SQLC#代码,写 添加、修改、列表、详细页面等等。这些活动都是围绕着一个个都数据表来开展的,在.NET领域有很多的OR Mapping的方案,但好多方案用起来好用,但原理很复杂,而且性能也不好把握,所以我们可以做一个轻型的ORM方案。有了ORM框架,根据数据表写 C#实体类这些劳动,其实也可以写一个代码生成器来帮我们生成,甚至代码生成器还能帮我们生成一些界面的代码。我们大概需要解决如下问题
1、我们要有一个通用的数据库操作帮助类,类似微软的DAAB,但最好能支持多种数据库;
2、我们要有一个使用简单的orm框架,能方便的用C#代码来进行数据库存取操作,而且要尽量保证性能,比如使用参数化查询;
3、我们要有一个代码生成器帮助我们解决一些重复性劳动,比如生成实体类,生成调用存储过程的c#代码等;

围绕这3个问题,我们一一来展开

一、通用的数据库吃操作帮助类

  ADO.NET 2.0为我们访问数据库提供了一套与具体数据库无关的模型,其核心类是DbProviderFactory,它遵循了Provider模式,就是把对各种 数据库的操作抽象出一个Provider,再由各种数据库去写与具体数据库相关的Provider,然后通过配置在运行时方便的切换数据库,而尽量少的不 修改业务逻辑层的代码,业务逻辑层依赖的是抽象的Provider。这也是典型的依赖倒置,就是说业务逻辑说我需要哪些接口,我依赖这些接口,而让别人去 实现这些接口,在运行的时候再去加载调用实现这些接口的具体类。
  为了提高性能,减少SQLServer执行计划的重编译,我们尽量使用参数化的查询,而一个固定的语句或者存储过程它的ADO.NET参数是固定的, 所以我们可以把这些参数缓存起来,避免每次执行SQL语句都创新新的参数对象。另外oledb的ado.net provider的参数是不能命名的,所以给参数赋值要按顺序赋值。

  为了使用方便,我们为执行SQL语句提供如下的API

public System.Data.DataSet SqlExecuteDateSet(string sql, string[] paramters, params object[] values)
public System.Data.DataTable SqlExecuteDateTable(string sql, string[] paramters, params object[] values)
public int SqlExecuteNonQuery(string sql, string[] paramters, params object[] values)
public System.Data.Common.DbDataReader SqlExecuteReader(string sql, string[] paramters, params object[] values)
public object SqlExecuteScalar(string sql, string[] paramters, params object[] values)

  当然,为了支持存储过程的执行,以及数据库事务,还需要提供相关的重载的API。大概的使用示例(面向SQLServer)如下:

DbHelper dbhelper = new DbHelper();
string sql = "delete from Citys where CityId = @id";
using (DatabaseTrans trans = new DatabaseTrans(dbhelper))
{
    
try
    {
        dbhelper.SqlExecuteNonQuery(trans, sql, 
new string[] { "@id" }, 1);
        dbhelper.SqlExecuteNonQuery(trans, sql, 
new string[] { "@id" }, 2);
        trans.Commit();
        OutPut(
"ok");
    }
    
catch (Exception)
    {
        trans.RollBack();
        OutPut(
"no ok");
    }
}

 

二、通用的ORM框架

先看如下的代码

 

//1、添加
xxxCase xxxCase = new xxxCase();
xxxCase.Title 
= "abc";
xxxCase.Content 
= "呵呵";
xxxCase.CaseFrom 
= CaseFrom.客服投诉;
xxxCase.PostUser 
= "huhao";
xxxCase.CreateTime 
= DateTime.Now;
xxxCase.CaseType 
= CaseType.生产环境查询;
xxxCase.Priority 
= CasePriority.中;
xxxCase.ReleationServices 
= "aaa,bbb";
xxxCase.ReleationClient 
= "ccc,ddd";
EntityBase.Insert(xxxCase);
//2、修改
xxxCase.ClearInnerData();
xxxCase.CaseId 
= 1;
xxxCase.Title 
= "嘿嘿";
EntityBase.Update(xxxCase);
//3、删除
xxxCase.ClearInnerData();
xxxCase.CaseId 
= 1;
EntityBase.Delete(xxxCase);
//4、复杂条件查询,查询大于昨天的客服投诉或者wawa关闭的问题
WhereCondition condition = new WhereCondition(
    xxxCase.CaseFromColName,SqlOperator.Equal, (
short)CaseFrom.客服投诉)
    .And(
    
new WhereCondition(xxxCase.CreateTimeColName, SqlOperator.GreaterThan ,
        DateTime.Now.AddDays(
1)))
    .Group()
    .Or(
    
new WhereCondition(xxxCase.CloseUserColName, SqlOperator.Equal, "wawa"));
IList
<xxxCase> list = EntityBase.Select<xxxCase>(
    
new string[] {"Title""PostUser"}, condition);
foreach (xxxCase item in list)
{
    Console.WriteLine(
"{0}-{1}",item.Title,item.PostUser);
}
Console.ReadKey();

  上面的代码是以面向对象(请忽略那些关于贫血模型的讨论,说上面的代码不够OO,上面的代码至少相对的面向对象,而且看起来很直观)的方式去执 行一些业务,这应该比到处写SQL语句要强很多吧,而且如果这些操作内部使用的仍然是参数化查询而不是拼sql字符串的话,性能也不会很差(请忽略具体语 句是否能使用索引的讨论,那得具体分析)。

  我们看一下EntityBase.Insert方法的实现,逻辑很简单明了,其他的Update,Delete,Select也是类似的思路。

 

private static DbHelper _db = new DbHelper();
public static void Insert(EntityBase entity) {
    
string sql = GetInsertSql(entity);
    
string[] parameters = GetParameters(entity.InnerData);
    
object[] parameterValues = GetParameterValuess(entity.InnerData);
    _db.SqlExecuteNonQuery(sql, parameters, parameterValues);
}
private static string GetInsertSql(EntityBase entity) {
    
int len = entity.InnerData.Count;
    StringBuilder sql 
= new StringBuilder();
    sql.AppendFormat(
"Insert INTO [{0}]\r\n", entity.TableName);
    sql.Append(
"(\r\n");
    
for (int i = 0; i < len; i++) {
        
if (i != len  1)
            sql.AppendFormat(
"[{0}],", entity.InnerData[i].Key);
        
else
            sql.AppendFormat(
"[{0}]", entity.InnerData[i].Key);
    }
    sql.Append(
")\r\n");
    sql.Append(
"VALUES(\r\n");
    
for (int i = 0; i < len; i++) {
        
if (i != len  1)
            sql.AppendFormat(
"@{0},", entity.InnerData[i].Key);
        
else
            sql.AppendFormat(
"@{0}", entity.InnerData[i].Key);
    }
    sql.Append(
")\r\n");
    
return sql.ToString();
}
private static string[] GetParameters(IList<DbCommonClass<stringobject>> items) {
    
int len = items.Count;
    List
<string> parameters = new List<string>();
    
for (int i = 0; i < len; i++) {
        parameters.Add(
string.Format("@{0}", items[i].Key));
    }
    
return parameters.ToArray();
}
private static object[] GetParameterValuess(List<DbCommonClass<stringobject>> items) {
    
int len = items.Count;
    List
<object> parameters = new List<object>();
    
for (int i = 0; i < len; i++) {
        parameters.Add(items[i].Value);
    }
    
return parameters.ToArray();
}

当然Select方法稍微复杂一些,因为我们要考虑复杂的Where字句,Top字句,OrderBy字句等,我们为Where字句建立了一个 WhereCondition对象,来方便的用c#代码来描述SQL的where语句,但是为了实现简单,我们不去实现表连接,复杂的子语句等支持(我个 人认为向NBear等框架做的过于强大了)。

三、代码生成器

  ADO.NET的各种数据库实现都有获取某个数据库Schema的API,其中最重要的是 SqlConnection.GetSchema(SqlClientMetaDataCollectionNames.Tables)和 SqlCommand.ExecuteReader( CommandBehavior.KeyInfo | CommandBehavior.CloseConnection)方法,有了这两个方法,我们可以枚举一个数据库的所有表,及某个表的所有字段,及每个 字段的类型,长度、可否为空,是否为主键,是否为标识列等信息,有了这些元数据,我们再根据一个模板就可以生成特定格式的代码了。而且我们需要新增加一种 代码生成的格式的话,只需添加一个模板就可以了,这样的代码生成器还有扩展性,而不是一个写死的针对特定框架的代码生成器。
  为了脱离对特定数据库的依赖,我们建立一个代码生成器的元数据模型,如下

public class CodeModel
{
 
public string ClassName;
 
public string TableName;
 
public string Descript;
 
public string Namespace;
 
public string PkColName;
 
public List<CodeProperty> Properties;
}
public class CodeProperty
{
 
public string DbColName;
 
public int? DbLength;
 
public bool DbAllowNull
 
public SqlDbType DbType;
 
public string DbTypeStr;
 
public bool DbIsIdentity;
 
public bool DbIsPk;
 
 
public string Descript;
 
public string PropertyName;
 
public System.Type CSharpType;
 
public string CSharpTypeStr;
 
 
public bool UiAllowEmpty;
 
public bool UiIsShowOn;
 
public long? UiMaxCheck;
 
public long? UiMinCheck;
 
public string UiRegxCheck;
}

得到元数据后,剩下的就是读取模板,然后替换字符串了,比如实体类的模板,如下

using System;
using System.Collections.Generic;
using WawaSoft.Common;
namespace $model.namespace$ {
    
public class $model.classname$ : EntityBase {
$
foreach.prop$
        
public const string $prop.property$ColName = "$prop.dbcolname$";
$endforeach$    
        
private static readonly List<string> _Cols = new List<string>();
        
static $model.classname$()
        {            
$
foreach.prop$
            _Cols.Add($prop.property$ColName);
$endforeach$            
        }
        
public $model.classname$() {
            _tableName 
= "$model.tablename$";
            _PkName 
= "$model.pkcolname$";            
        }
$
foreach.prop$
        
private $prop.csharptype$ $prop.property2$;
$endforeach$
 
$
foreach.prop$
        
public $prop.csharptype$ $prop.property$ {
            
get { return $prop.property2$; }
            
set {
                $prop.property2$ 
= value;
                AddInnerData(
"$prop.property2$", value);
            }
        }
$endforeach$
        
protected override IList<string> Cols
        {
            
get { return _Cols; }
        }
        
public override void ConvertToEntity(IEnumerable<DbCommonClass<stringobject>> items) {
            
foreach (DbCommonClass<stringobject> item in items) {
                
switch (item.Key) {
$
foreach.prop$
                    
case $prop.property$ColName:
                        $prop.property2$ 
= ($prop.csharptype$)item.Value;
                        
break;
$endforeach$
                }
            }
        }
    }
}

生成的实体类,如下

using System;
using System.Collections.Generic;
using WawaSoft.Common;
namespace Entities {
    
public class User : EntityBase {
        
public const string UserIdColName = "UserId";
        
public const string UsernameColName = "Username";
        
public const string NameColName = "Name";
        
public const string PasswordColName = "Password";
        
public const string CreateTimeColName = "CreateTime";
        
public const string IsAdminColName = "IsAdmin";
        
private static readonly List<string> _Cols = new List<string>();
        
static User() {
            _Cols.Add(UserIdColName);
            _Cols.Add(UsernameColName);
            _Cols.Add(NameColName);
            _Cols.Add(PasswordColName);
            _Cols.Add(CreateTimeColName);
            _Cols.Add(IsAdminColName);
        }
        
public User() {
            _tableName 
= "User";
            _PkName 
= "UserId";
        }
        
private Nullable<Int32> userid;
        
private String username;
        
private String name;
        
private String password;
        
private Nullable<DateTime> createtime;
        
private Nullable<Boolean> isadmin;
        
public Nullable<Int32> UserId {
            
get { return userid; }
            
set {
                userid 
= value;
                AddInnerData(
"userid", value);
            }
        }
        
public String Username {
            
get { return username; }
            
set {
                username 
= value;
                AddInnerData(
"username", value);
            }
        }
        
public String Name {
            
get { return name; }
            
set {
                name 
= value;
                AddInnerData(
"name", value);
            }
        }
        
public String Password {
            
get { return password; }
            
set {
                password 
= value;
                AddInnerData(
"password", value);
            }
        }
        
public Nullable<DateTime> CreateTime {
            
get { return createtime; }
            
set {
                createtime 
= value;
                AddInnerData(
"createtime", value);
            }
        }
        
public Nullable<Boolean> IsAdmin {
            
get { return isadmin; }
            
set {
                isadmin 
= value;
                AddInnerData(
"isadmin", value);
            }
        }
        
protected override IList<string> Cols {
            
get { return _Cols; }
        }
        
public override void ConvertToEntity(IEnumerable<DbCommonClass<stringobject>> items) {
            
foreach (DbCommonClass<stringobject> item in items) {
                
switch (item.Key) {
                    
case UserIdColName:
                        userid 
= (Nullable<Int32>)item.Value;
                        
break;
                    
case UsernameColName:
                        username 
= (String)item.Value;
                        
break;
                    
case NameColName:
                        name 
= (String)item.Value;
                        
break;
                    
case PasswordColName:
                        password 
= (String)item.Value;
                        
break;
                    
case CreateTimeColName:
                        
if (item.Value != DBNull.Value)
                            createtime 
= (Nullable<DateTime>)item.Value;
                        
break;
                    
case IsAdminColName:
                        
if (item.Value != DBNull.Value)
                            isadmin 
= (Nullable<Boolean>)item.Value;
                        
break;
                }
            }
        }
    }
}

小结

解决了以上几个问题,再开发数据库应用,应该会提高不少效率。
相关代码下载:code_wawa.zip

[C#]生成器(抽象工厂加存储过程)

mikel阅读(733)

首先建立一个Exam解决方案

在添加下面的类库

打开生成器

填写好后  安生成(注意  是多线程的   速度很快)

 

点  打开文件夹  里面生成的文件夹对应下面的文件 复制到项目里面就可以了

我时间有限  只写了 增 、删、改、查。  自己可以扩展  注释都写好了的

下面是生成的存储过程

下载地址:/Files/Evans/生成器r.rar

[C#]无缝的缓存读取:双存储缓存策略

mikel阅读(809)

最近在做一个WEB的数据统计的优化,但是由于数据量大,执行一次SQL统计要比较长的时间(一般700ms算是正常)。

正常的做法只要加个缓存就好了。

但是同时业务要求此数据最多1分钟就要更新,而且这一分种内数据可能会有较多变化(而且原系统不太易扩展)。

也就是说缓存1分钟就要失效重新统计,而且用户访问这页还很是频繁,如果使用一般缓存那么用户体验很差而且很容易造成超时。

 

看到以上需求,第一个进入我大脑的就是从前做游戏时接触到的DDraw的双缓冲显示方式。

image

在第一帧显示的同时,正在计算第二帧,这样读取和计算就可以分开了,也就避免了读取时计算,提高了用户体验。

我想当然我们也可以将这种方式用于缓存的策略中,但这样用空间换取时间的方式还是得权衡的,因为并不是所有时候都值得这么做,但这里我觉得这样做应该是最好的方式了。

注:为了可以好好演示,本篇中的缓存都以IEnumerable的形式来存储,当然这个文中原理也可以应用在WebCache中。

这里我使用以下数据结构做为存储单元:

namespace CHCache {
/// <summary>
/// 缓存介质
/// </summary>
public class Medium {
/// <summary>
/// 主要存储介质
/// </summary>
public object Primary { get; set; }
/// <summary>
/// 次要存储介质
/// </summary>
public object Secondary { get; set; }
/// <summary>
/// 是否正在使用主要存储
/// </summary>
public bool IsPrimary { get; set; }
/// <summary>
/// 是否正在更新
/// </summary>
public bool IsUpdating { get; set; }
/// <summary>
/// 是否更新完成
/// </summary>
public bool IsUpdated { get; set; }
}
}
   有了这个数据结构我们就可以将数据实现两份存储。再利用一些读写策略就可以实现上面我们讲的缓存方式。

整个的缓存我们使用如下缓存类来控制:

/*
* http://www.cnblogs.com/chsword/
* chsword
* Date: 2009-3-31
* Time: 17:00
*
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
namespace CHCache {
/// <summary>
/// 双存储的类
/// </summary>
public class DictionaryCache : IEnumerable {
/// <summary>
/// 在此缓存构造时初始化字典对象
/// </summary>
public DictionaryCache()
{
Store = new Dictionary<string, Medium>();
}
public void Add(string key,Func<object> func)
{
if (Store.ContainsKey(key)) {//修改,如果已经存在,再次添加时则采用其它线程
var elem = Store[key];
if (elem.IsUpdating)return;  //正在写入未命中
var th = new ThreadHelper(elem, func);//ThreadHelper将在下文提及,是向其它线程传参用的
var td = new Thread(th.Doit);
td.Start();
}
else {//首次添加时可能也要读取,所以要本线程执行
Console.WriteLine("Begin first write");
Store.Add(key, new Medium {IsPrimary = true, Primary =  func()});
Console.WriteLine("End first write");
}
}
/// <summary>
/// 读取时所用的索引
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public object this[string key] {
get {
if (!Store.ContainsKey(key))return null;
var elem = Store[key];
if (elem.IsUpdated) {//如果其它线程更新完毕,则将主次转置
elem.IsUpdated = false;
elem.IsPrimary = !elem.IsPrimary;
}
var ret = elem.IsPrimary ? elem.Primary : elem.Secondary;
var b = elem.IsPrimary ? " from 1" : " form 2";
return ret + b;
}
}
Dictionary<string, Medium> Store { get; set; }
public IEnumerator GetEnumerator() {
return ((IEnumerable)Store).GetEnumerator();
}
}
}

这里我只实现了插入一个缓存,以及读取的方法。

我读取缓存单元的逻辑是这样的

image 

从2个不同缓存读取当然是很容易了,但是比较复杂的就是向缓存写入的过程:

image

这里读取数据以及写入缓存时我使用了一个委托,在其它线程中仅在需要执行时才会执行。

这里除了首次写入缓存占用主线程时间(读取要等待)以外,其它时间都可以无延时的读取,实现了无缝的缓存。

但我们在委托中要操作缓存的元素Medium,所以要传递参数进其它线程,所以我这里使用了一个辅助类来传递参数进入其它线程:

using System;
namespace CHCache {
/// <summary>
/// 一个线程Helper,用于帮助多抛出线程时传递参数
/// </summary>
public class ThreadHelper {
Func<object> Fun { get; set; }
Medium Medium { get; set; }
/// <summary>
/// 通过构造函数来传递参数
/// </summary>
/// <param name="m">缓存单元</param>
/// <param name="fun">读取数据的委托</param>
public ThreadHelper(Medium m,Func<object> fun) {
Medium = m;
Fun = fun;
}
/// <summary>
/// 线程入口,ThreadStart委托所对应的方法
/// </summary>
public void Doit()
{
Medium.IsUpdating = true;
if (Medium.IsPrimary) {
Console.WriteLine("Begin write to 2.");
var ret = Fun.Invoke();
Medium.Secondary = ret;
Console.WriteLine("End write to 2.");
}
else {
Console.WriteLine("Begin write to 1.");
var ret = Fun.Invoke();
Medium.Primary = ret;
Console.WriteLine("End write to 1.");
}
Medium.IsUpdated = true;
Medium.IsUpdating = false;
}
}
}

这样我们就实现了在另个线程读取数据的过程,这样就在任何时候读取数据时都会无延时直接读取了。

最后我们写一个主函数来测试一下效果

/*
* http://www.cnblogs.com/chsword/
* chsword
* Date: 2009-3-31
* Time: 16:53
*/
using System;
using System.Threading;
namespace CHCache
{
class Program
{
public static void Main(string[] args)
{
var cache = new DictionaryCache();
Console.WriteLine("Init...4s,you can press the CTRL+C to close the console window.");
while (true)
{
cache.Add("1", GetValue);
Thread.Sleep(1000);
Console.WriteLine(cache["1"]);
}
}
/// <summary>
/// 获取数据的方法,假设是从数据库读取的,费时约4秒
/// </summary>
/// <returns></returns>
static object GetValue()
{
Thread.Sleep(4000);
return DateTime.Now;
}
}
}

得到如下数据:

image

这样就实现了平滑的读取缓存数据而没有任何等待时间

当然这里还有些问题,比如说传递不同参数时的解决方法,但是由于我仅是在一个统计时需要这种缓存提高性能,所以暂没有考虑通用的传参方式。

如果大家对这个话题感兴趣,欢迎讨论。

源码下载:点击下载

[C#]ASP.NET MVC 1.0浅析

mikel阅读(784)

最近的项目没有升级到ASP.NET MVC 1.0,也没有评论的资格,没详细看过MVC1.0的源码,据说改动挺大,看来需要升级的工作又艰巨了不少,以下转载自:

http://www.cnblogs.com/andy1027/archive/2009/04/01/1427369.html

为什么要用ASP.NET MVC 1.0?当我刚知道1.0发布的时候,经常这样问。

最近正在考虑是否在我们的企业级应用中使用ASP.NET MVC 1.0框架,因此会一直找使用它的理由,希望大家在关注技术的同时,结合企业应用谈谈自己的看法。

1、MVC的组成
Models
:访问数据库,装载数据、处理业务逻辑。在项目中体现为数据实体类加业务代理类。
Views:显示数据,用户界面。在项目中体现为aspx页面,偶尔可以加上code-behind。
Controller:按路由规则将请求的数据传送给指定页面,用于显示;也可以把用户输入的数据传递给逻辑处理类。它可以包含简单的验证逻辑
。不应包含数据访问逻辑。
2、为何使用MVC
提出MVC的目的无非是提高开发效率、提高可测试性。
官方的ASP.NET MVC 1.0指南中指出(以下简称指南),基于MVC的Web应用程序有如下优点:
[1]对复杂的程序管理更方便
It makes it easier to manage complexity by dividing an application into the model, the view, and the ontroller.

[2]在开发上有更高的可控性
It does not use view state or server-based forms. This makes the MVC framework ideal for developers who want full control
over the behavior of an application.
[3]Routing使软件设计有更多灵活性
It uses a Front Controller pattern that processes Web application requests through a single controller. This enables you to
design an application that supports a rich routing infrastructure.
[4]更加适合测试驱动开发
It provides better support for test-driven development (TDD).
[5]团队开发项目中有更高的可控性
It works well for Web applications that are supported by large teams of developers and Web designers who need a high degree
of control over the application behavior.
同时MVC框架还有以下特点:
[1]将应用程序分成各个组成部份,更有利于测试。MVC框架是基于接口的,这样可以利用MOCK方式来替换你的实际类;做单元测试的时候,也
可以不运行Contrllers,这样的测试就更快更灵活。
[2]MVC框架是可扩展的,你可以自己设计并替换视频引擎、URL导向规则、Action的参数序列等等。同时MVC框架也支持依赖注入和控制反转,
你可以从外部注入实例,而不用让类自己创建实例,你还可以通过配置文件的方式创建实例,这样使得测试更方便。
[3]强大的URL映射组件使得你的应用程序的URL更易理解,同时具备搜索能力。你的URL不必包括文件路径,这样的设计很适合自定义查询引擎
和REST架构。
[4]MVC框架仍然支持ASP.NET中的页面、用户控件、母版页作为视图的模板;同时你也还可以使用嵌套母版页、行内表达式(in-line
expressions<%= %>)、服务器端控件、模板、数据绑定、本地化等等属于ASP.NET已有的东西。
[5]同时ASP.NET中的FORM验证、Windows验证、URL授权、Membership、角色、输出、数据缓存、Session、Profile 状态管理、配置、Provider
框架等特性在MVC框架中仍然是可用的。

小结:ASP.NET MVC 1.0框架是基于ASP.NET的,所以他包括了ASP.NET中的几乎所有特性。同时他为设计人员提供了一套测试的方案(当然这是所有语言平台MVC模式的共性)。在安装了框架的VS2008中还增加了不少功能,可以方便地添加Views、Models、Controllers。
3、与三层结构的ASP.NET应用程序比较
与普通ASP.NET比较而言,最大的区别还是在于前台开发,后台包括的数据库访问、逻辑处理与以往的方式没有明显区别,在MVC框架中,这些
统称为Model。而三层结构中,这些可以称为数据访问层与逻辑处理层。
[1]页面开发
用这种模式开发的站点,光看页面的代码的确比以往少一些,但它更多地使用了页面脚本(<% … %>)用于显示数据。在指南中并未提到不推
荐使用服务器端控件,但是它提供了大量的HTML HELPER,而且还允许你自己添加Helper,比如DataGridHelper,所以在MVC框架中使用这些Helper会更方便些,不过这对于熟练工来说应该影响不大,因为实际开发中我们更多使用的是Ctrl+C/Ctrl+V,复制几个标签和复制几个Helper方法所花的时间差不多。可能对于新手来说,如果对标签不熟悉的话,用这些Helper的速度会快些,但是这样会影响新手掌握标签,真是矛盾呐
[2]数据提交
普通的ASP.Net开发,在提交数据的时候可能还需要通过设置数据绑定,或者在code-behind里写封装代码;而在MVC中,框架自动帮助你将页面
上填写的数据封装到事先指定的Model中,数据提交操作在MVC框架挺方便。而且在普通ASP.NET页面中,经常会出现某个属性无法绑回去的情况,这点在MVC中应该可以得到解决。指南中提到了Routing的使用使得MVC框架下的应用程序在操作自定义查询时变得更方便,实际上在查询方面跟普通方式并没有多大区别,都是对封装好的类进行解析。至于“URL更容易理解”,现在应用程序都是从界面上点击来实现操作,很少有人会关注URL本身吧,所以这个优点不算优点。
[3]单元测试
从测试上讲,MVC框架确实做得不错,若用MOCK方式测试可以更方便,一个好的WEB应用程序设计就应该将页面呈现与逻辑分开,这点普通
ASP.NET应用程序也是可以做到的,关键在于设计。
[4]其它
MVC框架在验证、母版页这些地方有几个新特性,但与普通ASP.NET的方式大同小异,因此不仔细说了。

文中分析不对的地方,请指正。

[JQuery]JQuery ajax批量上传图片

mikel阅读(962)

在网上搜索了一下,发现以JQuery+ajax方式实现单张图片上传的代码是有的,但实现批量上传图片的程序却没搜索到,于是根据搜索到的代码,写了一个可以批量上传的。

         先看效果图

点击增加按钮,会增加一个选择框,如下图:

 

选择要上传的图片,效果图如下:

 

上传成功如下图:

 

 

 

下面来看代码:

前台html主要代码:

<button id="SubUpload" class="ManagerButton" onClick="TSubmitUploadImageFile();return false;">确定上传</button>&nbsp;&nbsp;

<button id="CancelUpload" class="ManagerButton" onClick="JavaScript:history.go(-1);">取消</button>&nbsp;&nbsp;

<button id="AddUpload" class="ManagerButton" onClick="TAddFileUpload();return false;">增加</button>

<tr><td class="tdClass">

         图片1

         </td><td class="tdClass">

         <input name="" size="60" id="uploadImg1" type="file" />

         <span id="uploadImgState1"></span>

         </td></tr>

 

 

 

 

 

 

 

 

 

 

因为用了JQuery,所以你完全可以把click事件放在js文件中

增加按钮js代码:

 

var TfileUploadNum=1; //记录图片选择框个数

var Tnum=1; //ajax上传图片时索引

         function TAddFileUpload()

         {

                   var idnum = TfileUploadNum+1;

                   var str="<tr><td class='tdClass'>图片"+idnum+"</td>";

                   str += "<td class='tdClass'><input name='' size='60' id='uploadImg"+idnum+"' type='file' /><span id='uploadImgState"+idnum+"'>";

                   str += "</span></td></tr>";

                   $("#imgTable").append(str);

                   TfileUploadNum += 1;

         }

 

 

 

 

 

 

 

 

 

 

确定上传按钮js代码:

 

function TSubmitUploadImageFile()

         {

                  M("SubUpload").disabled=true;

                   M("CancelUpload").disabled=true;

                   M("AddUpload").disabled=true;

                   setTimeout("TajaxFileUpload()",1000);//此为关键代码

}

 

 

 

 

 

 

 

 

关于setTimeout("TajaxFileUpload()",1000);这句代码:因为所谓的批量上传,其实还是一个一个的上传,给用户的只是一个假象。只所以要延时执行TajaxFileUpload(),是因为在把图片上传到服务器上时,我在后台给图片重新命名了,命名的规则是,如下代码:

 

 

Random rd = new Random();

StringBuilder serial = new StringBuilder();

serial.Append(DateTime.Now.ToString("yyyyMMddHHmmssff"));

serial.Append(rd.Next(0, 999999).ToString());

return serial.ToString();

 

 

 

 

 

即使我命名精确到毫秒,另外再加上随机数,可是还是有上传的第二张图片把上传的第一张图片覆盖的情况出现。所以此处我设置了延时1秒后在上传下一张图片。刚开始做这个东西的时候,用的是for循环,来把所有的图片一个一个的循环地用ajax上传,可是for循环速度太快了,可能第一张图片还没来得及ajax,第二张就被for过来了,还是有第二张覆盖第一张的情况出现。

下面来看TajaxFileUpload()函数,代码如下:

 

function TajaxFileUpload()

         {

                   if(Tnum<TfileUploadNum+1)

                   {

                            //准备提交处理

                            $("#uploadImgState"+Tnum).html("<img src=../images/loading.gif />");

                            //开始提交

                            $.ajax

                            ({

                                     type: "POST",

                                     url:"http://localhost/ajaxText2/Handler1.ashx",

                                     data:{upfile:$("#uploadImg"+Tnum).val(),category:$("#pcategory").val()},

                                     success:function (data, status)

                                     {

                                               //alert(data);

                                               var stringArray = data.split("|");

                                              

                                               if(stringArray[0]=="1")

                                               {

                                                        //stringArray[0]    成功状态(1为成功,0为失败)

                                                        //stringArray[1]    上传成功的文件名

                                                        //stringArray[2]    消息提示

                                                        $("#uploadImgState"+Tnum).html("<img src=../images/note_ok.gif />");//+stringArray[1]+"|"+stringArray[2]);

                                               }           

                                               else

                                               {

                                                        //上传出错

                                                        $("#uploadImgState"+Tnum).html("<img src=../images/note_error.gif />"+stringArray[2]);//+stringArray[2]+"");

                                               }

                                               Tnum++;

                                             setTimeout("TSubmitUploadImageFile()",0);

                                      }

                             });                     

                   }

         }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 上面的代码没什么可说的,很容易看懂。下面来看Handler1.ashx(一般处理程序)如何来处理post过来的图片的(此代码来自网上,具体地址忘记了),下面只给出关键代码,全部代码在附件里。

1

  

string _fileNamePath = "";

            try

            {

                _fileNamePath = context.Request.Form["upfile"];

                //开始上传

                string _savedFileResult = UpLoadFile(_fileNamePath);

                context.Response.Write(_savedFileResult);

            }

            catch

            {

                context.Response.Write("0|error|上传提交出错");

            }

 

 

 

 

 

 

 

 

 

 

 

 

2

 

//生成将要保存的随机文件名

string fileName = GetFileName() + fileNameExt;

//物理完整路径                   

string toFileFullPath = HttpContext.Current.Server.MapPath(toFilePath);

//检查是否有该路径 没有就创建

if (!Directory.Exists(toFileFullPath))

{

     Directory.CreateDirectory(toFileFullPath);

}

///创建WebClient实例      

WebClient myWebClient = new WebClient();

//设定windows网络安全认证   方法1

myWebClient.Credentials = CredentialCache.DefaultCredentials;

//要上传的文件      

FileStream fs = new FileStream(fileNamePath, FileMode.Open, FileAccess.Read);

//FileStream fs = OpenFile();      

BinaryReader r = new BinaryReader(fs);

//使用UploadFile方法可以用下面的格式      

//myWebClient.UploadFile(toFile, "PUT",fileNamePath);      

byte[] postArray = r.ReadBytes((int)fs.Length);

Stream postStream = myWebClient.OpenWrite(toFile, "PUT");

if (postStream.CanWrite)

{

postStream.Write(postArray, 0, postArray.Length);

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

3、检查是否合法的上传文件

 

private bool CheckFileExt(string _fileExt)

{

     string[] allowExt = new string[] { ".gif", ".jpg", ".jpeg" };

     for (int i = 0; i < allowExt.Length; i++)

     {

         if (allowExt[i] == _fileExt) { return true; }

     }

    return false;

}

4、生成要保存的随即文件名

 

 

 

 

 

 

 

 

  

public static string GetFileName()

{

            Random rd = new Random();

            StringBuilder serial = new StringBuilder();

            serial.Append(DateTime.Now.ToString("yyyyMMddHHmmssff"));

            serial.Append(rd.Next(0, 999999).ToString());

            return serial.ToString();

}

 

 

 

 

 

 

 

Ok,基本上这个批量上传图片的jQuery+ajax方式实现的程序完成了。如果你要上传word文档,pdf文件,只要稍作修改,就可以实现了。

[JQuery]jQuery Calculation Plug-in

mikel阅读(772)

JQuery Calculation Plug-in (v0.4.04)

The Calculation plug-in is designed to give easy-to-use JQuery functions for commonly used mathematical functions.

This plug-in will work on all types of HTML elements—which means you can use it to calculate values in <td> elements or in <input> elements. You can even mix and match between element types.

Numbers are parsed from the element using parseNumber() method—which uses a regular expression (/-?\d+(,\d{3})*(\.\d{1,})?/g) to parse out the numeric value. You can change the regular expression that's used to determine what's consider a number by changing the default regular expression.

Download

Download the plug-in:
jquery.calculation.js
jquery.calculation.min.js

[DBMS]漫谈数据库索引

mikel阅读(915)

一、引言

对数据库索引的关注从未淡出我的们的讨论,那么数据库索引是什么样的?聚集索引与非聚集索引有什么不同?希望本文对各位同仁有一定的帮助。有不少存疑的地方,诚心希望各位不吝赐教指正,共同进步。[最近首页之争沸沸扬扬,也不知道这个放在这合适么,苦劳?功劳?……]

 

二、B-Tree

我们常见的数据库系统,其索引使用的数据结构多是B-Tree或者B+Tree。例如,MsSQL使用的是B+TreeOracleSysbase使用的是B-Tree。所以在最开始,简单地介绍一下B-Tree

B-Tree不同于Binary Tree(二叉树,最多有两个子树),一棵M阶的B-Tree满足以下条件:
1)每个结点至多有M个孩子;
2)除根结点和叶结点外,其它每个结点至少有M/2个孩子;
3)根结点至少有两个孩子(除非该树仅包含一个结点);
4)所有叶结点在同一层,叶结点不包含任何关键字信息;
5)有K个关键字的非叶结点恰好包含K+1个孩子;

另外,对于一个结点,其内部的关键字是从小到大排序的。以下是B-TreeM=4)的样例:

  

对于每个结点,主要包含一个关键字数组Key[],一个指针数组(指向儿子)Son[]。在B-Tree内,查找的流程是:使用顺序查找(数组长度较短时)或折半查找方法查找Key[]数组,若找到关键字K,则返回该结点的地址及KKey[]中的位置;否则,可确定K在某个Key[i]Key[i+1]之间,则从Son[i]所指的子结点继续查找,直到在某结点中查找成功;或直至找到叶结点且叶结点中的查找仍不成功时,查找过程失败。

接着,我们使用以下图片演示如何生成B-TreeM=4,依次插入1~6):
从图可见,当我们插入关键字4时,由于原结点已经满了,故进行分裂,基本按一半的原则进行分裂,然后取出中间的关键字2,升级(这里是成为根结点)。其它的依类推,就是这样一个大概的过程。

  

 

三、数据库索引

1.什么是索引

在数据库中,索引的含义与日常意义上的“索引”一词并无多大区别(想想小时候查字典),它是用于提高数据库表数据访问速度的数据库对象。
A)索引可以避免全表扫描。多数查询可以仅扫描少量索引页及数据页,而不是遍历所有数据页。
B对于非聚集索引,有些查询甚至可以不访问数据页。
C聚集索引可以避免数据插入操作集中于表的最后一个数据页。
D一些情况下,索引还可用于避免排序操作。

当然,众所周知,虽然索引可以提高查询速度,但是它们也会导致数据库系统更新数据的性能下降,因为大部分数据更新需要同时更新索引。

 

2.索引的存储

一条索引记录中包含的基本信息包括:键值(即你定义索引时指定的所有字段的值)+逻辑指针(指向数据页或者另一索引页)。

  

当你为一张空表创建索引时,数据库系统将为你分配一个索引页,该索引页在你插入数据前一直是空的。此页此时既是根结点,也是叶结点。每当你往表中插入一行数据,数据库系统即向此根结点中插入一行索引记录。当根结点满时,数据库系统大抵按以下步骤进行分裂:
A)创建两个儿子结点
B)将原根结点中的数据近似地拆成两半,分别写入新的两个儿子结点
C)根结点中加上指向两个儿子结点的指针

通常状况下,由于索引记录仅包含索引字段值(以及4-9字节的指针),索引实体比真实的数据行要小许多,索引页相较数据页来说要密集许多。一个索引页可以存储数量更多的索引记录,这意味着在索引中查找时在I/O上占很大的优势,理解这一点有助于从本质上了解使用索引的优势。

 

3.索引的类型

A聚集索引,表数据按照索引的顺序来存储的。对于聚集索引,叶子结点即存储了真实的数据行,不再有另外单独的数据页。
B非聚集索引,表数据存储顺序与索引顺序无关。对于非聚集索引,叶结点包含索引字段值及指向数据页数据行的逻辑指针,该层紧邻数据页,其行数量与数据表行数据量一致。

在一张表上只能创建一个聚集索引,因为真实数据的物理顺序只可能是一种。如果一张表没有聚集索引,那么它被称为堆集Heap)。这样的表中的数据行没有特定的顺序,所有的新行将被添加的表的末尾位置。

 

4.聚集索引

在聚集索引中,叶结点也即数据结点,所有数据行的存储顺序与索引的存储顺序一致。

  

1)聚集索引与查询操作

如上图,我们在名字字段上建立聚集索引,当需要在根据此字段查找特定的记录时,数据库系统会根据特定的系统表查找的此索引的根,然后根据指针查找下一个,直到找到。例如我们要查询“Green”,由于它介于[Bennet,Karsen],据此我们找到了索引页1007,在该页中“Green”介于[Greane, Hunter],据此我们找到叶结点1133(也即数据结点),并最终在此页中找以了目标数据行。

此次查询的IO包括3个索引页的查询(其中最后一次实际上是在数据页中查询)。这里的查找可能是从磁盘读取(Physical Read)或是从缓存中读取(Logical Read),如果此表访问频率较高,那么索引树中较高层的索引很可能在缓存中被找到。所以真正的IO可能小于上面的情况。

 

2)聚集索引与插入操作

最简单的情况下,插入操作根据索引找到对应的数据页,然后通过挪动已有的记录为新数据腾出空间,最后插入数据。

如果数据页已满,则需要拆分数据页(页拆分是一种耗费资源的操作,一般数据库系统中会有相应的机制要尽量减少页拆分的次数,通常是通过为每页预留空间来实现):
A在该使用的数据段(extent)上分配新的数据页,如果数据段已满,则需要分配新段。
B调整索引指针,这需要将相应的索引页读入内存并加锁。
C大约有一半的数据行被归入新的数据页中。
D
如果表还有非聚集索引,则需要更新这些索引指向新的数据页。

特殊情况:
A如果新插入的一条记录包含很大的数据,可能会分配两个新数据页,其中之一用来存储新记录,另一存储从原页中拆分出来的数据。
B通常数据库系统中会将重复的数据记录存储于相同的页中。
C类似于自增列为聚集索引的,数据库系统可能并不拆分数据页,页只是简单的新添数据页。

 

3)聚集索引与删除操作

删除行将导致其下方的数据行向上移动以填充删除记录造成的空白。

如果删除的行是该数据页中的最后一行,那么该数据页将被回收,相应的索引页中的记录将被删除。如果回收的数据页位于跟该表的其它数据页相同的段上,那么它可能在随后的时间内被利用。如果该数据页是该段的唯一一个数据页,则该段也被回收。

对于数据的删除操作,可能导致索引页中仅有一条记录,这时,该记录可能会被移至邻近的索引页中,原索引页将被回收,即所谓的“索引合并”。

 

5.非聚集索引

非聚集索引与聚集索引相比:
A叶子结点并非数据结点
B叶子结点为每一真正的数据行存储一个指针
C叶子结点中还存储了一个指针偏移量,根据页指针及指针偏移量可以定位到具体的数据行。
D
类似的,在除叶结点外的其它索引结点,存储的也是类似的内容,只不过它是指向下一级的索引页的。

聚集索引是一种稀疏索引,数据页上一级的索引页存储的是页指针,而不是行指针。而对于非聚集索引,则是密集索引,在数据页的上一级索引页它为每一个数据行存储一条索引记录。

对于根与中间级的索引记录,它的结构包括:
A索引字段值
BRowId(即对应数据页的页指针+指针偏移量)。在高层的索引页中包含RowId是为了当索引允许重复值时,当更改数据时精确定位数据行。
C下一级索引页的指针

对于叶子层的索引对象,它的结构包括:
A
索引字段值
BRowId

  

1)非聚集索引与查询操作

针对上图,如果我们同样查找“Green”,那么一次查询操作将包含以下IO3个索引页的读取+1个数据页的读取。同样,由于缓存的关系,真实的IO实际可能要小于上面列出的。

 

2)非聚集索引与插入操作

如果一张表包含一个非聚集索引但没有聚集索引,则新的数据将被插入到最末一个数据页中,然后非聚集索引将被更新。如果也包含聚集索引,该聚集索引将被用于查找新行将要处于什么位置,随后,聚集索引、以及非聚集索引将被更新。

 

3)非聚集索引与删除操作

如果在删除命令的Where子句中包含的列上,建有非聚集索引,那么该非聚集索引将被用于查找数据行的位置,数据删除之后,位于索引叶子上的对应记录也将被删除。如果该表上有其它非聚集索引,则它们叶子结点上的相应数据也要删除。

如果删除的数据是该数所页中的唯一一条,则该页也被回收,同时需要更新各个索引树上的指针。

由于没有自动的合并功能,如果应用程序中有频繁的随机删除操作,最后可能导致表包含多个数据页,但每个页中只有少量数据。

 

6.索引覆盖

索引覆盖是这样一种索引策略:当某一查询中包含的所需字段皆包含于一个索引中,此时索引将大大提高查询性能。

包含多个字段的索引,称为复合索引。索引最多可以包含31个字段,索引记录最大长度为600B。如果你在若干个字段上创建了一个复合的非聚集索引,且你的查询中所需Select字段及Where,Order By,Group By,Having子句中所涉及的字段都包含在索引中,则只搜索索引页即可满足查询,而不需要访问数据页。由于非聚集索引的叶结点包含所有数据行中的索引列值,使用这些结点即可返回真正的数据,这种情况称之为索引覆盖

在索引覆盖的情况下,包含两种索引扫描:
A)匹配索引扫描
B)非匹配索引扫描

 

1)匹配索引扫描

此类索引扫描可以让我们省去访问数据页的步骤,当查询仅返回一行数据时,性能提高是有限的,但在范围查询的情况下,性能提高将随结果集数量的增长而增长。

针对此类扫描,索引必须包含查询中涉及的的所有字段,另外,还需要满足:Where子句中包含索引中的引导列Leading Column),例如一个复合索引包含A,B,C,D四列,则A引导列。如果Where子句中所包含列是BCD或者BD等情况,则只能使用非匹配索引扫描。

 

2)非配置索引扫描

正如上述,如果Where子句中不包含索引的导引列,那么将使用非配置索引扫描。这最终导致扫描索引树上的所有叶子结点,当然,它的性能通常仍强于扫描所有的数据页。

 

[参考]
[1]http://manuals.sybase.com/onlinebooks/group-asarc/asg1200e/aseperf/@Generic__BookTextView/3358
[2]
http://publib.boulder.ibm.com/infocenter/idshelp/v10/index.jsp?topic=/com.ibm.adref.doc/adref235.htm

[C#]C#中的数字格式化 格式日期格式化

mikel阅读(1143)

字符串格式化这部分内容是我们经常用到的,如“2008-03-26”日期格式、“28.20”数字格式。
  举一个例子,我们有时需要将订单号“12”显示为“00000012”这种样式(不足8位前面补0),就可以使用下面的方法:
  int originalCode = 12;
  Response.Write(string.Format("{0:00000000}", originalCode));
  或者
  int originalCode = 12;
  Response.Write(originalCode.ToString("00000000"))又如我们在使用日期做为某种关键字时,比如图 片的文件名,一般是到秒级,如 “20080326082708”Response.Write(DateTime.Now.ToString("yyyyMMddHHmmss")); // 输出:20080326082708
  这样如果并发操作比较多的话,就会产生文件重名的现象。我们可以将日期精确到1/10000000秒,这样的话重名的可能性就很小了。
  Response.Write(DateTime.Now.ToString("yyyyMMddHHmmssfffffff"));// 输出:200803260827087983268
  =====================================================================
  格式
  基本内容是:可以在 Console.WriteLine(以及 String.Format,它被 Console.WriteLine 调用)中的格式字符串内的括号中放入非索引数字的内容。格式规范的完整形式如下:
  {index [, width][:formatstring]}
  其中,index 是此格式程序引用的格式字符串之后的参数,从零开始计数;width(如果有的话)是要设置格式的字段的宽度(以空格计)。width 取正数表示结果右对齐,取负数则意味着数字在字段中左对齐。(请参阅下面的前两个示例。)
  formatstring 是可选项,其中包含有关设置类型格式的格式说明。如果对象实现 IFormattable,formatstring 就会传递给对象的 Format 方法(在 Beta 2 和后续版本中,该方法的签名变为 ToString(string, IFormatProvider),但功能不变)。如果对象不实现 IFormattable,就会调用 Object.ToString(),而忽略 formatstring。
  另请注意,在 Beta 1 中不区分当前语言的 ToString 在 Beta 2 和后续版本中“将”区分语言。例如,对于用“.”分隔千位,用“,”分隔小数的国家,1,234.56 将会格式化成 1.234,56。如果您需要结果无论在什么语言下都是一样的,就请使用 CultureInfo.InvariantCulture 作为语言。
  若要获取有关格式的完整信息,请查阅“.NET 框架开发人员指南”中的格式概述(英文)。
  数字格式
  请注意,数字的格式是区分语言的:分隔符以及分隔符之间的空格,还有货币符号,都是由语言决定的 — 默认情况下,是您计算机上的默认语言。默认语言与执行线程相关,可以通过 Thread.CurrentThread.CurrentCulture 了解和设置语言。有几种方法,可以不必仅为一种给定的格式操作就立即更改语言。
  内置类型的字母格式
  有一种格式命令以单个字母开头,表示下列设置:
  G—常规,E 或 F 中较短的
  F—浮点数,常规表示法
  E—用 E 表示法表示的浮点数(其中,E 代表 10 的次幂)
  N—带有分隔符的浮点数(在美国为逗号)
  C—货币,带有货币符号和分隔符(在美国为逗号)
  D—十进制数,仅用于整型
  X—十六进制数,仅用于整型
  字母可以后跟一个数字,根据字母的不同,该数字可以表示总位数、有效位数或小数点后面的位数。
  下面是字母格式的一些示例:
  
  double pi = Math.PI;
  double p0 = pi * 10000;
  int i = 123;
  Console.WriteLine("浮点格式,无分隔符(逗号)");
  Console.WriteLine("pi, Left {0, -25}", pi); // 3.1415926535897931
  Console.WriteLine("p0, Rt. {0, 25}", p0); // 3.1415926535897931
  Console.WriteLine("pi, E {0, 25:E}", pi); // 3.1416E+000
  Console.WriteLine("使用 E 和 F 格式,小数点后保留 n(此处为 4)位");
  Console.WriteLine("pi, E4 {0, 25:E4}", pi); // 3.1416E+000
  Console.WriteLine("pi, F4 {0, 25:F4}", pi); // 3.1416
  Console.WriteLine("使用 G 格式,保留 4 位有效数字——如果需要请使用 E 格式");
  Console.WriteLine("pi, G4 {0, 25:G4}", pi); // 3.142
  Console.WriteLine("p0, G4 {0, 25:G4}", p0); // 3.142E4
  Console.WriteLine("N 和 C 格式带有逗号(分隔符)," +
  "默认小数点后保留两位,四舍五入。");
  Console.WriteLine("p0, N {0, 25:N}", p0); // 31,415.93
  Console.WriteLine("p0, N4 {0, 25:N4}", p0); // 31,415.9265
  Console.WriteLine("p0, C {0,25:C}", pi); //  $3.14
  Console.WriteLine("D 和 X 格式仅用于整型," +
  "非整型将产生格式异常——X 指十六进制");
  Console.WriteLine("i, D {0, 25:D}", i ); // 123
  Console.WriteLine("i, D7 {0, 25:D7}", i ); // 0000123
  Console.WriteLine("i, X {0, 25:X}", i ); // 7B
  Console.WriteLine("i, X8 {0, 25:X8}", i ); // 0000007B

  图片格式
  与字母格式不同,formatstring 可以包含“图片格式”。下面是从代码中摘录的几个实例。(这类似于 Basic 中的“Print Using”语句。)图片格式功能甚至包括以不同方式设置负数、正数和零的格式的能力。还有几个图片格式功能,下面的示例中未包括在内。有关详细信息,请 参阅“.NET 框架开发人员指南”或文档中的主题图片格式数字串(英文)。
  在下例中您将注意到,好心的博士既使用了“#”字符,又使用了“0”字符。如果相应的数字是前导零或尾随零,“#”字符就会替换为空值。无论相应数字 的值如何,“0”字符都会被替换为零字符 — 因此,数字将会被零填补。句号(如果有的话)表示小数分隔符的位置。
  那么,为什么要同时使用这些字母,比如“###0.##”? 如果要设置格式的值恰好为零,“#” 图片字符就被替换为“无”(连零字符也不是)。您可能“总是”希望在小数点的左边至少有一个“0”,否则,如果值为零,字段就没有输出。换言之,仅包含 “#”字符,一个“0”也没有的格式常被认为是一个编程错误。
  逗号有两种用法:如果一个逗号或一组逗号紧跟在句号的左边(或者没有句号时在结尾),它们就会告诉格式化程序分隔 10 ** (3 * n) 所显示的数字,其中,n 是逗号的个数。换言之,数字按千位、百万位、十亿位等分隔。
  如果逗号的右侧至少有一个“0”或“#”占位符,它就会告诉格式化程序在各数位组之间放置适当的组分隔符字符(在美国为逗号。)(在美国,每三个数位算一组。)
  可以设置百分比的格式,方法是在图片中放入“%”。“%”将在指定的位置显示,在显示前数字将被乘以 100(这样,0.28 就变成了 28%)。
  如果希望将图片格式用于指数表示法,可以指定“e”或“E”后跟加号或减号,再后跟任意个零,比如“E+00”或“e-000”。如果使用“e”,则 显示小写“e” 。如果使用“E”,则显示大写“E” 。如果使用加号,则指数的符号总是出现。如果使用减号,则符号只有在指数为负数时才会显示。(Beta 1 版在处理“-”时有问题,该符号会导致负号总是出现。)
  根据要设置格式的数字的符号,还有一个条件格式。在格式字符串中仅包含两个或三个独立的格式,它们由分号分隔。如果有两个格式,则第一个将用于非负数,第二个用于负数。如果有三个格式,则第一个将用于正数,第二个用于负数,第三个用于零。
  可以在格式字符串中包含文字字符。如果所需的字符具有特殊意义,请在其前面使用反斜杠符号,使其“转义”。例如,如果希望在不乘以 100 的情况下显示百分比符号,就可以在数字前面使用反斜杠(在 C++ 和 C# 中必须使用两个反斜杠),比如“#0.##\%”。(如果正在使用 C#,就可以使用极酷的逐字字符串文字,比如@"#0.##%"。)或者,也可以将字符串放入单引号或双引号中,以避免将其字符解释为格式命令。在 Beta 2 及更高版本中,可以通过使用双括号,从而在格式字符串中包含文字括号。
  下面是有关图片格式的一些示例:
  long m34 = 34000000; // 34,000,000
  Console.WriteLine("几种图片格式");
  Console.WriteLine("如果没有数位,0 将打印 0;" +
  "诸如 i: 的文字总是打印");
  Console.WriteLine("t句点代表小数分隔符的位置");
  Console.WriteLine("i, i: 0000.0 {0, 10:i: 0000.0}", i); //
  i:0123.0
  Console.WriteLine("如果没有有效数字 # 将不显示," +
  "逗号意味着放入分隔符");
  Console.WriteLine("请确保在数字图片中至少使用一个 0。");
  Console.WriteLine("p0, ##,##0.# {0, 10:##,##0.#}",-p0); // -31,415.9
  Console.WriteLine("m34, 0,, {0, 10:0,, 百万}", m34); // 34 百万
  Console.WriteLine("p0, #0.#E+00 {0, 10:#0.#E+00}", p0); // 31.4E+03
  Console.WriteLine("% 乘以 100 并打印百分号");
  Console.WriteLine("pi, ###0.##% {0, 10:###0.##%}", pi); // 314.16%
  Console.WriteLine("因为 \ 而没有进行乘法运算" +
  "(注意:两个反斜线!)");
  Console.WriteLine("pi, ###0.##\\% {0, 10:###0.##\%}", pi); // 3.14%
  Console.WriteLine("与 C# 的逐字字符串相同");
  Console.WriteLine(@"pi, ###0.##\% {0, 10:###0.##%}", pi); // 3.14%
  Console.WriteLine("10, '#'#0 {0, 10:'#'#0}", 10); // #10
  Console.WriteLine("基于符号的条件格式");
  Console.WriteLine("如果是 0 或正数打印 #,如果是负数打印 (#)");
  Console.WriteLine("-5 0;(0) {0, 10:0;(0)}", -5); // (5)
  Console.WriteLine("如果是正数打印 #,如果是负数打印 -#,如果是 0 打印 zip");
  Console.WriteLine(" 0 0;-0;zip {0, 10:0;-0;zip}", 0); // zip
  如您所见,格式功能非常强大。
  格式的工作方式
  文档中的示例对所传递的对象类型的变量调用 Format 方法。对这些 Format 方法仅传递格式规范的 formatstring 部分,而不传递 index 和 width。(在 Beta 2 中,对 Format 的调用将改为对 ToString 的调用。)
  index 和 width 由 String.Format(它被 Console.Write 和 Console.WriteLine 调用)使用,以获得调用 Format 的正确对象以及将该调用的结果左或右对齐。(顺便说一下,如果要设置格式的对象不实现 IFormattable(并因此调用 Format 方法),String.Format 将调用对象的 ToString() 方法,而忽略 formatstring。)
  换言之,Console.WriteLine 调用 String.Format,传递向它传递的所有参数。String.Format 分析字符串,查找“{”字符。找到该字符后,它将分析子字符串直到第一个“}”为止,以确定 index 数、width 和 formatstring。然后,它按照 index 访问相应的参数,并调用其 Format 方法,传递“{}”段中的 formatstring 部分。(如果参数对象不实现 IFormattable,则被调用的是 ToString。)
  无论是实现还是不实现,都会返回一个字符串,并且 String.Format 在继续分析格式字符串之前会将其与结果字符串连接。之后,String.Format 将生成的带格式字符串返回给 Console.WriteLine,由 Console.WriteLine 进行显示。
  对于 Beta 2 及更高版本,对象的 Format 方法(它是 IFormattable 中的 Format 方法)被 ToString 所替代,ToString 获取一个格式字符串和一个 IFormatProvider(或 null)。但 String.Format 仍存在,因此这些调用将不改变。
  自定义格式
  您自己也可以编写格式化程序,用于自己的类型或作为内置类型的自定义格式化程序,如“.NET 框架开发人员指南”中的自定义 Format 方法所说明的那样。如果编写内置类型的自定义格式化程序,就不能从 Console.WriteLine 中使用它,但可以通过调用 String.Format 的重载而使用它,String.Format 的重载将采用 IServiceObjectProvider(在 beta 2 及更高版本中称为 IFormatProvider)作为参数。
  日期和时间格式
  您将记起,有一个叫做 DateTime 的类,用于保存日期和时间。像您所猜想的那样,有大量方法可供设置 DateTime 对象的格式:仅日期、仅时间、世界时或本地时、若干种日/月/年顺序,甚至可分类。日期和时间格式是区分语言的。
  还可以使用自定义格式字符串来设置 DateTime 对象的格式。这种字符串将包含由某些字母组成的区分大小写的子字符串,以表示日期和时间的各个不同部分,如星期几、几号、月份、年份、纪元、小时、分钟、 秒或时区。这些部分中有许多具有多种格式,例如,M 是没有前导零的数字月份,MM 是有前导零的数字月份,MMM 是三个字母的月份缩写,MMMM 是所在国家语言对应的完整月份名称的拼写。在“.NET 框架参考”中可以找到自定义和标准格式字符的完整列表。
  下面是有关日期和时间格式的一个示例:
  Console.WriteLine("标准格式");
  // 后面的“分析”中会有更多信息
  DateTime dt = DateTime.Parse("2001 年 1 月 1 日,12:01:00am");
  Console.WriteLine("d: {0:d}", dt); // 1/1/2001
  Console.WriteLine("D: {0:D}", dt); // 2001 年 1 月 1 日,星期一
  Console.WriteLine("f: {0:f}", dt); // 2001 年 1 月 1 日,星期一 12:01 AM
  Console.Write("F: {0:F}", dt); // 2001 年 1 月 1 日,星期一 12:01:00 AM
  Console.WriteLine();
  Console.WriteLine("g: {0:g}", dt); // 1/1/2001 12:01 AM
  Console.WriteLine("G: {0:G}", dt); // 1/1/2001 12:01:00 AM
  Console.WriteLine("M/m: {0:M}", dt); // 2001 年 1 月
  Console.WriteLine("R/r: {0:R}", dt); // 2001 年 1 月 1 日,星期一 08:01:00 GMT
  Console.WriteLine("s: {0:s}", dt); // 2001-01-01T00:01:00
  Console.WriteLine("t: {0:t}", dt); // 12:01 AM
  Console.WriteLine("T: {0:T}", dt); // 12:01:00 AM
  Console.WriteLine("u: {0:u}", dt); // 2001-01-01 08:01:00Z
  Console.Write("U: {0:U}", dt); // 2001 年 1 月 1 日,星期一 8:01:00 AM
  Console.WriteLine();
  Console.WriteLine("Y/y: {0:Y}", dt); // 2001 年 1 月
  Console.WriteLine("自定义格式");
  // 对作为格式使用的字符必须“转义”—此处为 t 和 z
  // 同时使用引号(在文字字符串中)和反斜杠
  Console.WriteLine(@"dddd, dd MMMM yyyy"" at ""HH:mm:ss in zone zzz:");
  Console.WriteLine(@"{0:dddd, dd MMMM yyyy"" at ""HH:mm:ss in zone zzz}",
  dt);
  // 2001 年 1 月 1 日,星期一 00:01:00 于时区 -08:00
http://www.microsoft.com/china/MSDN/library/archives/library/welcome/dsmsdn/drguinet03292001.asp
  程序:
  using System;
  using System.Collections.Generic;
  using System.Text;
  namespace ConsoleApplication1
  {
   class Program
   {
   static void Main(string[] args)
   {
   double pi = Math.PI;
   double p0 = pi * 10000;
   int i = 123;
   long m34 = 34000000; // 34,000,000
   Console.WriteLine("几种图片格式");
   Console.WriteLine("如果没有数位,0 将打印 0;" +
   "诸如 i: 的文字总是打印");
   Console.WriteLine("t句点代表小数分隔符的位置");
   Console.WriteLine("i, i: 0000.0 {0, 10:i: 0000.0}", i); // i:0123.0
   Console.WriteLine("如果没有有效数字 # 将不显示," +
   "逗号意味着放入分隔符");
   Console.WriteLine("请确保在数字图片中至少使用一个 0。");
   Console.WriteLine("p0, ##,##0.# {0, 10:##,##0.#}", -p0); // -31,415.9
   Console.WriteLine("m34, 0,, {0, 10:0,, 百万}", m34); // 34 百万
   Console.WriteLine("p0, #0.#E+00 {0, 10:#0.#E+00}", p0); // 31.4E+03
   Console.WriteLine("% 乘以 100 并打印百分号");
   Console.WriteLine("pi, ###0.##% {0, 10:###0.##%}", pi); // 314.16%
   Console.WriteLine("pi, ###0.##% {0, 10:#######0.##%}", pi); // 314.16%
   Console.WriteLine("因为 \ 而没有进行乘法运算" +
   "(注意:两个反斜线!)");
   Console.WriteLine("pi, ###0.##\\% {0, 10:###0.##\%}", pi); // 3.14%
   Console.WriteLine("与 C# 的逐字字符串相同");
   Console.WriteLine(@"pi, ###0.##\% {0, 10:###0.##%}", pi); // 3.14%
  Console.WriteLine("10, '#'#0 {0, 10:'#'#0}", 10); // #10
   Console.WriteLine("10, '#'#0 {0, 10:##0}", 10); // 10
   Console.WriteLine("基于符号的条件格式");
   Console.WriteLine("如果是 0 或正数打印 #,如果是负数打印 (#)");
   Console.WriteLine("-5 0;(0) {0, 10:0;(0)}", -5); // (5)
   Console.WriteLine("如果是正数打印 #,如果是负数打印 -#,如果是 0 打印 zip");
   Console.WriteLine(" 0 0;-0;zip {0, 10:0;-0;zip}", 0); // zip
   Console.ReadLine();
   }
  }
 }

[C#]Emit学习系列文章导航

mikel阅读(1078)

Emit学习系列文章导航

这两个星期来一直在学习Emit方面的相关内容,基础的理论已经基本学习完毕,剩下的就要靠实践的积累了,在学习的过程中,也把自己的心得、体会、碰到的问题都记录了下来,形成了一个Emit学习的系列文章,现在这个系列暂时告一段落,等到有了一定的实践积累,或者在实践中发现了什么新的问题,我会继续更新这一系列的文章,尤其是最后实践篇中的内容。现在将这些内容整理了一下,发到首页,希望能够对那些对EmitIL有兴趣的人提供那么一点帮助,大家如果有什么问题可以在文后留言回复,我会尽力解答。这里留下我的联系方式:MSNyinqql.cn@163.com QQ:413183023 ,由于工作原因只能在晚上上网,请大家谅解,最后附上这一系列文章的导航:

1.   前言

2.   基础篇

l  HelloWorld

l  基本概念介

l  OpCodes说明

l  为动态类添加属性、构造函数、方法

l  使用循环

3.   进阶篇

l  异常处理

l  定义事件

4.   答疑篇

l  Call和Callvirt的区别

l  值类型和引用类型的区别

5.   实践篇

[SQL]在SQL Server中使用种子表生成流水号注意顺序

mikel阅读(1003)

前几天一个人问到了关于流水号重复的问题,我想了下,虽然说这个问题比较简单,但是具有广泛性,所以写了这篇博客来介绍下,希望对大家有所帮助。

在进行数据库应用开发时经常会遇到生成流水号的情况,比如说做了一个订单模块,要求订单号是唯一的,规则是:下订单时的年月日+6位的流水号这样的规则。

对于这种要生成流水号的系统,我们一般是在数据库中新建了一个种子表,每次生成新的订单时:

1.读取当天种子最大值。

2.根据种子最大值和当时的年月日生成唯一的订单号。

3.更新种子最大值,使最大值+1。

4.根据生成的订单号将订单数据插入到订单表中。

以上几步操作是在一个事务中完成,保证了流水号的连续。这个思路是正确的,使用起来好像也没有什么问题,但是在业务量比较大的情况下却经常报错:“订单号违反主键约束,不能将重复的订单号插入到订单表中。”这是怎么回事?让我们做一个简单的Demo来重现一下:

1.创建种子表和订单表,这里只是一个简单的Demo,所以就省去了很多字段,而且订单号假设就是一个流水号,不用再使用年月日+6位流水号了。

Create TABLE Seek 种子表
(
    SeekValue 
INT
)
GO
Insert INTO Seek VALUES(0)种子初始值为0
GO
Create TABLE Orders
(
    OrderID 
INT PRIMARY KEY订单号,主键
    Remark VARCHAR(5NOT NULL
)

 2.创建一个存储过程,该存储过程传入Remark参数,根据生成的流水号插入到订单表中:

Create PROC AddOrder Author:深蓝
@remark VARCHAR(5传入的参数
AS
DECLARE @seek int 
BEGIN TRAN  开启一个事务
Select @seek=SeekValue 读取种子表中的最大值作为流水号
FROM Seek
生成订单号这一步省略,因为这里假定的订单的编号就是流水号

Update Seek SET SeekValue=@seek+1 更新种子表,使最大值+1

Insert INTO t1 VALUES(@seek,@remark插入一条订单数据

COMMIT 提交事务

3.新建一个查询窗口,使用以下语句调用创建的存储过程,不断的插入新订单:

WHILE 1=1
EXEC AddOrder 'test1' 不断的插入订单

 

4.再新建一个查询窗口,使用通过的方式,不断的插入新订单,这样用于模拟高并发时候的情况:

WHILE 1=1
EXEC AddOrder 'test2'

 

5.运行了一段时间后,我们停止这两个死循环,我们可以看到消息窗口中存在大量的异常:

消息 2627,级别 14,状态 1,过程 AddOrder,第 11 行
违反了 PRIMARY KEY 约束 'PK__Orders__C3905BAF08EA5793'。不能在对象 'dbo.Orders' 中插入重复键。

语句已终止。

为什么会这样呢?这得从事务隔离级别和锁来解释:

一般我们写程序时都是使用的是默认的事务隔离级别——已提交读,在第一步查询Seek表时,系统会为该表放置共享锁,而锁的兼容性中共享锁和共享锁 是可以兼容的,所以一个事务在读取Seek表最大值时,其他事务也可以读取出相同的最大值,两个事务中读取到了相同的最大值,所以产生了相同的流水号,所 以产生了相同的订单号,所以才会出现违反主键约束的错误。

既然知道了这其中的原理了,那么解决办法也就有了,只需要先对种子表中的数+1,然后再进行读取即可,修改存储过程如下:

Alter PROC AddOrderAuthor:深蓝
@remark VARCHAR(5)
AS
DECLARE @seek int
BEGIN TRAN
Update Seek SET SeekValue=SeekValue+1  先修改数据

Select @seek=SeekValue1 已经加了1,所以这里-1下来
FROM Seek
Insert INTO Orders VALUES(@seek,@remark)
COMMIT

 

为什么这样写就可以呢?第一步执行更新操作,系统会请求更新锁然后再升级为排他锁,因为更新锁和更新锁以及排他锁都是不兼容的,所以一个事务对Seek表进行了更新后,其他的事务就不能对表进行更新操作,只有等到事务提交以后才能继续。

这里附上锁兼容性表:

现有授予模式
请求模式 IS S U IX SIX X
意向共享 (IS)
共享 (S)
更新 (U)
意向排他 (IX)
意向排他共享 (SIX)
排他 (X)
【出自博客园深蓝居,转载请注明作者出处】