需要Spring Service,Repository,Entity设计模式建议

时间:2014-03-04 08:25:01

标签: java spring design-patterns entity

我有一个(希望)简单的软件设计问题:我想让我的实体(=持久保存到DB的域对象)有点不可变。意味着:实体应由服务创建,并且应用程序的每个其他部分都使用仅具有getter方法的interface

示例:

  1. MyController希望使用MyEntity

  2. 检索id=5
  3. MyController必须询问MyService才能获取对象:myService.getMyEntityById(5)

  4. MyService会要求MyEntityRepository从数据库中获取对象

  5. MyServiceMyEntityInterface返回MyController

  6. 包装设计:

    root
      |--- service   
      |         |--- MyService.java
      |         |--- MyServiceImpl.java
      |         |
      |         |--- MyEntity.java
      |         |--- MyEntityImpl.java
      |         |
      |         |--- MyEntityRepository.java
      |
      |
      |------- web
                |--- MyController.java
    

    思路:

    我的第一个想法是在MyEntityImpl中简单地使用受包保护的构造函数,但这对我正在使用的其他库(即Orika)不起作用。所以它必须是public

    下一个想法是使用MyEntity界面。但现在我遇到了一些问题:

    问题:

    MyService(Impl)有一个名为updateMyEntityData(MyEntity e, Data data)的方法。现在我无法确定我的服务中这个MyEntity对象实际上是MyEntityImpl的实例。当然我可以做if(e instanceof MyEntityImpl) ...,但这正是我想要做的事情。

    下一个问题是:此服务方法使用MyEntityRepository可以保存和检索MyEntityImpl个对象,但无法处理MyEntity接口。作为一种解决方法,我可以执行额外的数据库查询,但同样我想要的内容:

    void updateMyEntityData(MyEntity e, Data data) {
      MyEntityImpl impl = repo.findOne(e.getId());
      impl.setData(data);
      repo.saveToDB(impl);
    }
    

    这是一个不必要的数据库查询,因为我知道 MyEntityMyEntityImpl的一个实例并且它已由此服务创建,因此必须< / strong>是来自DB的对象。另一种可能性是使用演员:

    void updateMyEntityData(MyEntity e, Data data) {
      MyEntityImpl impl = (MyEntityImpl) e;
      impl.setData(data);
      repo.saveToDB(impl);
    }
    

    要点:

    • 只允许服务构建MyEntityImpl
    • MyService(Impl)之后必须能够修改MyEntityImpl的字段(表示:必须有设置者)
    • 避免不必要的数据库查询

    提前谢谢!

2 个答案:

答案 0 :(得分:3)

我认为你需要克服公共构造函数。只有从存储库/数据库中检索到的对象才能分配有效的身份,您可以使用它来控制更新。

是的,你可以猜出身份,但你可以做一大堆愚蠢的事情来解决你认为你正在实施的任何保护 - 最终,我可以创建一个实例,如果我选择的话,可以分配字段

现在,不变性是一个更高尚的目标,至少在多线程环境中(如果你不在多线程执行更新的环境中,那么好处就不那么明显了,而且,不值得付出代价)。

问题是与通常被突变的域实体的不可抗性冲突。解决此问题的常见方法是包括一个时间戳,指示何时提交最后一个突变并使用变异副本。以下是使用构建器模式创建变异副本的简洁方法示例:

public MyEntity
{
  private Object identity;
  private long mutated;
  private Data data;

  public MyEntity(Object identity, long mutated, Data data)
  {
    this.identity = identity;
    this.mutated= mutated;
    this.data = data;        
  }

  public Object getIdentity()
  {
    return this.identity;
  }

  public Data getData()
  {
    return this.data;
  }

  public Builder copy()
  {
    return new Builder();
  }

  public class Builder
  {
    private Data data = MyEntity.this.data;

    public Builder setData(Data data)
    {
      this.data = data;  
    }

    public MyEntity build()
    {
      return new MyEntity(MyEntity.this.identity, MyEntity.this.mutated, this.data);
    }
  }
}

调用代码如下所示:

MyEntity mutatedMyEntity = myEntity.copy().setData(new Data()).build();

虽然这种方法使事情相对干净,但它引入了多个线程同时创建多个变异副本的问题。

根据您的具体要求,这意味着您需要通过检查具有最新版本的变异时间戳来提交更改(您的saveToDB方法)时检测冲突(以避免两次数据库命中,它是最好在存储过程中做很多事情,尽管替代方法是在执行写入的类中保持身份的缓存到变异的时间戳。冲突解决方案将再次降低到您的特定要求,因为它将传播对同一实体的其他实例的更改。

答案 1 :(得分:0)

我现在使用了一种更简单的方法:

public class MyEntity {

  MyEntity() {

  }

  @Id
  private ObjectId id;
  public ObjectId getId() { return id; }

  private String someOtherField;
  public String getSomeOtherField() { return someOtherField; }
  setSomeOtherField(String someOtherField) { this.someOtherField = someOtherField; }

}

如果Entity有一些“final”字段,那么它就是第二个构造函数,因为如果它不能将字段名称映射到构造函数参数名称,Spring Data会抛出异常,这种方式总是有效:

public class MyEntity {

  protected MyEntity() {} // this one is for Spring Data,
                          // because it can't map
  MyEntity(Integer i) {   // this constructor param "i"
    this.finalInt = i;    // to a field named "i". (The
  }                       // field is called "finalInt")

  @Id
  private ObjectId id;
  public ObjectId getId() { return id; }

  private Integer finalInt;
  public Integer getFinalInt() { return finalInt; }

  private String someOtherField;
  public String getSomeOtherField() { return someOtherField; }
  setSomeOtherField(String someOtherField) { this.someOtherField = someOtherField; }

}

包布局如下:

root
  |--- service   
  |         |--- MyService.java (public interface)
  |         |--- MyServiceImpl.java (package protected class implements MyService)
  |         |
  |         |--- MyEntity.java (public class)
  |         |
  |         |--- MyEntityRepository.java (package protected)
  |
  |
  |------- web
            |--- MyController.java

现在Controller无法构造自己的Entity个对象(至少在使用构造函数时没有),并且必须使用Service(由Spring连接到ServiceImpl 1}})。

Repository无法访问Controller,因为它受到包保护,因此只能由Service使用。

只有Service(和Repository)才能修改Entity的内容,因为所有的设置者都受到包保护。

我认为这是一个非常好的解决方案,它可以防止很多错误的代码,比如

  • Controller代码中的存储库访问

  • Controller中的实体修改并保存到DB而Service无法控制

  • 通过应用程序传递无效(自构造)对象,例如没有ID。

当然,仍然可以使用反射绕过它,但这不是重点。整个事情不是关于安全性,而是关于清洁代码结构良好的应用程序,其中数据和控制流明确定义