model - niubods/playframework-notes GitHub Wiki

领域对象模型

模型在Play应用程序中占有重要地位。它是应用所操作信息的领域特定表征。

马丁·福勒这样定义它:

负责表示业务的概念、业务状态信息和业务规则。反应业务情况的状态在这里控制和使用,尽管储存它的技术细节是委托给基础设施的。该层是业务软件的核心。

一个常见的Java反模式是保持模型为一系列简单的Java Bean,然后把应用逻辑放在一个“业务”层来操作模型对象。

马丁·福勒也给这种反模式命名为 贫血模型

一个贫血领域模型的基本症状是乍一看它像个真东西。那里有很多以领域空间的名词命名的对象,而且这些对象以丰富的关系和构造连接着,正如真正的领域模型那样。美中不足的是当你去查看行为,你发现在这些对象上几乎没有任何行为,完全就是个只有getter和setter方法的袋子而已。的确这些模型来自于一条设计原则,说你不要往领域对象中加入领域逻辑。的确那里有一堆业务对象囊括了所有这些领域逻辑。这些业务对象存在于领域模型之上,拿领域模型当数据来使用。

这种反模式最根本的恐怖之处在于,它是和把数据和处理过程组合在一起的这种向对象的设计思想背道而驰。这种贫血领域模型真的只是面向过程的设计,这正是像我(还有埃里克)这样的面向对象偏执狂在Smalltalk早期所极力抗争的东西。更糟的是,好多人认为贫血对象才是真正的对象,因而全然失去了面向对象设计的要点。

属性的模拟

如果你你稍微看一下Play的示例程序,你经常会发现类里声明了一些公有变量。如果你是一个有哪怕只有一点点开发经验的Java程序员,现在看到公有变量的时候大概会警钟狂鸣了吧。在Java(还有其他面向对象的语言)中,最佳实践说要保证所有字段都是私有的并且提供访问器和修改器。这是为了保证封装性,是面向对象设计中的一个重要概念。

Java没有真正内置属性定义系统。它采用一种称为Java Bean的约定:一个Java属性通过一对getXxx/SetXxx方法来定义。如果这个属性是只读的,那么只有一个getter。

尽管这个系统运行良好,但写起来也太麻烦了。对每一个属性,你必须声明一个私有变量然后再写上两个方法。因此,多数时候getter和setter的实现总是一样的:

private String name;
 
public String getName() {
    return name;
}
 
public void setName(String value) {
    name = value;
}

Play框架会自动对模型部分生成这些模式内容,这样可以保持你的代码整洁。实际上,所有公有变量都会成为实例属性。按照约定,类中所有公有、非静态,非final字段都会被看成属性。

例如,当你像这样定义一个类:

public class Product {
 
    public String name;
    public Integer price;
}

实际加载的类会是这样:

public class Product {
 
    public String name;
    public Integer price;
 
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public Integer getPrice() {
        return price;
    }
 
    public void setPrice(Integer price) {
        this.price = price;
    }
}

然后,当你想访问属性的时候你可以只这样写:

product.name = "My product";
product.price = 58;

在加载时会转换成:

product.setName("My product");
product.setPrice(58);

警告!

如果你依赖自动生成的话你不能直接使用getter和setter方法来访问属性。这些方法是在运行时生成的。所以如果你在你写的代码中引用他们,编译器找不到这些方法就会报错。

当然你可以自己定义getter和setter方法。如果一个方法已经存在,Play会使用已经存在的访问器。

那么,要保护Product类的price属性值,你可以这么写:

public class Product {
 
    public String name;
    public Integer price;
 
    public void setPrice(Integer price) {
        if (price < 0) {
            throw new IllegalArgumentException("Price can’t be negative!");
        }
        this.price = price;
    }
}

然后如果你试图设置一个负值,会抛出一个异常:

product.price = -10: // Oops! IllegalArgumentException

Play总是会使用已经定义好的getter和setter。来看这一段代码:

@Entity
public class Data extends Model {
 
   @Required
   public String value;
   public Integer anotherValue;
 
   public Integer getAnotherValue() {
       if(anotherValue == null) {
           return 0;
       }
       return anotherValue;
   }
 
   public void setAnotherValue(Integer value) {
       if(value == null) {
           this.anotherValue = null;
       } else {
           this.anotherValue = value * 2;
       }
   }
 
   public String toString() {
       return value + " - " + anotherValue;
   }
 
}

在另一个类中,你可以尝试这些断言:

Data data = new Data();
data.anotherValue = null;
assert data.anotherValue == 0;
data.anotherValue = 4
assert data.anotherValue == 8;

是的,它起作用了。而且因为增强的类遵循Java Bean约定,当你把你的对象和一个期望一个JavaBean的库一起使用时,它会运行完美。

设置一个数据库来持久化你的模型对象

多数时候你需要永久保存模型对象数据。最自然的方式是把这些数据保存到一个数据库中。

在开发期间,通过数据库配置,你可以很快地建立一个嵌入式数据库,保存在内存中或者你应用的一个子目录下。

Play的发布版中包含了H2和MySQL的JDBC驱动,在 $PLAY_HOME/framework/lib 目录下。如果你正在使用例如PostgreSQL或者Oracle数据库,你得把JDBC驱动库放进那个目录,或者放在你应用的 lib/ 目录下。

要连接任何JDBC兼容的数据库,只需添加驱动库,然后定义JDBC属性 db.urldb.driverdb.userdb.pass

db.url=jdbc:mysql://localhost/test
db.driver=com.mysql.jdbc.Driver
db.user=root
db.pass=

你还可以用 jpa.dialect 配置一个JPA方言。

在你的代码中,稍后你可以从 play.db.DB 中获取一个 java.sql.Connection ,然后按标准方法来使用它。

Connection conn = DB.getConnection();
conn.createStatement().execute("select * from products");

使用Hibernate来持久化模型对象

你可以使用Hibernate(通过JPA)自动将你的Java对象持久化到数据库中。

当你给任意Java对象加@javax.persistence.Entity注解来定义JPA实体时,Play会自动启动一个JPA实体管理器。

@Entity
public class Product {
 
    public String name;
    public Integer price;
}

警告!

一个常见的错误是使用了Hibernate的@Entity注解而不是JPA的。要记住Play是通过JPA的API来使用Hibernate。

稍后你可以从 play.db.jpa.JPA 对象中获取EntityManager:

EntityManager em = JPA.em();
em.persist(product);
em.createQuery("from Product where price > 50").getResultList();

Play提供了一个很好的支持类来帮助你处理JPA。只需要继承 play.db.jpa.Model

@Entity
public class Product extends Model {
 
    public String name;
    public Integer price;
}

然后使用 Product 实例的简单方法来操纵 Product 对象:

Product.find("price > ?", 50).fetch();
Product product = Product.findById(2L);
product.save();
product.delete();

保持模型为“无状态”的

Play采用了“无共享”架构的操作设计。这种思想是为了保持应用完全无状态化。这样做的好处是你可以让你的应用同时跑在你所需的任意多服务器节点上。

为了使模型无状态化,什么是你需要避开的常见陷阱? 不要在多次请求中把任何对象存在Java堆内存中

当你想跨多请求保存数据时,你有几种选择:

  1. 如果数据足够小足够简单,把它存在flash或session作用域中。不过这些作用域每个都有大概4KB的限制,而且只允许字符串数据。
  2. 把数据永久地保存到持久化存储中(比如数据库)。比如你需要通过一个跨多次请求的“向导”创建一个对象:
    • 在第一次请求时初始化并保存这个对象到数据库。
    • 把新创建对象的ID保存到flash作用域。
    • 在连续请求期间,使用对象ID从数据库中取回这个对象,更新它,并再次保存。
  3. 临时将数据保存到一个暂时存储中。比如你需要通过一个跨多次请求的“向导”创建一个对象:
    • 在第一次请求时初始化对象,然后把它保存在Cache中。
    • 把新创建对象的ID保存在flash作用域中。
    • 在连续请求期间,从catch中(使用正确的key)取回这个对象,更新它,并再次保存到cache。

Cache并不是一个可靠的存储,但是如果你把一个对象保放在catche中一般你还是能取回它的。取决于你的需要,Cache可以是一个非常好的选择,而且是是Java Servlet session的良好替代品。

⚠️ **GitHub.com Fallback** ⚠️