RSS 2.0 Feed

Sunday, August 17, 2008

很久没有在博客堂上发文了,先表示歉意。希望这次的文章能够带给你价值。 

关键词:BI、数据权限、Sharepoint、ajax

BI数据分析是目前企业的热门应用,而对企业来说,权限控制是非常重要的,尤其是作为决策用的企业报表。目前基于微软SQL Server体系的BI架构为Integration Services + Analysis Service + Reporting Services,Integration Services和Analysis都属于应用后台的服务,不会在用户前端展现,其权限控制体系不在我们这篇文章的讨论范围内(但是实现数据级权限控制,需要Analysis Services的参与)。而对于前端展示用的企业报表,权限控制体系分为2种:报表级权限和数据级权限。报表级权限较为简单,主要用于控制谁能够看这个报表;数据级权限则比较复杂了,任何人看同一张报表,报表上的数据只能是他有权限查看的数据。简单说,就是总经理看到的数据和经理看到的数据是不一样的,虽然他们在看同一张报表。比较报表级权限和数据级权限,会发现如果实现了数据级权限的控制,那么企业报表是否需要进行权限控制已经不再重要(当然,为了界面友好性,还是应该控制下的)。

这篇文章主要就是讲述基于SQL Server架构的BI数据级权限的解决方案,这也是我给一个德国大型跨国企业客户实施其BI项目中,对方非常重视的一个功能。这里先简单介绍下这个客户和项目,出于保密要求,我把该客户叫做Customer S(简称CS,呵呵,不是那个游戏哦)。

CS项目前端采用Sharepoint,后台采用SQL Server,主要分析客户S的销售数据。CS的组织结构分为部门、区域;部门和区域是相互交叉的;某个部门的总部人员能够看到全国所有区域的数据;而区域员工则只能看到该区域的数据了。用户能够查看的数据权限,需要在网页上可以进行配置。这就是客户对数据级权限的要求。

针对这些需求,数据级权限解决方案采用如下架构:

 

报表查看流程说明:

  1. 用户查看报表
  2. 报表从Cube中获取数据
  3. Cube从数据库中(记录用户的数据权限配置)获得访问用户的权限配置,根据配置返回相应的数据
  4. 报表显示结果数据
 
数据权限配置流程说明:
  1. 用户访问数据权限配置页面(由于基于Sharepoint,因此是内嵌数据权限Webpart的Sharepoint页面)
  2. 页面获取Cube结构(由于Cube的结构内容很庞大,为了避免网页响应慢,一般通过ajax树状来展示其结构)
  3. 用户修改数据权限设置,并且保存到数据库中
说明:这里面进行数据权限控制的对象为域帐号(可以为域用户或者组)。
 
纵览数据权限实现的这个流程,我们提取出中间几个重要的实现具体讲解解决方案,他们是:
  • Cube中如何进行权限控制
  • 设置数据权限时,如何读取Cube结构
Cube中如何进行权限控制
SQL Server Analysis Services本身提供了一种设置Cube数据数据权限的机制。打开Analysis Services,我们可以看到“程序集”和“角色”2个条目,他们就是和数据权限设置紧密相关的内容了。如下图所示:
  • 程序集:这是一个DLL类库,通过Visual Studio中新建一个Class Library(类库)来实现。主要作用是返回用户能够访问的Cube数据。
  • 角色:这是访问用户的角色。在这里面可以设置角色的用户,更重要的是设置Cube调用哪个程序集来获取用户能够访问的数据。
我们先来看DataSecurity.dll程序集。这个程序集的代码其实很简单,不会超过30行。其主要流程如下:
1.      读取访问用户的数据权限设置 2.      根据数据权限设置,返回一个能够访问的Dimension数据集MDX字符串
我们看看如下的主要代码(这个类库也就只需要这样一个CS文件):
namespace BI
{
    public class DataSecurity
    {
        public static string GetDimensionSet(string domain_account, string dimension)
        // 方法的名字无所谓,参数比较重要
              // domain_account:访问用户的帐号,后面我们会知道是从角色的设置中传入
              // dimension:是获取哪个维度的数据。在角色里,需要对每一个维度进行设置
        {
//return "{[Location].[City].&[Seattle]}", 返回的结果示例
            SqlConnection connection = new SqlConnection(connection_string);
 
            connection.Open();
 
            SqlCommand command = new SqlCommand("SP_Security_GetDimensionSetByLoginAccount", connection);
 
            command.CommandType = System.Data.CommandType.StoredProcedure;
 
            SqlParameter p1 = new SqlParameter("@domain_account", domain_account);
            SqlParameter p2 = new SqlParameter("@dimension", dimension);
 
            command.Parameters.Add(p1);
            command.Parameters.Add(p2);
 
            SqlDataReader reader = command.ExecuteReader();
 
            string result_set = string.Empty;
 
            int count = 0;
 
            while (reader.Read())
            {
                count++;
 
                if (result_set != string.Empty) result_set += ",";
 
                result_set += (string)reader["DimensionSet"];
            }
 
            command.Dispose();
 
            connection.Close();
 
            connection.Dispose();
 
            return "{" + result_set + "}";
        }
 
        public DataSecurity()
        {
        }
    }
}
这个类库的作用很简单,抛开BI不谈,其实他就调用了一个存储过程,把返回结果做了一个字符串拼接,然后返回这个字符串。一般的返回结果会是大致如下:{[Location].[City].&[Seattle]},这表示用户在Location维度下只能够看到Seattle的数据,其他的城市数据都看不到。当然如果是多个城市,那就是用逗号分隔的列表,比如:{[Location].[City].&[Seattle],[Location].[City].&[Washington]}。
如上所示,字符串拼接很简单,但是这些用户能够访问的具体数据记录在哪呢?这就是用户在网页上设置好数据权限,记录在数据库中的字符串了。
在这里你要更清楚地话,就需要进一步了解MDX,这不在这篇文章的讨论范围之内。
我们首先完成了第一步,结下来就是在角色设置里调用这个DataSecurity.dll类库了。这个比较简单,但是繁琐,对于Cube中的每一个维度都需要手动设置。这个步骤根据如下的示意图走就是了,没有什么代码工作。
 
 
打开SecruityRole角色的属性,进入“维度数据”中就可以设置数据权限了。每一个需要控制数据权限的维度和属性都需要设置下,基本上设置为一条语句:
StrtoSet(BI.DataSecurity.GetDimensionSet(USERNAME, "City"))
对这个语句解释下:StrtoSet是将字符串转换为MDX里的数据集。USERNAME是访问者的域帐号,City则为我们自定义的参数,表示要获取City属性维度的授权数据。
到了这里,我们已经完成了很重要的一步,数据权限的主体已经实现了。但是对于用户来说,他需要有一个前端界面来设置这些数据权限。下面的内容就是为了解决这个问题,不过这里,我只挑出最重要的部分,读取Cube结构来讲,其他的部分你完全可以自己设计。在CS这个项目中,我们是做了如下工作:
  • 所有的设置界面都是Sharepoint Webpart(请参见相关内容)
  • Webpart中的ajax(这个要单独拿出来说,是因为这个部分比较麻烦),需要用到ajax的原因就是Cube的结构是很大的,如果一次性读出来,肯定是等到花儿也谢了,相信没有人会用他。
  • 后台可以控制哪些维度需要设置数据权限(有些维度不需要设置数据权限,那么就不让他在ajax树中展示出来了)
  • 有自定义的角色,这个角色不同于刚才讲的Cube中的角色。这是用户自己定义的数据权限角色,刚才的角色只需要那一个就足够了。
  • 有了角色自然有搜索、设置用户、设置角色的数据权限、编辑、删除
这个都可以根据你的需要进行设计,不一定要完全相同,就比如我用了ajax树来展示Cube结构,但是你可以采用别的方式。
下面我们就进入下一个重要话题,就是读取Cube的结构(其实在读取这个结构本身,前面的数据权限就已经开始起作用了,没有权限访问的数据不会出现在ajax树中)。
设置数据权限时,如何读取Cube结构
读取Cube的结构,微软已经提供了一套非常丰富的类库给我们。这里就简单介绍下,如果你在具体实现过程中遇到了问题,相信上Google是一个最好的办法。
下面我们介绍读取Cube结构的具体内容,首先就是连上你的Cube。这主要通过如下语句完成。
AdomdConnection adomdConnection = new AdomdConnection();
adomdConnection.ConnectionString = “Data Source=localhost;Catalog=MyCube;”;
 
adomdConnection.Open();
 
// 这里读取具体的Cube结构
 
adomdConnection.Close();
adomdConnection.Dispose();
在连上Cube之后,通过adomdConnection可以读取整个Cube了。主要涉及到如下内容:
  • CubeCubeDef cube = adomdConnection.Cubes[“MyCubeName”];
  • Dimensioncube.Dimensions,这里面是所有的Dimension。
  • Hierarchydimension.Hierarchies,所有的层次
  • Memberhierarchy.Levels[0].GetMembers(),所有成员
通过以上几个内容就可以把整个Cube的结构完全展现出来,有了他们,就看你怎么展示你的Cube数据了。当然了,首先我们不能忘了添加一个引用:
Microsoft.AnalysisServices.AdomdClient
结束语
到这里为止,我介绍了BI数据权限解决方案中涉及到的最重要的内容,基于这些内容,你可以实现自己的BI数据权限解决方案,并且把它应用到你的项目中去,从而给你的项目添上亮点。
当然了,实现整个解决方案还是需要花费很多时间的,毕竟对用户来说,他们需要一个简单易用的结果。这些友好的界面工作都需要留给你来做。

posted @ | Feedback (1) |

Monday, February 26, 2007

RSS博客镜像是CommunityServer在较高级版本中(好像是从2.1开始)推出的功能,其主要的作用是,将其它博客(博客软件或者博客服务提供商)中的日志通过解析RSS方式自动发布到CommunityServer中。

这是一个相当实用的功能,有了RSS博客镜像,就不需要在多处同时发布博客日志了。比如你同时在新浪上有自己的博客,又同时有自己的个人博客站点,那么你只需要在新浪上发布博客日志,而个人博客站点通过RSS博客镜像功能将新浪博客上发布的新日志自动更新到站点中。

我们在肥猫博客中,也提供了这样的一个功能,在这里就此介绍下实现方法,这个实现方式是针对ASP.NET的,至于asp,由于其天生的局限性,可能无法直接在Web应用程序中完成这一功能,而需要额外的Windows应用程序的支持。

1. 管理RSS博客镜像

首先我们需要在肥猫博客软件系统中管理好RSS博客镜像,需要管理的内容有:

l         RSS地址

l         更新频率

l         最后更新的时间

需要管理的内容如图所示,这里所列出的3个是必须的字段,当然根据你自己系统的需要,你可能需要更多的字段。

这部分工作由于和肥猫博客系统相关性比较强,如果你需要自己实现RSS博客镜像,和你本身的系统也是比较相关的,这里就不再多做介绍,你根据自己系统的需要去实现就行。

2. 通过ASP.NET的定时器来抓取RSS

有了上面的对每个RSS种子定时更新的信息,就需要在ASP.NET中设置一个定时器来定时更新这些RSS种子内容了。

这部分内容请主要参考如下文章《在 ASP.NET 中使用计时器(Timer)》,根据这篇文章实现定时器就足够了。下面我主要介绍下在肥猫博客中的实现方式:

这下面的代码都在Global.ascx.cs中实现。

        protected void Application_Start(Object sender, EventArgs e)

        {

   

            SetApplicationStatus(RssMirror, true);

 

            // 设置定时器

            System.Timers.Timer timer = new System.Timers.Timer();

 

            timer.Elapsed += new System.Timers.ElapsedEventHandler(this.RefressRssMirror);

           

            timer.Interval = 300000;  // 5分钟中触发定时器

            timer.AutoReset = true;

           

            timer.Enabled = true;

 

        }

 

        private void SetApplicationStatus(string keyword, object result)

        {

            Application.Lock();

            Application[keyword] = result;

            Application.UnLock();

        }

 

        private void UpdateRssMirror(fmRssMirror item, fmblog.Data.DataProviders.DataProvider provider)

        {

            try

            {

                DateTime now = DateTime.Now;

 

                XmlDocument doc = new XmlDocument();

 

                // 载入RSS种子

                doc.Load(item.FeedUrl);

 

                // 解析RSS种子内容

                fmPostCollection list = fmFeedParser.GetPosts(item.UserName, doc, item.LastUpdateTime);

 

                for(int i=list.Count-1; i>=0; i--)

                {

                    // 根据标题,判断博客日志是否已经存在

                    if(provider.GetPost(item.UserName, list[i].Title)==null)

                    {

                        provider.NewPost(list[i], string.Empty);

                    }

                }

 

                // 设置最后更新时间

                provider.SetRssMirrorLastUpdateTime(item.Id, now, item.UserName);

            }

            catch(Exception)

            {

            }

        }

 

        protected void RefressRssMirror(object sender, System.Timers.ElapsedEventArgs e)

        {

            // 判断上次触发的定时器是否已经完成

            if((bool)Application[RssMirror])

            {

                SetApplicationStatus(RssMirror, false);

 

                try

                {

                    fmblog.Data.DataProviders.DataProvider provider = fmblog.Data.DataProviders.DataProvider.CreateInstance(Application);

 

                    provider.OpenConnection();

 

                    try

                    {

                        fmRssMirrorCollection list = provider.GetAllRssMirrors();

 

                        DateTime now = DateTime.Now;

                        // 更新RSS博客镜像里的所有RSS种子

                        foreach(fmRssMirror item in list)

                        {

                            TimeSpan span = now - item.LastUpdateTime;

 

                            if(span.Hours>=item.UpdateInterval)

                            {

                                UpdateRssMirror(item, provider);

                            }

                        }

                    }

                    catch(Exception)

                    {

                    }

               

                    provider.CloseConnection();

                }

                catch(Exception)

                {

                }

 

                SetApplicationStatus(RssMirror, true);

            }

        }

       这段代码主要是在Application_Start函数中创建一个5分钟的定时器,定时器的作用就是载入所有的RSS博客镜像种子,然后将新内容更新到系统中。

3. 如何支持多协议(RSSATOMRDF等),支持协议的多种版本

在实现RSS博客镜像时,我们不得不面对一个问题,就是多种协议的支持,并且对每个协议可能还有版本的区别;这样就导致我们必须进行多协议,多版本的支持。

为了解决这个问题,我们必须研究好RSSATOMRDF协议,及他们的多种版本,比如RSS1.02.0等等。

我们在研究中发现,一些字段有在协议中有多种表达方式,不如内容这个字段,可能存在"content:encoded", "description", "dc:description"等等XML表达方式,并且他们还有优先级的区别。

很多的RSS种子,在content:encoded中记录全文,而在description中记录摘要。因此,我们设置了如下的函数来统一处理这个问题,大大的方便了兼容性。

        public static string GetNodeData(XmlNode node, params string[] argv)

        {

            foreach(string arg in argv)

            {

                foreach(XmlNode subnode in node.ChildNodes)

                {

                    if(subnode.Name == arg)

                    {

                        return subnode.InnerText;

                    }

                }

            }

 

            return string.Empty;

        }

这个函数会接收一串XML标签记号,然后在父结点中按照优先级别来读取值。其调用方式如下:

post.Content=GetNodeData(node,"content:encoded","description","dc:description");

post.PublishTime = GetTime(GetNodeData(node, "pubDate", "dc:date"));

这个函数是我们解决多版本的一个办法。因为大部分版本其主体结构都差不多,只是有些XML标签记号的区别(各种博客系统在记号处理上也存在少许差别)。

至于各种协议,你按照标准协议来写代码解析就可以了,这里不再作阐述。可能存在的一个问题就是,需要测试多种RSS种子才能够知道RSS博客镜像功能的兼容性,因为很多种子的处理方式都不同。

4. ACCESS数据库会遇到的小问题

在处理ACCESS数据库的时候,我们遇到了一个小问题,那就是定时器触发的函数中HttpContext.Current为空,这让我们无法通过MapPath方法取得ACCESS数据库的绝对路径。

为了解决这个问题,我们采用了一点投机取巧的方法,那就是将这个绝对路径记录在Application的全局变量中。这个记录动作在第一次访问肥猫博客后发生(因为第一次访问肥猫博客,就会读取数据库,而IE的访问HttpContext.Current是有值的),因此为了启动RSS博客镜像,需要进行一次浏览器的访问。

这个问题我们调试了大半天才搞定,如果你用的也是ACCESS数据库,希望这个解决办法对你也有用处。

如果你是SQL Server数据库,就不会存在这个问题。

基本上,有了以上几个内容,你就可以实现自己的RSS博客镜像功能了。

posted @ | Feedback (5) |

Monday, January 29, 2007

数据库的自动安装、升级是一个非常实用的功能,在初次安装软件,升级软件的时候可以节省大量的时间和无趣的工作,而且也节省了给用户写数据库升级指导的说明。

要实现数据库的自动安装、升级,其实并不难。我们在肥猫博客中,提供了这样的功能。每次程序升级,自己用起来都觉得非常地爽,不用繁琐的指导用户,也不用无趣的操作。那么在这里我们就介绍下如何像肥猫博客一样在asp.net中提供这样的功能。

首先,我们要对sql语句非常熟悉,数据库的自动安装和升级都是基于sql脚本的,准备好脚本文件是必须的。

一般来讲,会存在多个脚本文件:

1.         第一次安装所需要的脚本文件

2.         每一次升级所需要的脚本文件。这个升级脚本文件只需要提供从上一版本升级过来所需要的Sql语句就可以,其他的都不需要。

建立一份版本历史文件,这一般可以是一个xml文件,类似如下:

<?xml version="1.0" encoding="utf-8"?>

<sqlscripts>

    <sqlscript dbversion="1.1" dspt="V1.0V1.10的升级包:增加了皮肤的数据" filename="v1_1.sql" />

    <sqlscript dbversion="1.11" dspt="V1.10V1.11的升级包:增加了订阅中最新文章的处理" filename="v1_11.sql" />

    <sqlscript dbversion="1.2" dspt="V1.11V1.20的升级包:增加了垃圾信息、IP管理" filename="v1_2.sql" />

    <sqlscript dbversion="1.3" dspt="V1.20V1.30的升级包:增加了流量统计分析" filename="v1_3.sql" />

    <sqlscript dbversion="1.4" dspt="V1.30V1.40的升级包:增加了我读,我看" filename="v1_4.sql" />

</sqlscripts>

dbversion指的是数据库的版本号,dspt是对应的说明,filename就是上面提到的Sql脚本文件了。我们每次升级程序中的功能时,如果有数据库的升级,都应该增加对应的数据和Sql脚本文件。

另外还要做的准备工作是,要在程序中记录当前程序的版本codeversion,在数据库中记录当前数据库的版本dbversion。需要升级哪些sql脚本,是通过比较这两个版本值决定的。也就是根据codeversion和dbversion的值来确定我们应该执行上面列出的哪些脚本文件。

上面说的都是一些准备工作,下面我们可以开始具体的数据库自动安装、升级的程序开发了。

1.         提供类似如下的界面,告诉用户当前的版本,以及将会进行哪些升级工作。