ORM和层次结构

5

抱歉,这个问题有些混乱...但是我感觉自己像只追着自己的尾巴的狗,现在非常困惑。

我正在尝试找出开发三层架构解决方案(IL、BL、DL)中最干净的方式,其中DL使用ORM来抽象访问数据库。

到处都是人们使用LinqToSQL或LLBLGen Pro生成表示数据库表的对象,并在所有三层中引用这些类。似乎忽略了40年的编码模式 - 或者出现了范式转移,而我错过了说明为什么这样做完全可以接受的部分。

然而,似乎仍然有一些基础原则希望成为数据存储机制不可知的 - 看看刚刚发生的LinqToSQL:写了很多代码 - 只为MS放弃它...所以我想尽可能地隔离ORM部分,只是不知道如何。

因此,回到绝对基础知识,这里是我希望以非常简洁的方式组装的基本部件:

我要从以下程序集开始工作: UL.dll BL.dll DL.dll

主要的类:

一个Message类,它具有公开集合(称为MessageAddresses)的属性,该集合包含MessageAddress对象:

class Message 
{
    public MessageAddress From {get;}
    public MessageAddresses To {get;}
}

每个层的功能:

业务逻辑层(BL)向用户界面(UI)公开了一个名为 GetMessage(Guid id)的方法,该方法返回 Message 的一个实例。

BL 又把数据访问层(DL)包装起来。

DL 有一个 ProviderFactory,用于包装 Provider 实例。 DL.ProviderFactory 公开(可能是……我的问题的一部分)两个静态方法,分别称为 GetMessage(Guid id) 和 SaveMessage(Message message)。 最终目标是能够更换为针对 Linq2SQL 编写的提供程序,或者是针对 LLBLGen Pro 的另一个提供程序,或者是针对没有使用 ORM 的提供程序(如 VistaDB)。

设计目标: 我希望有层次分离。 我希望每个层只依赖于它下面的层,而不是它上面的层。 我希望 ORM 生成的类仅在 DL 层中存在。 我希望 UL 与 BL 共享 Message 类。

因此,这是否意味着:

a)Message 在 BL 中定义 b)数据库/ORM/手动表示数据库表的类(“DbMessageRecord”、“MessageEntity”或其他 ORM 称其为的任何名称)在 DL 中定义 c)BL 依赖于 DL d)在调用不具有 ref 或知道 BL 的 DL 方法之前,BL 必须将它们转换为 BL 实体(例如:DbMessageRecord)?

UL:

Main() 
{
    id = 1;
    Message m = BL.GetMessage(id);
    Console.Write (string.Format("{0} to {1} recipients...", m.From, m.To.Count));
}

BL:

static class MessageService 
{ 
    public static Message GetMessage(id)
    {
        DbMessageRecord message = DLManager.GetMessage(id);
        DbMessageAddressRecord[] messageAddresses = DLManager.GetMessageAddresses(id);

        return MapMessage(message, 
    }

    protected static Message MapMessage(DbMessageRecord dbMessage. DbMessageAddressRecord[] dbAddresses)
    {
        Message m = new Message(dbMessage.From);
        foreach(DbMessageAddressRecord dbAddressRecord in dbAddresses){
        m.To.Add(new MessageAddress (dbAddressRecord.Name, dbAddressRecord.Address);
    }
}

DL:

static class MessageManager 
{
    public static DbMessageRecord GetMessage(id);
    public static DbMessageAddressRecord  GetMessageAddresses(id);
}

问题: a)显然,这是很多工作,迟早会发生。 b)更多的漏洞 c)更慢 d)由于BL现在依赖于DL,并且引用DL中的类(例如DbMessageRecord),因此似乎由于这些类是由ORM定义的,您无法摆脱一个提供程序,并将其替换为另一个...这使得整个练习毫无意义...最好使用ORM的所有类来贯穿BL。 e)或者...需要在BL和DL之间添加另一个程序集,并且需要进行另一种映射,以使BL独立于底层DL类。
希望我能更清楚地提出问题...但我真的迷失了。任何帮助都将不胜感激。

如果出现了范式转变,要么是人们意识到数据传输对象并不太符合DRY原则,或者可能是馄饨代码比千层饼代码更受欢迎的结构。 - Jeffrey Hantin
我曾认为千层面代码是实现软件可扩展性的唯一方法(个人认为我不知道,因为我从未涉及过这样的规模)。 你是否在说使用Ravioli Code + WCF同样可以实现,并且因此千层面代码的分层已不再那么重要?而且,我不明白如何使千层面代码跨越多个ORM提供程序进行模块化。似乎需要某种映射。顺便说一下,你让我感到饥饿了。 - Ciel
5个回答

2
这有点杂乱无章,让我想起了我第一次尝试ORM和DDD的经历。我个人使用核心领域对象、消息对象、消息处理程序和存储库。因此,我的UI向处理程序发送消息,处理程序通过存储库来使我的对象恢复并在该领域对象中执行业务逻辑。我使用NHibernate进行数据访问,使用FluentNHibernate进行类型绑定,而不是松散的.hbm配置。
因此,消息传递是我UI和处理程序之间共享的所有内容,所有BL都在领域中。
我知道我的解释可能会招致惩罚,如果不清楚,我将稍后进行辩护。
个人而言,我不太喜欢代码生成的对象。
我必须继续完善这个答案。请将您的消息视为命令,而不是表示您数据库的数据实体。我将给您一个简单类的示例和一个基础设施决策,这对我非常有效,但我不能为此负责。
[Serializable]
public class AddMediaCategoryRequest : IRequest<AddMediaCategoryResponse>
{
    private readonly Guid _parentCategory;
    private readonly string _label;
    private readonly string _description;

    public AddMediaCategoryRequest(Guid parentCategory, string label, string description)
    {
        _parentCategory = parentCategory;
        _description = description;
        _label = label;
    }

    public string Description
    {
        get { return _description; }
    }

    public string Label
    {
        get { return _label; }
    }

    public Guid ParentCategory
    {
        get { return _parentCategory; }
    }
}

[Serializable]
public class AddMediaCategoryResponse : Response 
{
    public Guid ID;
}


public interface IRequest<T> : IRequest where T : Response, new() {}


[Serializable]
public class Response
{
    protected bool _success;
    private string _failureMessage = "This is the default error message.  If a failure has been reported, it should have overwritten this message.";
    private Exception _exception;

    public Response()
    {
        _success = false;
    }

    public Response(bool success)
    {
        _success = success;
    }

    public Response(string failureMessage)
    {
        _failureMessage = failureMessage;
    }

    public Response(string failureMessage, Exception exception)
    {
        _failureMessage = failureMessage;
        _exception = exception;
    }

    public bool Success
    {
        get { return _success; }
    }

    public string FailureMessage
    {
        get { return _failureMessage; }
    }

    public Exception Exception
    {
        get { return _exception; }
    }

    public void Failed(string failureMessage)
    {
        _success = false;
        _failureMessage = failureMessage;
    }

    public void Failed(string failureMessage, Exception exception)
    {
        _success = false;
        _failureMessage = failureMessage;
        _exception = exception;
    }
}


public class AddMediaCategoryRequestHandler : IRequestHandler<AddMediaCategoryRequest,AddMediaCategoryResponse>
{
    private readonly IMediaCategoryRepository _mediaCategoryRepository;
    public AddMediaCategoryRequestHandler(IMediaCategoryRepository mediaCategoryRepository)
    {
        _mediaCategoryRepository = mediaCategoryRepository;
    }

    public AddMediaCategoryResponse HandleRequest(AddMediaCategoryRequest request)
    {
        MediaCategory parentCategory = null;
        MediaCategory mediaCategory = new MediaCategory(request.Description, request.Label,false);
        Guid id = _mediaCategoryRepository.Save(mediaCategory);
        if(request.ParentCategory!=Guid.Empty)
        {
            parentCategory = _mediaCategoryRepository.Get(request.ParentCategory);
            parentCategory.AddCategoryTo(mediaCategory);
        }
        AddMediaCategoryResponse response = new AddMediaCategoryResponse();
        response.ID = id;
        return response;
    }
}

我知道这个过程会一直进行下去,但是这个基本系统在过去的一年左右时间里为我提供了很好的服务。

你可以看到处理程序允许域对象处理特定于域的逻辑。


谢谢您详细解释。好的。所以,与其传递DTO/POCO/表格表示形式,您正在传递消息结构...有点类似于EventArgs包,但是是CommandArgs包,其中包含(在我的情况下)可能是我想要返回的Message记录的Guid。 然后,您发送回一个ResponseArgs包,其中包含XXX。在您的简单情况下,XXX是值类型(ID)-但是当返回消息内容时,我们是否应该返回DbMessageRecord?或者将其展开为参数(无依赖项),以在BL中重新组装成Message? 还是使用在其他地方定义的DTO? - Ciel
ORM部分从其存储库中注入到类中的领域对象进行填充。使用类似于Castle的工具,我有一个启动程序,可以查找所有相关程序集并加载它们,然后注入它们的依赖项,并为应用程序注册所有处理程序,以便当消息传输时,它知道该将其发送到哪里。一旦使用NHibernate(如您所见,我使用所有存储库的接口,以便在需要时可以重构掉NHibernate)从存储库中填充领域对象。 - Brandon Grossutti
一旦域对象被填充,我就在那里执行所有的业务逻辑。Guid响应只是我创建新对象时经常做的事情,这样我的UI应用程序就有一个对在我的域中创建的某些东西的引用,以便如果它需要对象的DTO(消息),它可以访问它。 - Brandon Grossutti
总的来说,这种分层结构真的帮助我在过去的一年里变得非常有效率。我构建了很多客户端服务器应用程序和类似ESB的交互,因此这使我拥有一个强大的基础架构,可以从项目到项目中使用。我还将这些东西移植到不同的传输和不同的持久化层,几乎没有任何问题。这些序列化消息的好处是,如果需要,所有内容都可以重新创建,并且更容易进行测试。 - Brandon Grossutti
我的第一条规则是很久以前教给我的,也很有道理:不要担心你的数据库,它可以采取任何你想要的形式,仅仅因为你有一个带有2个字符串的对象并不意味着你的数据库也是这样。所有应用程序/模型都应该是持久性无知的,不要让数据库定义你的领域。正如Jim所说,Martin Fowler的书非常棒,Jimmy Nilsson和Eric Evans也是如此,他们改变了我编程的方式,Greg Young也在这个过程中提供了很多帮助。祝你好运,Ciel。 - Brandon Grossutti
显示剩余3条评论

2
你似乎没有理解IoC / DI(即控制反转/依赖注入)的概念。你应该避免使用静态方法,每个层次只应该依赖于下一层的接口,并将实际实例注入构造函数中。你可以将DL称为存储库、提供程序或任何其他名称,只要它是底层持久性机制的清晰抽象。
至于表示实体的对象(大致映射到表格),我强烈建议不要有两组对象(一个特定于数据库,一个不是)。只要它们是POCOs(它们不应该真正知道它们被持久化),甚至是DTOs(纯结构,没有任何行为),它们就可以被所有三个层引用。使它们成为DTOs更符合BL概念,但我更喜欢我的业务逻辑分布在我的领域对象中(“面向对象编程风格”),而不是具有BL概念(“Microsoft风格”)。
关于Llblgen我不确定,但NHibernate + SpringFramework.NET或Windsor等任何IoC都提供了相当干净的模型来支持此功能。

1

这可能是一个过于间接的答案,但去年我在Java领域中与这些问题搏斗,并发现Martin Fowler的企业应用架构模式非常有帮助(还可以看看他的模式目录)。许多模式都涉及到您正在努力解决的相同问题。它们都很好地抽象出来,帮助我组织思路,以便能够从更高的层次上看到问题。

我选择了一种使用iBatis SQL映射器来封装我们与数据库交互的方法。(SQL映射器从SQL表驱动编程语言数据模型,而像你们这样的ORM则相反。)SQL映射器返回数据传输对象的列表和层次结构,每个对象代表某个查询结果的一行。查询(以及插入、更新、删除)的参数也作为DTO传递。业务逻辑层对SQL映射器进行调用(运行此查询,执行该插入等),并传递DTO。DTO上升到表示层(UI),在那里它们驱动模板扩展机制,生成数据的XHTML、XML和JSON表示形式。因此,对我们来说,唯一向UI传递的DL依赖是一组DTO,但它们使UI比传递未打包的字段值更加流畅。
如果你将Fowler的书与其他帖子的具体帮助结合起来,你会做得很好。这是一个有很多工具和先前经验的领域,所以应该有许多良好的前进道路。

编辑:@Ciel,你说得很对,DTO实例只是一个POCO(或者在我的情况下是Java POJO)。一个Person DTO可以有一个first_name字段为“Jim”等等。每个DTO基本上对应于数据库表的一行,只是一组字段,没有更多的东西。这意味着它与DL没有紧密耦合,并且非常适合传递到UI。Fowler在第401页谈到了这些(作为第一个模式来磨练你的牙齿还不错)。

现在我没有使用ORM,它会将您的数据对象创建为数据库。我正在使用SQL映射器,它只是一种非常高效和方便的方式,用于打包和执行SQL中的数据库查询。我先设计了我的SQL(我碰巧非常了解它),然后设计了我的DTO,然后设置了我的iBatis配置,以便说,“select * from Person where personid = #personid#”应该返回一个Java Person DTO对象列表。我还没有使用ORM(例如,在Java世界中使用Hibernate),但是使用其中之一,您将首先创建数据模型对象,然后从中构建数据库。

如果您的数据模型对象具有各种特定于ORM的附加组件,那么我可以理解为什么在将它们公开到UI层之前会三思而后行。但是,您可以创建一个C#接口,仅定义POCO获取和设置方法,并在所有非DL API中使用它,并创建一个实现类,其中包含所有ORM特定的内容。
interface Person ...

class ORMPerson : Person ...

如果您稍后更改ORM,可以创建替代的POCO实现:

class NewORMPerson : Person ...

这只会影响您的DL层代码,因为您的BL和UI代码使用Person。

@Zvolkov(下面)建议采用“编码到接口,而不是实现”的方法,将此方法提升到下一个级别,建议您以这种方式编写应用程序,使所有代码都使用Person对象,并且您可以使用依赖注入框架动态配置应用程序,根据当天要使用的ORM来创建ORMPersons或NewORMPersons。


嗨,吉姆:我桌子上就有PEAA(上周由同事借给我的),但我大多数时间都在迷茫。简单的模式我能理解,但我发现我对过度抽象化并不擅长。我理解代码...但还不理解模式。*这就是我遇到问题的根源...尝试理解足够的模式以便重新开始编写代码 :-)至于你写的内容...等一下...DTO只是代表表格的类(POCOs),对吧?所以你一直使用它们到UI层...与使用ORM生成的类有些相似:没有分离? - Ciel
你是在ORM生成的类和DTO之间进行映射吗?如果是这样,在BL还是DL中进行?(有哪些依赖关系,以及方向是什么?) - Ciel

0

尝试使用存储库模式集中所有数据访问。就您的实体而言,您可以尝试实现某种翻译层,将映射您的实体,以便不会破坏您的应用程序。这只是暂时的,并且将允许您缓慢地重构代码。

很明显,我不知道您的代码基础的完整范围,请考虑痛苦和收益。


仓储库应该在BL还是DL中定义?并返回DbMessage或Message对象? 目前(查找仓储库模式的定义后),我的提供程序非常类似于该模式,提供隐藏任何查询方法引用的方法,只需要参数,并返回与底层存储机制无关的结果。唯一的问题 - 再次提问,它们返回什么,以及它们在哪里来回映射...或者不是? - Ciel

0

仅代表我的观点,可能因人而异。

当我尝试使用任何新技术时,我认为它应该满足两个标准,否则我就浪费时间了。(或者我还没有完全理解它。)

  1. 它应该简化事情,或者在最坏的情况下不会使它们更加复杂。

  2. 它不应该增加耦合或降低内聚性。

听起来你似乎感觉自己正在走相反的方向,我知道这不是LINQ或ORM的意图。

我对这些新东西的价值的看法是,它可以帮助开发人员将DL和BL之间的边界移动到更抽象的领域。DL看起来不像原始表格,而更像对象。就是这样。(我通常会花费更多的精力使用一些更重的SQL和存储过程来实现这一点,但我可能比平均水平更熟悉SQL)。但如果LINQ和ORM还没有帮助您实现这一点,我建议继续努力,但那就是隧道的尽头;简化,并将抽象边界移动一点。


嗨Le Dorfier: 事情应该会变得更容易...但很多事情是容易的...但不是好的编码。我只是还没有弄清楚ORM在其中扮演的角色。这是一个滑坡。使用Linq2SQL很容易,但是加入延迟执行后,我就没有了关注点的分离。只是..."意大利面"。 :-) - Ciel

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接