前言:通常情况下,每个公司都会有自己的基础信息库,比如存储的省市区县等等。而在实际开发中,我们可能不止一次要用到全国省市区县三级联动的效果。下面我就总结一下自己在开发中用到的三级联动代码,包括数据库脚本,C#,IBatisNet和javascript实现的前后端代码,类似实现其实也同样可以扩展到三级类别的实现上。本文贴代码为主,有兴趣的可以下载示例看一下,也许对您有帮助。

 

1、开发环境和组织结构介绍

环境:VS2010+SqlServer2005Express+IBatisNet

省市区县等位置信息 java表结构_省市区县等位置信息 java表结构

如果您已经熟悉本文作者一贯的编码风格,应该已经猜到了各项目及各个文件夹的大概作用。

 

2、SQL Server数据库脚本

上图AreaDAL项目下,DataBase文件夹内包含着一个Area.sql脚本文件。这个脚本我是从网上下载然后稍作部分列名和字段的改进。原文url已经不记得了,对原作者表示非常抱歉。请注意,数据库中我国的台湾省缺少对应的城市和区县。

 

3、javascript脚本

在客户端实现联动的效果,我们可以通过下面的两种途径实现

(1)、二维数组

一个省份对应的城市的javascript二维数组结构是这样的:

var provinceCityArr = new Array(10);
var provinceId = 1234; // 江苏id
var cityArr = new Array('3456', '南京', '3457', '苏州'); //数字是城市id,汉字是城市名称
provinceCityArr[0] = new Array(provinceId, cityArr); //一个provinceId 对应一个city数组

一个城市和对应的区县类同上面的一个省份对应的城市(如果javascript可以有c#那样现成的数据结构如哈希表或者字典等等,这一方面的工作将非常简单)。

当我们选择不同的省份或者不同的城市的时候,就会触发这两个事件,这两个事件(包括下面介绍的json实现中)是在服务端(页面类文件)通过下面的形式注册的:

this.ddlProvince.Attributes.Add("onchange", "displayFirst(" + this.SelectFirstId + ", " + this.SelectSecondId + "," + this.SelectThirdId + ") ");
       this.ddlCity.Attributes.Add("onchange", "displaySecond(" + this.SelectSecondId + ", " + this.SelectThirdId + ")");

而核心的两个javascript函数displayFirst和displaySecond就是对数组的遍历匹配和控件option的填充而已:

//必须先引用selectUtil.js

/*
第一层级联

参数说明:
oSelectFirst:第一个select控件
oSelectSecond:第二个select控件
oSelectThird:第三个select控件
oArr:注册在客户端的第一个级联数组
 
可选参数:
oSecondValue:第一层级联的关联值  比如某一省份下对应选中的城市值
*/
function displayFirst(oSelectFirst, oSelectSecond, oSelectThird, oArr) {
    try {
        var parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value;
        if (oArr.length > 0) {
            if (parentKey.length > 0) { //说明选择的不是“请选择”
                for (var ii = 0; ii < oArr.length; ii++) {
                    var item = oArr[ii];
                    if (parseInt(item[0]) == parseInt(parentKey)) {
                        var arrItems = item[1];
                        removeSelItems(oSelectSecond);
                        selectOptionAdd(oSelectSecond, "请选择", "");
                        for (var i = 0; i < arrItems.length / 2; i++) {
                            selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2] + "_" + arrItems[i * 2 + 1]); // key_value
                            //selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2]);//key
                        }
                        if (arguments.length > 4) {
                            oSelectSecond.value = arguments[4]; //初始化选项用
                        }
                        break;
                    }
                }
            }
            else {
                removeSelItems(oSelectSecond);
                selectOptionAdd(oSelectSecond, "请选择", "");
            }
            removeSelItems(oSelectThird);
            selectOptionAdd(oSelectThird, "请选择", "");
        }
    }
    catch (e) {
        alert(e.message);
    }
}

/*
第二层级联

参数说明:
oSelectFirst:第一个select控件 (这里对应城市)
oSelectSecond:第二个select控件(这里对应区县)
oArr:注册在客户端的第二个级联数组

可选参数:
oThirdValue:第二层级联的关联值  比如某一城市下对应选中的区县值
*/
function displaySecond(oSelectFirst, oSelectSecond, oArr) {
    try {
        var parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value.split('_')[0];
        if (oArr.length > 0) {
            if (parentKey.length > 0) { //说明选择的不是“请选择”
                for (var ii = 0; ii < oArr.length; ii++) {
                    var item = oArr[ii];
                    if (parseInt(item[0]) == parseInt(parentKey)) {
                        var arrItems = item[1];
                        removeSelItems(oSelectSecond);
                        selectOptionAdd(oSelectSecond, "请选择", "");
                        for (var i = 0; i < arrItems.length / 2; i++) {
                            selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2] + "_" + arrItems[i * 2 + 1]); //key_value
                            //selectOptionAdd(oSelectSecond, arrItems[i * 2 + 1], arrItems[i * 2]);//key
                        }
                        if (arguments.length > 3) {
                            oSelectSecond.value = arguments[3]; //初始化选项用
                        }
                        break;
                    }
                }
            }
            else {
                removeSelItems(oSelectSecond);
                selectOptionAdd(oSelectSecond, "请选择", "");
            }
        }
    }
    catch (e) {
        alert(e.message);
    }
}

说明一下,上面注释中的selectUtil.js文件是一个对select控件操作的函数集合文件,您可以参考这一篇

(2)、json

当我们选择省份的时候,都会借助jQuery发出一个同步ajax请求,返回省份对应的城市区县json。我们在服务端组织好的json数据格式如下所示:

var areaJson =
    {
        "ProvinceId": 1234, "ProvinceName": "江苏",
        "Cities": // 数组
            [
                {
                    "CityId": 3456, "CityName": "南京",
                    "Counties":
                    [
                    { "CountyId": 34560, "CountyName": "玄武区" },
                    { "CountyId": 34561, "CountyName": "秦淮区" }
                    ]
                },
               { "CityId": 3457,
                   "CityName": "苏州",
                   "Counties":
                    [
                    { "CountyId": 34570, "CountyName": "吴中区" },
                    { "CountyId": 34571, "CountyName": "昆山市" }
                    ]
               }
            ]
    }

其实,在我实现的代码里,在实现省份对应城市,城市对应区县的时候,json最终还是映射成数组,然后按照选中的option,给关联的select控件填充数据,和(1)非常类似,但是js数组结构发生了变化:

//必须先引用jQuery.js和selectUtil.js

var jqRequestUrl = "/Handler/AreaHandler.ashx";
var areaJson = null; //级联json

/*
第一层级联

参数说明:
oSelectFirst:第一个select控件
oSelectSecond:第二个select控件
oSelectThird:第三个select控件

可选参数:
oSecondValue:第一层级联的关联值  比如某一省份下对应选中的城市值
*/
function displayFirst(oSelectFirst, oSelectSecond, oSelectThird) {
    try {
        var parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value;
        if (String(parentKey).length > 0) {
            getAreaJson(parentKey);
        }
        var oArr = null;
        if (areaJson != null) {
            oArr = areaJson.Cities; //城市
        }
        if (parentKey.length > 0) {//说明选择的不是“请选择”
            if (oArr != null) {
                removeSelItems(oSelectSecond);
                selectOptionAdd(oSelectSecond, "请选择", "");
                for (var ii = 0; ii < oArr.length; ii++) {
                    var item = oArr[ii];
                    var cityId = item.CityId;
                    var cityName = item.CityName;
                    selectOptionAdd(oSelectSecond, cityName, cityId + "_" + cityName); // key_value
                    //selectOptionAdd(oSelectSecond,cityName, cityId);//key
                }
                if (arguments.length > 3) {
                    oSelectSecond.value = arguments[3]; //初始化选项用
                }
            }
            else {
                removeSelItems(oSelectSecond);
                selectOptionAdd(oSelectSecond, "请选择", "");
            }
        }
        else {
            removeSelItems(oSelectSecond);
            selectOptionAdd(oSelectSecond, "请选择", "");
        }
        removeSelItems(oSelectThird);
        selectOptionAdd(oSelectThird, "请选择", "");
    }
    catch (e) {
        alert(e.message);
    }
}

/*
第二层级联

参数说明:
oSelectFirst:第一个select控件 (这里对应城市)
oSelectSecond:第二个select控件(这里对应区县)

可选参数:
oThirdValue:第二层级联的关联值  比如某一城市下对应选中的区县值
*/
function displaySecond(oSelectFirst, oSelectSecond) {
    try {
        var parentKey = oSelectFirst.options[oSelectFirst.selectedIndex].value.split('_')[0];
        var oArr = null;
        if (areaJson != null) {
            var oFirstArr = areaJson.Cities; //城市
            for (var i = 0; i < oFirstArr.length; i++) {
                if (oFirstArr[i].CityId == parentKey) {
                    oArr = oFirstArr[i].Counties; //区县
                    break;
                }
            }
        }

        if (parentKey.length > 0) { //说明选择的不是“请选择”
            if (oArr != null) {
                removeSelItems(oSelectSecond);
                selectOptionAdd(oSelectSecond, "请选择", "");
                for (var ii = 0; ii < oArr.length; ii++) {
                    var item = oArr[ii];
                    var countyId = item.CountyId;
                    var countyName = item.CountyName;
                    selectOptionAdd(oSelectSecond, countyName, countyId + "_" + countyName); //key_value
                    //selectOptionAdd(oSelectSecond, countyName, countyId);//key
                }
                if (arguments.length > 2) {
                    oSelectSecond.value = arguments[2]; //初始化选项用
                }
            }
            else {
                removeSelItems(oSelectSecond);
                selectOptionAdd(oSelectSecond, "请选择", "");
            }
        }
        else {
            removeSelItems(oSelectSecond);
            selectOptionAdd(oSelectSecond, "请选择", "");
        }
    }
    catch (e) {
        alert(e.message);
    }
}


//从服务端取json数据
function getAreaJson(provinceId) {
    //第一个参数表示同步调用
    $.ajax({
        async: false,
        cache: false,
        type: "POST",
        url: jqRequestUrl,
        data: "action=getareajson&provinceid=" + provinceId,
        success: function (html) {
            areaJson = eval('(' + html + ')'); //eval转换成json
        }
    });
}

比较起来,应该比(1)更方便一些,而且效率应该比(1)高,因为遍历的次数大大减少了。

大家可以运行代码试试看,点击”Get  Value”按钮,在服务端会输出您选择的数据:

省市区县等位置信息 java表结构_省市区县等位置信息 java表结构_02

其实,在客户端脚本编程中,我们大部分精力都花在初始化(通过cascadeInit函数)和数据节点匹配以及填充上。

4、C#和IBatisNet实现部分

主要是通过IBatisNet作为ORM进行数据层的操作,通过IBatis取数据等等具体细节我就不具体介绍了,这里主要来谈谈对取出数据的缓存。我们发现省市区县这类数据的一个重要特点就是它们相对稳定,变更的情况很少,而且不是敏感的数据,数据量说多也不多,说少也不少,合理利用缓存可以提升系统性能。下面就简单总结一下项目中对于不常改动的基础数据如省市县,品类等等的可以采取的几种不同的缓存方案:

(1)、静态字段缓存

用static变量进行缓存(其实我们大多时候缓存的是一个引用类型,变量存放的只是一个指针引用),大家可能会觉得不可行,尤其是知道asp.net中页面静态字段造成的问题,大家可能更加不信任这种方案。我觉得某些情况下可以使用静态变量来缓存,尤其是对于那些只读而且永远不会过期的数据。静态变量有一个非常突出的好处,就是对于开发者而言,就是定义一处静态变量,系统全局都可以调用,不用担心它“不翼而飞”,而对于所有使用的用户来说,他们所使用的都是同一份数据:

public static IList<Province> listProvinces = null;//省份 对外公开
        private static IDictionary<int, Province> dictProvinces = null;
        private static IDictionary<int, City> dictCities = null;
        private static IDictionary<int, County> dictCounties = null;

但是,静态字段缓存的一个缺点就是,它没有提供缓存过期方案,也没有线程安全机制,一旦创建后,静态变量不能被回收(但是可以将它置为null空引用,它所引用的数据对象就可以被GC回收)。还好,通过静态字段缓存大多数情况下不用考虑线程安全,因为多数情况下都是只读的数据,不会发生不一致的情况,而对于缓存过期方案,我们可以利用一个定时器代替,当然这和asp.net所提供的缓存过期方案是完全不同的,比如考虑到可能对数据进行的小部分修改,本文的程序中,我在里面加了个Timer,控制某一时刻(凌晨3点)定时更新静态字段:

public const long timerPeriodTime = 1000 * 60 * 60 * 1;//每小时timer触发一次
         private static System.Timers.Timer areaTimer = null;

        /// <summary>
        /// 设置timer  每天凌晨3点重新访问数据库 获取相关数据
        /// </summary>
        private static void TimerSetUp()
        {
            areaTimer = new System.Timers.Timer();
            areaTimer.Elapsed += new ElapsedEventHandler(TimerArea_Elapsed);//附加公告事件
            areaTimer.Interval = timerPeriodTime;
            areaTimer.Enabled = true;
            /*当 AutoReset 设置为 false 时,Timer 只在第一个 Interval 过后引发一次 Elapsed 事件。
              若要保持以 Interval 时间间隔引发 Elapsed 事件,请将 AutoReset 设置为 true。*/
            areaTimer.AutoReset = true;
        }

        /// <summary>
        /// 定时更新
        /// </summary>
        /// <param name="sender"></param>
        private static void TimerArea_Elapsed(object sender, ElapsedEventArgs e)
        {
            if (DateTime.Now.Hour % 24 != 3)//凌晨3点访问数据库
            {
                return;
            }
            try
            {
                WaitCallback wcb = new WaitCallback(AsyncInitArea);
                int workerThreads, availabeThreads;
                ThreadPool.GetAvailableThreads(out workerThreads, out availabeThreads);
                if (workerThreads > 0)//可用线程数>0
                {
                    ThreadPool.QueueUserWorkItem(wcb, "定时获取地区信息");//异步
                }
                else
                {
                    InitArea();
                }
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        private static void AsyncInitArea(object objState)
        {
            InitArea();
        }

当然,如果利用下面(2)介绍asp.net的应用程序缓存,这个Timer完全用不到,可以省去上面的代码。

(2)、asp.net缓存

大家都知道asp.net缓存有几种不同的类别。通常我们可以借助System.Web.HttpRuntime.Cache或者System.Web.Caching.Cache进行数据缓存处理。这种缓存方案也就是asp.net的应用程序数据缓存,它的优势在于它向开发人员提供了完整的依赖、过期、线程同步等的支持,也是我们最常见也最放心使用的。

(3)、IBatisNet缓存

IBatisNet自生带有缓存功能,但是需要修改配置文件,还要改进取数据的代码,而且有时候它还不稳定。虽然这是一个可供选择的解决方案,但是开发中相对用的很少,毕竟我们对缓存的控制欲常常达到了“丧心病狂”的地步,而不会过分信任这种包装过的缓存方式。

(4)、分布式缓存系统

分布式缓存,顾名思义,从表面理解,就是一台机器内存不够,将要缓存的数据分布到不同机器上存储,同时它必须确保数据不能丢失,在多台机器上都有备份。需要注意的是,缓存的数据需要序列化和反序列化,比较折腾cpu的运算能力,还有就是在网络环境下进行数据传输会占用带宽,所以对于一个大量的数据集合,通常都分割存储成多个key:value字典的形式,而不是一个key,一个大量的数据集合(我曾经在使用Memcached的过程中犯过这样的失误)。

通常,对于流量较大的大中型站点,有条件的话,几乎都会借助于分布式缓存系统如Memcached、MongoDB等等。而在本文示例中,我选择了最简单静态字段缓存。实际项目中,(2)和(4)用的相对多一些,当然通过IBatisNet的缓存也是没有问题的。

 

最后需要说明的是,这个联动效果我在IE9、最新的FireFox和Chrome上测试全部通过,其他浏览器(或不同版本)没有测试。

 

update:我刚刚注册完InfoQ的时候,点击浏览器的后退按钮,发现它的国家对应省份不见了,这好像就有问题了,不知道是不是浏览器的问题:

省市区县等位置信息 java表结构_控件_03

(最下面的必填项:州/省…数据丢失了)