模型(Model)在Play应用中处于非常核心的地位,是应用对操作信息的特定域的表现形式。
Martin fowler做了如下的定义:模型层负责表示业务概念,业务状况的信息及其规则。尽管保存这些内容的技术细节由基础架构来完成,但反映了上述信息是在模型层中被控制和使用的,因而在软件业务开发当中处于非常核心的地位。
普遍使用的Java设计模式是尽可能地将模型定义为一些简单的Java Bean,然后将应用的业务逻辑放到用来操作这些模型的service层。在Play中,将这种传统的设计模式定义为反模式。
反模式:在开发、设计、管理中采用的糟糕的解决方案。
与优秀的改进型模式相反,反模式告诉我们应该尽量避免这些糟糕的模式而采用优秀的模式,以此起到警示作用。但反模式不是固定的,其中的“良好”或“糟糕”是对应于一定的环境而言的,因为一种良好的设计如果应用在错误的环境下也可能成为一种反模式。
Martin fowler将上述的这种反模式命名为贫血模型(Anemic object model):贫血域模型(Anemic Domain Model)最基本的表现是乍一看跟真实世界中的区别不大,有对象,有许多以名词命名的域空间。将这些对象与那些真实域模型进行对照,会发现两者在宏观的关系和结构方面有着紧密的联系。但美中不足的是当我们仔细观察其行为时,会意识到除了getXxx和setXxx方法以外很难发现有基于对象自身的操作。这些模型拥有约定俗成的设计规则:不要将业务逻辑放到域模型中,取而代之的是将这些逻辑交给上面的service层去处理,间接使用模型来对数据进行操作。
这个反模式最大的恐怖之处在于,它与面向对象设计中的基本概念背道而驰,因为它没有将数据和操作放在一起。贫血域模型实际上仅仅是面向过程的设计风格,而且这种设计风格是面向对象的设计者们所反对的。可现在的情况是很多人认为贫血对象就是真实的对象,因此完全错过了去挖掘面向对象设计究竟是怎么一回事的机会。
1、域模型属性模拟
查看Play提供的示例应用,模型类里面会频繁地使用声明为public的变量。即使是经验尚浅的Java开发者,也懂得慎用public类型的变量。在Java开发中(当然还有其他的面向对象语言),实践经验是这样告诉我们的:将所有的成员变量声明为私有,只提供获取与修改的方法。这样做的目的在于增强程序的封装性,而“封装”在面向对象设计中恰恰是非常关键的概念。
Java没有真正的内置属性定义机制,而是使用Java Bean来进行约束:Java对象的属性通过一对getXxx/setXxx的方法来修改,如果对象是只读的那么只需要提供getXxx方法。在过去的开发中我们一直这样做,但是编码过程就显得有些乏味了。每个属性必须声明为private,同时还有相应的getXxx/setXxx方法,而且大多数情况下,getXxx和setXxx方法的实现都是类似的。
private String name;
public String getName() {
return name;
}
public void setName(String value) {
name = value;
}
Play框架的模型部分会自动生成getXxx/setXxx方法,保持代码的简洁。也就是说,在Play中开发者可以直接把属性变量声明为public,运行时Play会自动生成相应的getXxx/setXxx方法(在这里我们将声明为public的字段都视为属性)。
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;
}
}
因为变量被声明为public,可以使用如下方式对属性进行操作:
product.name = "My product";
product.price = 58;
product.setName("My product");
product.setPrice(58);
因为这些getXxx/setXxx方法是在运行时动态生成的,所以不能直接调用。如果在编码阶段使用他们,编译器会因为找不到该方法而报错误。
当然也可以自己定义相应的getXxx/setXxx方法,Play会优先选择手动编写的方法。如果需要保护Product类的price属性就可以定义setPrice()方法:
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
@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;
}
}
@Entity注解的作用是通知Play自动开启JPA实体管理器,@Required是对该属性的约束。该类继承于play.db.jpa.Model,Model提供了非常简单的对象处理方式,在后面章节会做详细介绍。
针对以上例子可以进行如下测试断言:
Data data = new Data();
data.anotherValue = null;
assert data.anotherValue == 0;
data.anotherValue = 4
assert data.anotherValue == 8;
2、数据库配置
通常情况下,开发者需要将模型对象持久化。最常用的方法是把这些数据保存到数据库中。
在Play应用的开发过程中,开发者可以迅速配置嵌入式内存数据库或者直接将数据保存到文件系统中。开启内存数据库H2,只需要在conf/application.conf文件中进行如下配置:
db=mem
H2是开放源代码的Java数据库,其具有标准的SQL语法和Java接口,可以自由使用和分发,且非常简洁和快速。将数据保存在内存中相比从磁盘上访问能够极大地提高应用的性能,但由于内存容量的限制,内存数据库适用于开发阶段,或者原型示例开发。
如果需要将数据保存在文件系统中,则使用如下配置:
db=fs
db=mysql:user:pwd@database_name
Play框架集成了H2数据库和MySQL数据库的驱动程序,存放在$PLAY_HOME/framework/lib/目录下。如果需要使用PostgreSQL,Oracle或者其他数据库,需要在该目录(或者应用程序的lib/目录)下添加相应的数据库驱动。
Play可以连接任何JDBC兼容的数据库,只需要将相应的驱动类库添加到/lib目录中,并在conf/application.conf文件中定义JDBC配置:
db.url=jdbc:mysql://localhost/test
db.driver=com.mysql.jdbc.Driver
db.user=root
db.pass=123456
jpa.dialect=<dialect>
由于不同的数据库产品支持不同的ANSI SQL标准,所以Hibernate必须要使用“方言”才能与各种数据库成功的进行通信。在Play中,大多数情况下会自动根据配置信息识别特定数据库方言,但是存在某些数据库,Play无法判断其使用的方言。这时就需要开发者显式地在Play配置文件中指定。
除了使用Hibernate外,在编码时还可以直接从play.db.DB中获得java.sql.Connection,然后使用标准SQL语句来执行数据库操作。
Connection conn = DB.getConnection();
conn.createStatement().execute("select * from products");
3、数据持久化
Play的持久层框架采用的是Hibernate,使用Hibernate(通过JPA)自动地将Java对象持久化到数据库。当在任意的实体类上增加@javax.persistence.Entity注解后,Play会自动为其开启JPA实体管理器。
@Entity
public class Product {
public String name;
public Integer price;
}
Play应用开发者一开始可能经常会犯的错误是使用Hibernate的@Entity注解来取代JPA。这里请读者注意,Play是直接调用JPA的API来使用Hibernate。
也可以直接从play.db.jpa.JPA对象中得到实体管理器,通过实体管理器可以将Model持久化到数据库或者执行HQL语句,例如:
EntityManager em = JPA.em();
em.persist(product);
em.createQuery("from Product where price > 50").getResultList();
Play为JPA的使用提供了非常好的支持,只需要继承Play提供的play.db.jpa.Model类:
@Entity
public class Product extends Model {
public String name;
public Integer price;
}
Product.find("price > ?", 50).fetch();
Product product = Product.findById(2L);
product.save();
product.delete();
ActiveRecord也属于ORM层,由Rails最早提出,遵循标准的ORM模型:表映射到记录,记录映射到对象,字段映射到对象属性。配合遵循的命名和配置惯例,能够很大程度的快速实现模型的操作,而且简洁易懂。Play也提倡使用ActiveRecord模式进行快速开发,其主要思想是:
4、无状态模型
Play被设计成为“无共享”的架构,目的就是为了保持应用的完全无状态化。这样做的好处在于可以让一个应用同一时刻在多个服务器节点上运行。
Play为了保持模型无状态化,需要避免一些常见的陷阱,最重要的就是不要因为多请求而将对象保存到Java堆中。Play应用中多请求之间保存数据有以下几种解决方案:
1.如果数据很小而且非常简单,那么可以将其存储在Session或者Flash作用域,但是这些作用域最大只允许存放4K的内容,并且存储的数据只能为字符串类型。
2.将数据保存到持久化存储中(数据库或者文件系统)。比如用户创建了需要跨越多个请求的对象,就可以按照以下步骤对其进行操作:
3.将数据保存在瞬时存储中(比如Cache):
根据具体应用的需求,第三种解决方案使用缓存可以是一种非常好的选择,也是Java Servlet Session的良好的替代方案。但缓存并不是可靠的数据存储方式,因此如果选择将对象保存到缓存中,就必须确保能够将它重新读取回来。