65.9K
CodeProject 正在变化。 阅读更多。
Home

构建健壮的中间层

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.40/5 (18投票s)

2004年10月6日

6分钟阅读

viewsIcon

65814

downloadIcon

645

构建快速、健壮的中间层有许多解决方案中的一种。

引言

三层架构是一种客户端-服务器架构,其中用户界面、功能性流程逻辑(“业务规则”)以及数据存储和数据访问被开发并维护为独立的模块,通常运行在不同的平台上。在本文中,我将展示一种构建健壮的中间层的设计解决方案,这种中间层易于维护且易于修改,而不会影响其他层。

对象关系持久化

对象关系持久化框架为您的业务对象提供基本的数据 CRUD(创建、检索、更新和删除)服务。持久化框架实际上不负责做任何超出这些范围的事情。如果您的应用程序有业务逻辑(我无法想象一个不需要业务逻辑的业务应用程序开发项目),那么持久化框架就不是负责处理这些的架构部分。

协调业务逻辑与持久化是更高级别框架的责任。这就是 Microsoft Business Framework 的职责所在。持久化框架的唯一工作是对业务对象执行操作,这些操作对应于 SQL insertselectupdatedelete 命令。

一个好的持久化框架还应该协调相关业务对象之间的持久化消息,协调基本事务,管理乐观并发,并将数据库生成的键传播回中间层。持久化框架还提供机制——通常以某种元数据的形式——允许将业务对象映射到数据库表,并将业务对象属性映射到表属性。

持久化框架具有理解业务对象结构的智能,并且可以推断出对底层持久化数据存储执行 CRUD 操作所需的 SQL 命令文本。

使用代码

本文将开发一个示例对象存储库,该存储库利用 SQL Server 的 XML 扩展在 XML 和关系数据结构之间进行转换。在下面的示例中,我使用了 SQLXML 3.0 和 SQL Server 2000 的 Pubs 数据库中的 Jobs 实体。

首先,我们准备存储过程 CRUD(创建、检索、更新、删除)

----------------------Select Job by job_id---------------------------------

CREATE PROCEDURE dbo.SelectJob
    @job_id smallint
AS
    SELECT    1            as Tag,
            null        as Parent,
            job_id        as [Job!1!JobId!element],
            job_desc    as [Job!1!JobDesc!element],
            min_lvl        as [Job!1!MinLvl!element],
            max_lvl        as [Job!1!MaxLvl!element]
    FROM jobs
    WHERE job_id = @job_id
    FOR XML EXPLICIT

GO

----------------------Select all Jobs---------------------------------

CREATE PROCEDURE dbo.SelectJobs
AS
    SELECT    1            as Tag,
            null        as Parent,
            null        as [JobCollection!1!],
            null        as [Job!2!JobId!element],
            null        as [Job!2!JobDesc!element],
            null        as [Job!2!MinLvl!element],
            null        as [Job!2!MaxLvl!element]
    UNION ALL
    SELECT  2,
            1,
            null,
            job_id,
            job_desc,
            min_lvl,
            max_lvl
    FROM jobs
    FOR XML EXPLICIT
GO

----------------------Update Job by job_id---------------------------------

CREATE PROCEDURE dbo.UpdateJob
    @job ntext
AS
BEGIN
    DECLARE @idoc int
    DECLARE @job_id smallint
    EXEC sp_xml_preparedocument @idoc OUTPUT, @job
    
    SELECT @job_id = job_id
    FROM OPENXML(@idoc, '/Job')
    WITH (JobId smallint './JobId')
    
    UPDATE jobs
    SET jobs.job_desc = xml.JobDesc,
        jobs.max_lvl = xml.MaxLvl,
        jobs.min_lvl = xml.MinLvl
    FROM OPENXML (@idoc, '/Job')
    WITH (JobDesc varchar(50) './JobDesc',
          MaxLvl tinyint './MaxLvl',
          MinLvl tinyint './MinLvl') xml
    WHERE jobs.job_id = @job_id
        
    EXEC sp_xml_removedocument @idoc
END
GO

----------------------Insert Job---------------------------------

CREATE PROCEDURE dbo.InsertJob
    @job ntext
AS
BEGIN
    DECLARE @idoc int
    EXEC sp_xml_preparedocument @idoc OUTPUT, @job
    
    INSERT INTO jobs (job_desc, min_lvl, max_lvl)
    SELECT JobDesc, MinLvl, MaxLvl
    FROM OPENXML(@idoc, './Job')
    WITH (JobDesc varchar(50) './JobDesc',
          MaxLvl tinyint './MaxLvl',
          MinLvl tinyint './MinLvl')
        
    EXEC sp_xml_removedocument @idoc
    
    SELECT @@IDENTITY AS id FOR XML RAW
    
END
GO

----------------------Delete Job---------------------------------

CREATE PROCEDURE dbo.DeleteJob
    @job_id smallint
AS
    DELETE FROM jobs WHERE job_id = @job_id

GO

现在,我们可以基于 Pubs 数据库的 Jobs 表创建业务对象

using System;
using System.Collections;

namespace Database.Pubs
{
    public interface IJob
    {
        short JobId {get; set;}
        string JobDesc {get; set;}
        byte MaxLvl {get; set;}
        byte MinLvl {get; set;}
        
        // Job has collection of employee

        // add this property later after you build Employee,

        // EmployeeCollection, EmployeeDB, EmployeeBiz classes

        // I include this property to show how you can make

        // relationship between entities(tables)

        // this should be read only

        EmployeeCollection Employees {get;}        
    }
    
    [Serializable]
    public class Job : IJob
    {
        private short job_id = -1;
        private string job_desc;
        private byte min_lvl;
        private byte max_lvl;
        
        // add this field later

        private EmployeeCollection employees;
        
        public Job() {}
        
        public short JobId
        {
            get
            {
                return job_id;
            }
            set
            {
                job_id = value;
            }
        }
        
        [Required]
        [Length(50)]
        public string JobDesc
        {
            get
            {
                return job_desc;
            }
            set
            {
                job_desc = value;
            }
        }
        
        [Eval("[this]>=10")]
        [Required]
        public byte MinLvl
        {
            get
            {
                return min_lvl;
            }
            set
            {
                min_lvl = value;
            }
        }
        
        [Eval("[this]<=250")]
        [Required]
        public byte MaxLvl
        {
            get
            {
                return max_lvl;
            }
            set
            {
                max_lvl = value;
            }
        }
        
        // add this property later after we build Employee,

        // EmployeeCollection, EmployeeDB, EmployeeBiz classes

        // I include this property to show how you can make

        // relationship between entities(tables)

        // In this example I am not going to build Employee object,

        // you can do it by yourself at the end of this article

        public EmployeeCollection Employees
        {
            get
            {
                if (employees == null)
                {
                    EmployeeBiz biz = new EmloyeeBiz();
                    employees = biz.GetEmployees(this.JobId);
                }
                return employees;
            }
        }
    }
    
    // here we create our job collection class

    // (basic data collection container)

    
    [Serializable]
    public class JobCollection : ObjectCollection
    {
        public JobCollection()
        {
            
        }
        
        public void Add(Job job)
        {
            base.Add(job);
        }
        
        public void Delete(int index)
        {
            base.RemoveAt(index);
        }
        
        public Job this[index]
        {
            get
            {
                return (Job)base.List[index];
            }
            set
            {
                base.List[index] = value;
            }
        }
    }
}

我们有 JobJobCollection 类,它们被映射到 Jobs 表。我使用了自定义属性(System.Attribute)来指定每个属性的验证,这将会在我们持久化(AddUpdateJob 到数据库之前验证 Job 类。您可以创建自己的自定义属性来进一步描述类属性。在此示例中,我使用了

  • RequiredAttribute - 验证数据(是否允许 null)
  • EvalAttribute - 验证数据(匹配特定的数学函数),基于表列的 CHECK 约束
  • LengthAttribute - 验证数据的允许长度
  • RegexpAttribute - 验证数据(特定的正则表达式),基于表列的 CHECK 约束

如果数据有效,我们将继续更新或插入数据到数据库,否则我们将抛出错误。这是在将数据发送到数据库之前验证数据输入的一个非常重要的步骤。验证必须在业务逻辑层进行。有些应用程序使用客户端验证而没有服务器端验证。这可能会造成问题。让我解释一下为什么我们需要在中间层中使用验证。我们的中间层不依赖于客户端应用程序,无论是 Web 应用程序、Web 服务还是 Windows 应用程序。有些应用程序可能不在我们的控制之下,并且不会实现与我们的业务层相同的验证;例如,您可以提供的 Web 服务。在这种情况下,为了防止不良数据进入我们的数据库,我们必须在中间层实现验证。

此外,您可以通过指定 XmlElement 属性来按需更改类的序列化。例如,[XmlElement(ElementName = "Job_Id")]。在这种情况下,您的序列化 XML 中将是 Job_Id 元素,而不是 JobId 元素。

好的,现在是时候构建 Job 数据访问层了,稍后,在其之上,我们将创建 Job 业务层,在那里我将向您展示如何验证类属性值

using System;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Microsoft.Data.SqlXml;

namespace Database.Pubs
{
    public class DbHelper
    {
        public static readonly string PUBS = 
           "provider=SQLOLEDB;Database=pubs;" + 
           "Server=localhost; Integrated Security=SSPI";
        
        public static object DeserializeObject(string root, 
                            System.Type type, XmlReader reader)
        {
            XmlRootAttribute xmlRoot = new XmlRootAttribute();
            xmlRoot.ElementName = root;
            XmlSerializer serializer = new XmlSerializer(type, xmlRoot);
            return serializer.Desirialize(reader);
        }
        
        public static string SerializeObject(object obj, System.Type type)
        {
            XmlSerializer serializer = new XmlSerializer(type);
            StringBuilder sb = new StringBuilder();
            TextWriter writer = new StringWriter(sb);
            serializer.Serialize(writer, obj);
            writer.Close();
            return sb.ToString();
        }
    }
    
    public class JobDB
    {
        public JobDB() {}
        
        public XmlReader GetJob(short jobId)
        {
            SqlXmlCommand command = new SqlXmlCommand(DbHelper.PUBS);
            command.CommandType = SqlXmlCommandType.Sql;
            command.CommandText = "exec SelectJob ?";
            command.CreateParameter().Value = jobId;
            return command.ExecuteXmlReader();
        }
        
        public XmlReader GetJobs()
        {
            SqlXmlCommand command = new SqlXmlCommand(DbHelper.PUBS);
            command.CommandType = SqlXmlCommandType.Sql;
            command.CommandText = "exec SelectJobs";
            return command.ExecuteXmlReader();
        }
        
        // job_id is an identity so we need to return new identity id

        public short AddJob(string serializedJob)
        {
            short jobId = -1;
            SqlXmlCommand command = new SqlXmlCommand(DbHelper.PUBS);
            command.CommandType = SqlXmlCommandType.Sql;
            command.CommandText = "exec InsertJob ?";
            command.CreateParameter().Value = serializedJob;
            XmlReader reader = command.ExecuteXmlReader();
            if (reader.Read())
            {
                reader.MoveToAttribute("id");
                jobId = Convert.ToInt16(reader.Value);
            }
            if (jobId <= 0)
                throw new Exception("Insert operation is failed");
                
            return jobId;
        }
        
        public void UpdateJob(string serializedJob)
        {
            SqlXmlCommand command = new SqlXmlCommand(DbHelper.PUBS);
            command.CommandType = SqlXmlCommandType.Sql;
            command.CommandText = "exec UpdateJob ?";
            command.CreateParameter().Value = serializedJob;
            command.ExecuteNonQuery();
        }
        
        public void DeleteJob(short jobId)
        {
            SqlXmlCommand command = new SqlXmlCommand(DbHelper.PUBS);
            command.CommandType = SqlXmlCommandType.Sql;
            command.CommandText = "exec DeleteJob ?";
            command.CreateParameter().Value = jobId;
            command.ExecuteNonQuery();
        }        
    }
    
    public class JobBiz
    {
        private JobDB db;
        
        public JobBiz()
        {
            db = new JobDB();
        }
        
        public Job GetJob(short jobId)
        {
            XmlReader reader = db.GetJob(jobId);
            if (!reader.Read())
                return null;
            Job job = (Job)DbHelper.DeserializeObject("Job", typeof(Job), reader); 
            reader.Close();
            return job;
        }
        
        public JobCollection GetJobs()
        {
            XmlReader reader = db.GetJobs();
            JobCollection coll = 
              (JobCollection)DbHelper.DeserializeObject("JobCollection", 
              typeof(JobCollection), reader); 
            reader.Close();
            return coll;
        }
        
        public short AddJob(Job job)
        {
            string message;
            EntityValidationRule rule = new EntityValidationRule();
            if (!rule.Validate(job, out message))
                throw new Exception (message);
            string serializedJob = DBHelper.SerializeObject(job, typeof(Job));
            return db.AddJob(serializedJob);
        }
        
        public void UpdateJob(Job job)
        {
            string message;
            EntityValidationRule rule = new EntityValidationRule();
            if (!rule.Validate(job, out message))
                throw new Exception (message);
            string serializedJob = DBHelper.SerializeObject(job, typeof(Job));
            db.UpdateJob(serializedJob);
        }    
        
        public void DeleteJob(short jobId)
        {
            db.DeleteJob(jobId);
        }
    }
}

让我们更新我们的 JobCollection 类以添加一些持久化方法。这一步取决于您,无论您是想在对象集合中添加、更新还是删除对象时将数据持久化到数据库。我在 JobCollection 类中添加了三个方法:UpdatePersistDeletePersistAddPersist

public class JobCollection
{
    private JobBiz biz;
    
    public JobCollection()
    {
        biz = new JobBiz();
    }
    ...
    public void DeletePersistent(short jobId)
    {
        biz.Delete(jobId);
        for(int i = 0; i < this.Count; i++)
        {
            Job _job = this[i];
            if (_job.JobId == jobId)
            {
                base.RemoveAt(i);
                break;
            }
        }
    }
    
    public void AddPersistent(Job job)
    {
        job.JobId = biz.AddJob(job);
        base.Add(job);
    }
    
    public void UpdatePersistent(Job job)
    {
        biz.UpdateJob(job);
        for(int i = 0; i < this.Count; i++)
        {
            Job _job = this[i];
            if (_job.JobId == jobId)
            {
                this[i] = job;
                break;
            }
        }
    }
    ...    
}

扩展业务对象

因此,在产品 1.0 版本发布后不久,应用程序的第二个版本就开始开发了,客户正在等待 Job 对象的新需求列表。为了处理这些新增功能,我们添加了一个新类——Job2,它是原始 Job 类的子类

public class Job2 : Job
{
    private string additionalInfo;
    
    public Job2() : base() {}

    public string AdditionalInfo
    {
        get 
        {
            return additionalInfo;
        }
        set 
        {
            additionalInfo = value;
        }
    }
}

在传统的对象到关系数据库访问代码中,这些更改将意味着修改关系存储的结构。也许,在 Jobs 表中添加几个字段。(而且大家都知道,有些 DBA 对此很棘手)。然而,由于 XML 被用作到关系数据库的传输,并且数据库设计者提前计划,在初始 Jobs 表中包含了一个溢出字段,因此对象存储库可以在不更改关系数据库结构、存储过程或对象存储库代码的情况下支持新的业务对象。例如,持久化 Job2 对象的代码现在看起来像这样

Job2 job = new Job2();
job.JobDesc = "job desc";
job.AdditionalInfo = "add info";
...
JobCollection coll = new JobCollection();
coll.AddPersistent(job);

这基本上与用于原始业务对象的代码相同,唯一的区别是 Job2 对象是显式创建的。总的来说,对象存储库已被证明是一种非常可扩展的设计。事实上,除非从业务对象层次结构生成的 XML 文档的形状发生巨大变化,否则业务对象开发人员可以自由地扩展或调整对象。例如,这种调整可能非常剧烈,比如从 Job 对象中移除字段,这会破坏大多数代码。在这种情况下,OpenXML 代码将在序列化格式中找不到该元素,而是插入一个 Null 值。

因此,这里有几个值得注意的技术细节。首先,当 Job2 被添加为 Job 类的子类时,有必要向 Job 类添加 XmlInclude 属性

[XmlInclude(typeof(Job2))]
public class Job
{
   ...
}

通过包含此属性并指定新的 Job2 类型,XmlSerializer 就可以在序列化和反序列化时识别基类和派生类。对于序列化,这并不复杂,因为 XmlSerializer 可以动态确定要序列化的实例的类型。然而,在反序列化时,XmlSerializer 在从数据库获取 Job2 对象的数据时,实际上会创建 Job2 类型。XmlSerializer 是如何弄清楚这一点的?这个问题的关键在于检查数据库中的溢出内容。

结论

与任何设计一样,确实存在一些权衡。XML 作为传输显然不是最有效的数据库访问机制。有大量的标记在网络上传输。但是正如所示,通过使用 XML 作为到数据库的传输以及 OpenXML 作为持久化机制,可以显著解耦数据库层和业务逻辑层,从而开发出非常灵活和可扩展的设计。总的来说,这取决于特定应用程序的具体要求。

您可以创建自己的 JobDBJobBiz 类,以及它们连接到数据库的方式。

历史

  • 10/1/2004
© . All rights reserved.