除了属性存取方法外,任何没有参数的方法都可以用以上annotation标记。一个方法也可以使用多个annotation标记。以下是个persistent classes的例子
关于entity
listeners的调用顺序,默认listener会最先被调用;接下来,父类的listeners会先调用,然后是子类的listeners;最后,
如果entity上还有某个事件的多个callback方法,那么会按照这些callback方法在entity上声明顺序来调用。
可以使用以下两个class-level的annotation,以便在entity listeners的调用链上去掉默认listener和父类中定义的listener:
3 Metadata
通过javax.persistence 包中定义的Annotation或者XML
mapping files来指定Persistence metadata。当混合使用Annotation 和XML mapping file
的时候,如果发生冲突,那么以XML mapping file为准。
3.1 Class Metadata
3.1.1 Entity
Entity annotation用来指定entity class。它有一个属性name用来在Query中引用entity。如果不指定name,那么缺省是entity class的名字(unqualified)。
以下是个使用Entity annotation的例子:
-
@Entity
-
public
class
Company {
-
@Id
-
private int id;
-
-
@Basic
-
private String name;
-
}
-
-
@Entity
-
public
class
Address {
-
@Basic
-
private String province;
-
-
@Basic
-
private String city;
-
-
@Basic
-
private String street;
-
-
@Basic
-
private String zip;
-
}
如果使用MySQL数据库,那么用MappingTool生成的SQL如下:
- CREATE TABLE Company (id INTEGER NOT NULL, name VARCHAR(255), PRIMARY KEY (id)) TYPE = innodb;
- CREATE TABLE Address (id BIGINT NOT NULL, city VARCHAR(255), province VARCHAR(255), street VARCHAR(255), zip VARCHAR(255), PRIMARY KEY (id)) TYPE = innodb;
- CREATE TABLE OPENJPA_SEQUENCE_TABLE (ID TINYINT NOT NULL, SEQUENCE_VALUE BIGINT, PRIMARY KEY (ID)) TYPE = innodb;
JPA要求每个persistent
class都必须至少声明一个identity字段,但是OpenJPA允许不声明identity字段。如果persistent
class没有声明identity字段,那么OpenJPA会缺省增加id列作为数据库表的主键,同时采用Table
Generator的方式为id赋值,Table Generator缺省使用OPENJPA_SEQUENCE_TABLE。
3.1.2 IdClass
当某个entity class中包含多个identity 字段的时候,必须使用identity
class(例如之前提到的MagazineId类)。IdClass annotation用来指定identity
class,它的value属性是java.lang.Class 型。
3.1.3 MappedSuperclass
Mapped superclass不是entity class,但是它可以为entity class定义persistent
state和mapping information。Mapped superclass通常是抽象类。Mapped
superclass不能用于Query,或者被运用到任何EntityManager或者Query中。Mapped superclass通过
MappedSuperclass annotation指定。
以下是个使用MappedSuperclass annotation的例子:
- @MappedSuperclass
- public abstract class Document {
- @Id
- protected int id;
-
- @Version
- protected int version;
- }
-
- @Entity
- public class Contract extends Document {
- @Basic
- private String name;
- }
如果使用MySQL数据库,那么用MappingTool生成的SQL如下:
- CREATE TABLE Contract (id INTEGER NOT NULL, name VARCHAR(255), version INTEGER, PRIMARY KEY (id)) TYPE = innodb;
3.1.4 Embeddable
Embeddable annotation指定embeddable persistent class。Embeddable
instances被映射到datastore record的一部分。JPA要求Persistent class只能是entity
class或embeddable class之一。OpenJPA允许Persistent class既是entity
class,也是embeddable class。只要同时使用@Entity和@ Embeddable即可。
以下是个使用Embeddable annotation的例子:
- @Entity
- public class Company {
- @Id
- private int id;
-
- @Basic
- private String name;
-
- @Embedded
- private Address address;
- }
-
- @Embeddable
- public class Address {
- @Basic
- private String province;
-
- @Basic
- private String city;
-
- @Basic
- private String street;
-
- @Basic
- private String zip;
- }
如果使用MySQL数据库,那么用MappingTool生成的SQL如下:
- CREATE TABLE Company (id INTEGER NOT NULL, name VARCHAR(255), city VARCHAR(255), province VARCHAR(255), street VARCHAR(255), zip VARCHAR(255), PRIMARY KEY (id)) TYPE = innodb;
从以上SQL可以看出,OpenJPA并没有为Address embeddable class生成一个表,而是在Company表中包含了Address信息。
3.1.5 EntityListeners
EntityListeners annotation用来指定entity listeners,它的value属性是java.lang.Class数组型。
3.1.6 Table
Table annotation 用来指定entity class对应的数据库表名。它有以下几个属性:
- String name: 表名,缺省使用unqualified class name。
- String schema: schema名。缺省使用数据库连接的缺省schema。
- String catalog: catalog名. 缺省使用数据库连接的缺省catalog。
- UniqueConstraint[] uniqueConstraints: Unique constraints 用来确保一个或多个列的值在表中唯一。它只有一个属性columnNames。
以下是个简单的例子:
- @Entity
- @Table(name="ART", uniqueConstraints=@Unique(columnNames="TITLE"))
- public class Article {
- ...
- }
3.1.7 SecondaryTable
有时候一个逻辑上的记录被分散在多个数据库表中。通过SecondaryTable annotation可以指定entity
class的secondary tables。通过column annotation的table属性来引用这些secondary
tables。例如:
- @Entity
- @Table(name="ART")
- @SecondaryTable(name="ART_DATA", pkJoinColumns=@PrimaryKeyJoinColumn(name="ART_ID", referencedColumnName="ID"))
- public class Article {
-
- @Id
- private long id;
-
- @Column(table="ART_DATA")
- private byte[] content;
-
- ...
- }
3.2 Field and Property Metadata
JPA提供了两种访问persistent state的方法:field access和property
access。每个entity只能使用field access或property access之一来访问同一个persistence
state,子类和父类必须使用相同的访问方式来访问同一个persistence state。
如果使用field access,那么JPA会直接把persistent state注入到filed中,并直接从filed上得到persistent state的改变。Annotation直接标记在field上,例如:
- @ManyToOne
- private Company publisher;
如果使用property access,那么JPA会通过Java Bean的"getter" 和 "setter" 方法访问persistent state。Annotation标记在"getter"方法上,例如:
- @ManyToOne
- private Company getPublisher() { ... }
- private void setPublisher(Company publisher) { ... }
当使用property access 的时候,你应该只通过"getter"和"setter"方法访问persistence
state。这个类中其它的方法(例如业务逻辑方法),也应该只通过"getter"和"setter"方法来访问persistence
state。当你需要在"getter"和"setter"方法内添加其它业务逻辑的时候,你应该认识到,"getter"和"setter"方法也会被
JPA调用。
3.2.1 Transient
Transient annotation指定一个字段为non-persistent。
3.2.2 Id
Id annotation用来指定identity字段。
3.2.3 Generated Value
GeneratedValue 用来为identity字段生成唯一值,它有两个属性,分别是strategy和generator。
Strategy有以下可选值:
- GeneratorType.AUTO: 缺省值,生成策略取决于JPA的实现。
- GenerationType.IDENTITY: 在插入的时候由database设置。
- GenerationType.SEQUENCE: 根据数据库中序列生成。
- GenerationType.TABLE: 根据数据库中某个表的某个字段的值生成。
JPA支持以下两种generator,分别是Sequence Generator和Table Generator。Sequence Generator有以下属性:
- String name: 指定generator的名字。
- String sequenceName: 指定数据库中序列名。
- int initialValue: 指定序列的初始值。
- int allocationSize: 有些数据库支持预分配一定数量的序列值,来减少访问次数以提高性能。allocationSize的缺省值是50。
Table Generator有以下属性:
- String name: 指定generator的名字。
- String table: 指定数据库中表名。
- String pkColumnName: 指定数据库表的主键名。
- String valueColumnName: 指定数据库表的保存序列值的列名。
- String pkColumnValue: 指定数据库表的某个主键值。如果用同一个表来生成多个序列值,那么需要指定pkColumnValue。
- int allocationSize: 每一次访问数据库的时候,预分配序列值的数目,缺省值是50。
除了identity字段之外,OpenJPA 支持在任何字段上使用 GeneratedValue annotation。 OpenJPA额外提供了两种generator,如下:
- uuid-string: OpenJPA会生成128位的UUID,用16-character string表示。
- uuid-hex: 跟uuid-string一样,OpenJPA会生成128位的UUID,但是用32-character hexadecimal string 表示。
以下是使用GeneratedValue的一个例子:
- import javax.persistence.Entity;
- import javax.persistence.GeneratedValue;
- import javax.persistence.GenerationType;
- import javax.persistence.Id;
- import javax.persistence.TableGenerator;
-
- @Entity
- public class GenerationStrategy {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private int identity;
-
- @GeneratedValue(strategy = GenerationType.AUTO, generator="uuid-string")
- private String uuidString;
-
- @GeneratedValue(strategy = GenerationType.AUTO, generator="uuid-hex")
- private String uuidHex;
-
- @GeneratedValue(strategy = GenerationType.TABLE, generator="tg1")
- @TableGenerator(name="tg1", table="GenerationStrategyTable", pkColumnName="id", valueColumnName="sequence", allocationSize=1)
- private long tableGeneratorValue;
-
- }
以上例子中,声明了一个named generator:"tg1"。在运行时可以通过OpenJPAEntityManager.
getNamedGenerator("tg1")方法来访问它。笔者通过MappingTool生成以上例子相关SQL的代码如下(也可以通过命令行生
成),如果generateSql是false,那么MappingTool会直接更改数据库。
- boolean generateSql = true;
- String java = "./src/com/example/jpa/GenerationStrategy.java";
- if(!generateSql) {
- MappingTool.main(new String[]{java});
- System.out.println("the table successfully generated");
- } else {
- String sql = java.replace("java", "sql");
- MappingTool.main(new String[]{"-sql", sql, java});
- System.out.println("the sql file was successfully generated: " + sql);
- }
如果使用MySql数据库,那么用MappingTool生成的SQL如下:
- CREATE TABLE GenerationStrategy (identity INTEGER NOT NULL AUTO_INCREMENT, tableGeneratorValue BIGINT, uuidHex VARCHAR(255), uuidString VARCHAR(255), PRIMARY KEY (identity)) TYPE = innodb;
- CREATE TABLE GenerationStrategyTable (ID VARCHAR(255) NOT NULL, SEQUENCE BIGINT, PRIMARY KEY (ID)) TYPE = innodb;
3.2.4 Embedded Id
如果一个entity class有多个identity字段,那么你可以声明多个@Id 字段,或者声明一个@EmbeddedId字段。@EmbeddedId字段必须是一个embeddable entity class。
3.2.5 Version
Version annotation用来指定version字段。
3.2.6 Basic
跟Transient annotation不同,basic
annotation指定一个字段为persistent。可以在以下类型的字段上使用basic annotation:primitives,
primitive wrappers, java.lang.String, byte[], Byte[], char[],
Character[], java.math.BigDecimal, java.math.BigInteger, java.util.Date,
java.util.Calendar, java.sql.Date, java.sql.Timestamp, Enums, 和
Serializable types.
Basic annotation有以下两个属性:
- FetchType fetch: 指定加载方式,可选值FetchType.EAGER(缺省值)和 FetchType.LAZY。
- boolean optional: datastore 中相关字段是否允许为null,缺省值是true。
OpenJPA 可以在任何字段上进行延迟加载(lazy-load),也允许在运行时动态修改某个字段的加载方式。
3.2.7 Embedded
@Embedded字段必须是字段必须是一个embeddable entity class,它用来映射datastore record的一部分。
3.2.8 Many To One
如果多个entity A引用entity B,而且其它entity A也可能引用相同的entity B,那么称A和B之间的关系是Many To One。JPA使用ManyToOne annotation来指定这种关系。它有以下属性:
- Class targetEntity: 关联entity的Class。
- CascadeType[] cascade: 指定级连行为,缺省是空数组。
- FetchType fetch: 指定加载方式,可选值FetchType.EAGER(缺省值)和 FetchType.LAZY。
- boolean optional: 关联的entity是否必须存在。如果是false,那么这个字段不能是null,缺省值是true。
关于Cascade
Type,有以下可选值:CascadeType.PERSIST、CascadeType.REMOVE、CascadeType.REFRESH和
CascadeType.MERGE。CascadeType.ALL用来指定以上所有CascadeType,以下两种写法是等价的:
- @ManyToOne(cascade={CascadeType.PERSIST,CascadeType.REMOVE, CascadeType.REFRESH,CascadeType.MERGE})
- private Company publisher;
-
- @ManyToOne(cascade=CascadeType.ALL)
- private Company publisher;
以下是个使用ManyToOne annotation的例子:
- @Entity
- public class Publisher {
- @Id
- private int publisherId;
- }
-
- @Entity
- public class Magazine {
- @Id
- private int magazineId;
-
- @ManyToOne
- private Publisher publisher;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Magazine (magazineId INTEGER NOT NULL, publisher_publisherId INTEGER, PRIMARY KEY (magazineId)) TYPE = innodb;
- CREATE TABLE Publisher (publisherId INTEGER NOT NULL, PRIMARY KEY (publisherId)) TYPE = innodb;
- CREATE INDEX I_MGAZINE_PUBLISHER ON Magazine (publisher_publisherId);
如果不希望使用OpenJPA缺省的关联字段名(例如publisher_publisherId),那么可以使用JoinColumn annotation,例如:
- @Entity
- public class Publisher {
- @Id
- private int publisherId;
- }
-
- @Entity
- public class Magazine {
- @Id
- private int magazineId;
-
- @ManyToOne
- @JoinColumn(name = "publisherId", referencedColumnName = "publisherId")
- private Publisher publisher;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Magazine (magazineId INTEGER NOT NULL, publisherId INTEGER, PRIMARY KEY (magazineId)) TYPE = innodb;
- CREATE TABLE Publisher (publisherId INTEGER NOT NULL, PRIMARY KEY (publisherId)) TYPE = innodb;
- CREATE INDEX I_MGAZINE_PUBLISHER ON Magazine (publisherId);
3.2.9 One To Many
如果entity A引用多个entity B,而且没有两个entity A引用相同的entity B,那么称A和B之间的关系是One To Many。JPA使用OneToMany annotation来指定这种关系。它有以下属性:
- Class targetEntity: 关联entity的Class。通常使用在parameterized 集合类上,如果集合类不是parameterized 类型,那么必须指定这个属性。
- String mappedBy: 用来之指定双向关系另一端的entity name。如果没有指定这个属性,那么认为是标准的单向关系。
- CascadeType[] cascade: A指定级连行为,缺省是空数组。
- FetchType fetch: 指定load方式,可选值FetchType.EAGER和 FetchType.LAZY(缺省值)。
如果使用OneToMany关系,那么缺省情况下JPA将会生成中间表来进行关联,例如:
- @Entity
- public class Publisher {
- @Id
- private int publisherId;
-
- @OneToMany
- private List<Magazine> magazines;
- }
-
- @Entity
- public class Magazine {
- @Id
- private int magazineId;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Magazine (magazineId INTEGER NOT NULL, PRIMARY KEY (magazineId)) TYPE = innodb;
- CREATE TABLE Publisher (publisherId INTEGER NOT NULL, PRIMARY KEY (publisherId)) TYPE = innodb;
- CREATE TABLE Publisher_Magazine (Publisher_publisherId INTEGER, magazines_magazineId INTEGER) TYPE = innodb;
- CREATE INDEX I_PBLSGZN_ELEMENT ON Publisher_Magazine (magazines_magazineId);
- CREATE INDEX I_PBLSGZN_PUBLISHER_PUBLISHERID ON Publisher_Magazine (Publisher_publisherId);
如果不希望使用OpenJPA缺省的关联表名和字段名(例如Publisher_Magazine,Publisher_publisherId),那么可以使用JoinTable annotation,例如:
- @Entity
- public class Publisher {
- @Id
- private int publisherId;
-
- @OneToMany
- @JoinTable(name="PublisherMagazine",
- joinColumns=@JoinColumn(name="publisherId", referencedColumnName="publisherId"),
- inverseJoinColumns=@JoinColumn(name="magazineId", referencedColumnName="magazineId"))
- private List<Magazine> magazines;
- }
-
- @Entity
- public class Magazine {
- @Id
- private int magazineId;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Magazine (magazineId INTEGER NOT NULL, PRIMARY KEY (magazineId)) TYPE = innodb;
- CREATE TABLE Publisher (publisherId INTEGER NOT NULL, PRIMARY KEY (publisherId)) TYPE = innodb;
- CREATE TABLE PublisherMagazine (publisherId INTEGER, magazineId INTEGER) TYPE = innodb;
- CREATE INDEX I_PBLSGZN_ELEMENT ON PublisherMagazine (magazineId);
- CREATE INDEX I_PBLSGZN_PUBLISHERID ON PublisherMagazine (publisherId);
考虑下面这种情况:
- @Entity
- public class Publisher {
- @Id
- private int publisherId;
-
- @OneToMany()
- private List<Magazine> magazines;
- }
-
- @Entity
- public class Magazine {
- @Id
- private int magazineId;
-
- @ManyToOne()
- private Publisher publisher;
- }
在这种情况下,JPA会根据Publisher上的OneToMany关系生成关联表,同时根据Magazine上的ManyToOne关系在
Magazine表上生成关联字段。如果希望避免这种冗余,那么可以通过OneToMany上的mappedBy属性来告知JPA:谁拥有这个双向关系。
例如:
- @Entity
- public class Publisher {
- @Id
- private int publisherId;
-
- @OneToMany(mappedBy="publisher")
- private List<Magazine> magazines;
- }
-
- @Entity
- public class Magazine {
- @Id
- private int magazineId;
-
- @ManyToOne()
- private Publisher publisher;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Magazine (magazineId INTEGER NOT NULL, publisher_publisherId INTEGER, PRIMARY KEY (magazineId)) TYPE = innodb;
- CREATE TABLE Publisher (publisherId INTEGER NOT NULL, PRIMARY KEY (publisherId)) TYPE = innodb;
- CREATE INDEX I_MGAZINE_PUBLISHER ON Magazine (publisher_publisherId);
3.2.10 One To One
如果entity A引用唯一的entity B,而且没有其它entity A引用相同的entity B,那么称A和B之间的关系是One To One。JPA使用OneToOne annotation来指定这种关系。它有以下属性:
- Class targetEntity: 关联entity的Class。通常也可以从字段类型上得到这个信息。
- String mappedBy: 用来之指定双向关系另一端的entity name。如果没有指定这个属性,那么认为是标准的单向关系。
- CascadeType[] cascade: A指定级连行为,缺省是空数组。
- FetchType fetch: 指定load方式,可选值FetchType.EAGER(缺省值)和 FetchType.LAZY。
- boolean optional: 关联的entity是否必须存在。如果是false,那么这个字段不能是null,缺省值是true。
以下是几个使用OneToOne关系的例子:
- @Entity
- public class Company {
- @Id
- private int id;
-
- @OneToOne
- private Address address;
- }
-
- @Entity
- public class Address {
- @Id
- private int id;
-
- @OneToOne
- private Company company;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下(两个表都生成了跟对方表主键类型相同的字段及其索引,但是并没有显式的外键关联):
- CREATE TABLE Address (id INTEGER NOT NULL, company_id INTEGER, PRIMARY KEY (id)) TYPE = innodb;
- CREATE TABLE Company (id INTEGER NOT NULL, address_id INTEGER, PRIMARY KEY (id)) TYPE = innodb;
- CREATE INDEX I_ADDRESS_COMPANY ON Address (company_id);
- CREATE INDEX I_COMPANY_ADDRESS ON Company (address_id);
如果将Address类中company属性的annotation改成@OneToOne(mappedBy =
"address"),那么用MappingTool生成的SQL(MySQL数据库)如下(只有Company表生成了跟Address表主键类型相同
的字段及其索引,但是并没有显式的外键关联):
- CREATE TABLE Address (id INTEGER NOT NULL, PRIMARY KEY (id)) TYPE = innodb;
- CREATE TABLE Company (id INTEGER NOT NULL, address_id INTEGER, PRIMARY KEY (id)) TYPE = innodb;
- CREATE INDEX I_COMPANY_ADDRESS ON Company (address_id);
如果不希望使用OpenJPA缺省的关联字段名(例如address_id),那么可以使用JoinColumn annotation,例如:
- @Entity
- public class Company {
- @Id
- private int id;
-
- @OneToOne
- @JoinColumn(name = "AddressId", referencedColumnName = "id")
- private Address address;
- }
-
- @Entity
- public class Address {
- @Id
- private int id;
-
- @OneToOne(mappedBy = "address")
- private Company company;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Address (id INTEGER NOT NULL, PRIMARY KEY (id)) TYPE = innodb;
- CREATE TABLE Company (id INTEGER NOT NULL, AddressId INTEGER, PRIMARY KEY (id)) TYPE = innodb;
- CREATE INDEX I_COMPANY_ADDRESS ON Company (AddressId);
3.2.11 Many To Many
如果entity A引用多个entity B,而且其它的entity A也可能引用其中某些entity B,那么称A和B之间的关系是Many To Many。JPA使用ManyToMany annotation来指定这种关系。它有以下属性:
- Class targetEntity: 关联entity的Class。通常使用在parameterized 集合类上,如果集合类不是parameterized 类型,那么必须指定这个属性。
- String mappedBy: 用来之指定双向关系另一端的entity name。如果没有指定这个属性,那么认为是标准的单向关系。
- CascadeType[] cascade: A指定级连行为,缺省是空数组。
- FetchType fetch: 指定load方式,可选值FetchType.EAGER和 FetchType.LAZY(缺省值)。
如果使用ManyToMany关系,那么JPA将会生成中间表来进行关联,例如:
- @Entity
- public class Author {
- @Id
- private int authorId;
-
- @ManyToMany(cascade=CascadeType.ALL)
- private List<Article> articles;
- }
-
- @Entity
- public class Article {
- @Id
- private int articleId;
-
- @ManyToMany(cascade=CascadeType.ALL)
- private List<Author> authors;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Article (articleId INTEGER NOT NULL, PRIMARY KEY (articleId)) TYPE = innodb;
- CREATE TABLE Article_Author (Article_articleId INTEGER, authors_authorId INTEGER) TYPE = innodb;
- CREATE TABLE Author (authorId INTEGER NOT NULL, PRIMARY KEY (authorId)) TYPE = innodb;
- CREATE TABLE Author_Article (Author_authorId INTEGER, articles_articleId INTEGER) TYPE = innodb;
- CREATE INDEX I_RTCLTHR_ARTICLE_ARTICLEID ON Article_Author (Article_articleId);
- CREATE INDEX I_RTCLTHR_ELEMENT ON Article_Author (authors_authorId);
- CREATE INDEX I_THR_TCL_AUTHOR_AUTHORID ON Author_Article (Author_authorId);
- CREATE INDEX I_THR_TCL_ELEMENT ON Author_Article (articles_articleId);
如果将Article类中authors属性的annotation改成@ManyToMany(cascade=CascadeType.ALL,
mappedBy="articles"),那么用MappingTool生成的SQL(MySQL数据库)如下:
- CREATE TABLE Article (articleId INTEGER NOT NULL, PRIMARY KEY (articleId)) TYPE = innodb;
- CREATE TABLE Author (authorId INTEGER NOT NULL, PRIMARY KEY (authorId)) TYPE = innodb;
- CREATE TABLE Author_Article (authors_authorId INTEGER, articles_articleId INTEGER) TYPE = innodb;
- CREATE INDEX I_THR_TCL_AUTHORS_AUTHORID ON Author_Article (authors_authorId);
- CREATE INDEX I_THR_TCL_ELEMENT ON Author_Article (articles_articleId);
如果不希望使用OpenJPA缺省的关联表名和字段名(例如Author_Article,authors_authorId),那么可以使用JoinTable annotation,例如:
- @Entity
- public class Author {
- @Id
- private int authorId;
-
- @ManyToMany(cascade=CascadeType.ALL)
- @JoinTable(name="AuthorArticle",
- joinColumns=@JoinColumn(name="AuthorId", referencedColumnName="AuthorId"),
- inverseJoinColumns=@JoinColumn(name="ArticleId", referencedColumnName="ArticleId"))
- private List<Article> articles;
- }
-
- @Entity
- public class Article {
- @Id
- private int articleId;
-
- @ManyToMany(cascade=CascadeType.ALL, mappedBy="articles")
- private List<Author> authors;
- }
用MappingTool生成以上两个entity class的SQL(MySQL数据库)如下:
- CREATE TABLE Article (articleId INTEGER NOT NULL, PRIMARY KEY (articleId)) TYPE = innodb;
- CREATE TABLE Author (authorId INTEGER NOT NULL, PRIMARY KEY (authorId)) TYPE = innodb;
- CREATE TABLE AuthorArticle (AuthorId INTEGER, ArticleId INTEGER) TYPE = innodb;
- CREATE INDEX I_THRRTCL_AUTHORID ON AuthorArticle (AuthorId);
- CREATE INDEX I_THRRTCL_ELEMENT ON AuthorArticle (ArticleId);
3.2.12 Order By
由于关系型数据库并不保存records的顺序,因此为了保证集合类型的字段有一致的顺序,必须使用OrderBy annotation。缺省是采用identity values的升序。以下是个简单的例子:
- @OrderBy("lastName, firstName")
- private Collection<Author> authors;
3.2.13 Map Key
JPA在OneToMany或者ManyToMany关系中支持Map类型的字段,关联的entities组成了Map的value。JPA在每个
entity中挑选一个字段作为Map的key。MapKey
annotation有一个name属性,用来指定被挑选作为key的字段名。如果没有指定name,那么会使用关联entity的identity字
段。
3.2.14 Persistent Field Defaults
当某个字段没有使用以上介绍的annotations时,JPA对该字段采取以下的缺省行为:
- 被声明成static, transient, final的字段缺省是non-persistent。
-
以下类型的字段:primitive type, primitive wrapper type, java.lang.String,
byte[], Byte[], char[], Character[], java.math.BigDecimal,
java.math.BigInteger, java.util.Date, java.util.Calendar, java.sql.Date,
java.sql.Timestamp, Serializable type 缺省时persistent,就像使用了Basic
annotation一样。
- Embeddable type缺省是persistent,就像使用了Embedded annotation一样。
- 以上以外的字段缺省是non-persistent。
3.2.15 Column
Column annotation 用来指定列的属性,它有以下可选属性:
- String name: 列名,缺省是属性名。
- String columnDefinition: 数据库中列的数据类型。只有当JPA vendor支持通过metadata创建表的时候,这个属性才被使用。
- int length: 列的长度,这个属性通常也是在创建表的时候使用。也有的JPA vendor在flush之前通过这个属性验证数据。
- int precision: 列的精度。
- int scale: 列的小数范围。
- boolean nullable: 列是否允许为null。缺省是true。
- boolean insertable: 如果false,那么SQL 的insert语句中会忽略这个列。缺省是true。
- boolean updatable: 如果false,那么SQL 的update语句中会忽略这个列。缺省是true。
- String table: 指定secondary table的表名。
4 EntityManagerFactory
4.1 Overview
EntityManagerFactory可以被注入到应用中,也可以通过以下方式创建:
EntityManagerFactory emf = Persistence.createEntityManagerFactory("openjpa");
Persistence.createEntityManagerFactory方法通过在类路径上查找META-INF目录中的
persistence.xml文件来获得EntityManagerFactory的配置,persistence.xml文件中可以定义多个
persistence-unit。其name属性值可以作为Persistence.createEntityManagerFactory方法的参
数,transaction-type用来指定是使用JTA,还是使用局部事务。以下是个persistence.xml文件的例子:
- <?xml version="1.0"?>
- <persistence>
- <persistence-unit name="openjpa" transaction-type="RESOURCE_LOCAL" >
- <provider>org.apache.openjpa.persistence.PersistenceProviderImpl</provider>
- <class>tutorial.Animal</class>
- <class>tutorial.Dog</class>
- <class>tutorial.Rabbit</class>
- <class>tutorial.Snake</class>
- <properties>
- <property name="openjpa.ConnectionURL" value="jdbc:hsqldb:tutorial_database"/>
- <property name="openjpa.ConnectionDriverName" value="org.hsqldb.jdbcDriver"/>
- <property name="openjpa.ConnectionUserName" value="sa"/>
- <property name="openjpa.ConnectionPassword" value=""/>
- <property name="openjpa.Log" value="DefaultLevel=WARN, Tool=INFO"/>
- </properties>
- </persistence-unit>
- </persistence>
EntityManagerFactory 实例是重量级对象。每个EntityManagerFactory 实例可能要维护metadata
cache、object state cache、EntityManager pool、connection
pool等等资源。如果应用程序不再使用EntityManagerFactory,那么应该及时关闭它以便释放资源。如果在一个或多个
EntityManager处于活跃状态时试图关闭EntityManagerFactory,那么会导致一个
IllegalStateException。
4.2 Persistence Context
Persistence
context包含一组entities,这些entities都有唯一的persistent identity。在persistence
context中,EntityManager管理entities的生命周期,entities可以访问datastore来获取persistent
state。当persistence
context结束的时候,被EntityManager管理的所有entities都变成detached状态。Detached
entities不再被EntityManager管理,也不能访问datastore。有两种类型的persistent
context:transaction persistence context和extended persistence context。
4.2.1 Transaction Persistence Context
在transaction persistence
context模型中,EntityManager为每一个事务开始一个新的persistence
context。当事务被提交或者回滚后,persistence
context也就自动结束,被EntityManager管理的所有entity都变成detached状态。此时如果访问entity上尚未被加载的
字段(例如lazy fetch字段)会导致没有定义的结果。
如果不在事务中通过EntityManager访问datastore,那么EntityManager会对每一次方法调用都创建一个新的
persistence context。当方法调用结束时,persistence
context也自动结束。例如当在事务之外调用EntityManager.find方法,EntityManager会创建一个临时的
persistence context,并在这个临时的persistence
context中访问datastore。当EntityManager.find方法结束时,临时的persistence
context自动结束,并且EntityManager.find方法会返回一个detached entity。如果用相同的identity
object再次调用EntityManager.find方法,那么会得到一个新的detached
entity。以下是个描述transaction persistence context模型的行为的例子:
- EntityManager em;
- ...
-
-
-
-
-
- Magazine mag1 = em.find(Magazine.class, magId);
- Magazine mag2 = em.find(Magazine.class, magId);
- assertTrue(mag2 != mag1);
- ...
-
-
-
-
-
-
-
- Magazine mag3 = em.find(Magazine.class, magId);
- assertTrue(mag3 != mag1 && mag3 != mag2);
- Magazine mag4 = em.find(Magazine.class (magId);
- assertTrue(mag4 == mag3);
- ...
-
-
-
-
- Magazine mag5 = em.find(Magazine.class, magId);
- assertTrue(mag5 != mag3);
4.2.2 Extended Persistence Context
在extended persistence
context模型中,不论是否在事务内,EntityManager在其整个生命周期内维护相同一个persistence
context。所有通过EntityManager得到的entities都被EntityManager管理,只有在EntityManager被关
闭,或者entity被序列化的时候,entity才变成detached状态。
以下是个描述extended persistence context模型的行为的例子:
- EntityManagerFactory emf = ...
- EntityManager em = emf.createEntityManager();
-
-
-
- Magazine mag1 = em.find(Magazine.class, magId);
- Magazine mag2 = em.find(Magazine.class, magId);
- assertTrue(mag2 == mag1);
-
- em.getTransaction().begin();
-
-
- Magazine mag3 = em.find(Magazine.class, magId);
- assertTrue(mag3 == mag1);
- Magazine mag4 = em.find(Magazine.class (magId);
- assertTrue(mag4 == mag1);
-
- em.getTransaction.commit ();
-
-
- Magazine mag5 = em.find(Magazine.class, magId);
- assertTrue(mag5 == mag1);
-
-
- em.close();
5 EntityManager
EntityManager接口的方法可以大致分为以下几类。
5.1 Transaction Association
- public EntityTransaction getTransaction ();
EntityManager实例和EntityTransaction实例之间是一对一的关系。 通过getTransaction方法可以得到与EntityManager关联的EntityTransaction实例。
5.2 Entity Identity Management
EntityManager负责管理entities,以下的这些方法的行为会因persistence context(transaction
persistence context或者extended persistence context)的不同而有差异。
- public <T> T find(Class<T> cls, Object oid);
find方法返回persistent identity指定的entity。 如果这个entity存在于当前的persistence
context中,那么返回值是被缓存的对象;否则会创建一个新的entity,并从datastore中加载相关的persistent
state。如果datastore不存在持有指定persistent identity的记录,那么这个方法返回null。
- public <T> T getReference(Class<T> cls, Object oid);
getReference方法与find相似。不同的是:如果缓存中没有指定的entity,EntityManager会创建一个新的entity,不
是立即访问datastore(不加载persistent state),而是在第一次访问某个持久字段的时候才加载相应的persistent
state。此外,getReference
方法不返回null,如果在datastore中找不到相应的entity,这个方法会抛出EntityNotFoundException。
- public boolean contains(Object entity);
如果当前的persistence context 包含指定的entity,那么返回true;否则返回false。
5.3 Cache Management
Flush方法把当前事务中所有的修改写入到datastore中。如果EntityManager没有连接到datastore,那么
EntityManager首先会获取一个连接并一直持有到事务结束。Flush方法抛出的任何异常都会导致事务回滚。如果调用flush方法时没有一个
活跃的事务,那么flush方法会抛出TransactionRequiredException。
- public FlushModeType getFlushMode();
- public void setFlushMode(FlushModeType flushMode);
EntityManager的FlushMode 属性用来指定是否在执行query之前进行flush。这可以确保当前事务中的任何修改会体现在query的结果中。此外也可以在query实例上设置FlushMode。它有以下两个可选值:
- COMMIT: 只是在事务提交的时候flush。当前事务中的修改可能不会体现在query的结果中。
- AUTO: 在需要的时候进行flush以确保当前事务中的任何修改都会体现在query的结果中。
OpenJPA 只在当前事务的任何修改可能会影响到将要执行的query的结果时才进行flush。
Clear方法会结束当前的persistence context,被EntityManager管理的所有entities变成detached状态。
5.4 Query Factory
- public Query createQuery(String query);
createQuery方法根据提供的Java Persistence Query Language (JPQL) string来创建一个query。
- public Query createNamedQuery(String name);
createNamedQuery方法用来得到通过metadata定义的命名query。
- public Query createNativeQuery(String sql);
- public Query createNativeQuery(String sql, Class resultCls);
- public Query createNativeQuery(String sql, String resultMapping);
以上方法用来创建datastore特有的native queries。例如在关系型数据库中使用的Structured Query Language (SQL)。
5.5 Closing
- public boolean isOpen();
- public void close();
当不再使用EntityManager 的时候,需要及时关闭它以便释放资源。如果EntityManager 已经关闭,那么除了调用isOpen
方法外,调用EntityManager上的其它方法会导致IllegalStateException。不能在一个事务正在进行中的时候关闭
EntityManager。
5.6 Entity Lifecycle Management
5.6.1 public void persist(Object entity);
persist方法用于将新创建的entity纳入EntityManager的管理,在下一次的flush或者commit时,这个entity会被插入到datastore中。这个方法只能在一个活跃的事务环境中调用。它的行为如下:
- 如果是新entity,那么它会被EntityManager管理。
- 如果这个entity已经被EntityManager管理,那么会被忽略。
- 如果这个entity之前被remove,那么它会重新被管理。
- 如果这个entity的状态是detached,那么会导致IllegalArgumentException。
以下是个简单的例子:
- import java.util.Iterator;
- import java.util.List;
-
- import javax.persistence.Basic;
- import javax.persistence.CascadeType;
- import javax.persistence.Entity;
- import javax.persistence.FetchType;
- import javax.persistence.Id;
- import javax.persistence.OneToMany;
-
- import org.apache.openjpa.persistence.FetchAttribute;
- import org.apache.openjpa.persistence.FetchGroup;
- import org.apache.openjpa.persistence.FetchGroups;
-
- @Entity
- @FetchGroups({
- @FetchGroup(name="detail", attributes={
- @FetchAttribute(name="grade"),
- @FetchAttribute(name="magazines")
- })
- })
- public class Publisher {
- private int id;
-
- private String name;
-
- private String grade;
-
- private List<Magazine> magazines;
-
- public String toString() {
- StringBuffer sb = new StringBuffer();
- sb.append("id: " + getId());
- sb.append(", name: " + getName());
- sb.append(", grade: " + getGrade());
-
- sb.append(", magazines[");
- if(getMagazines() != null) {
- for(Iterator<Magazine> iter = getMagazines().iterator(); iter.hasNext(); ) {
- sb.append(iter.next().toString());
- if(iter.hasNext()) {
- sb.append("; ");
- }
- }
- }
- sb.append("]");
- return sb.toString();
- }
-
- @Id
- public int getId() {
- return id;
- }
-
- public void setId(int id) {
- this.id = id;
- }
-
- @Basic
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
-
- @Basic(fetch=FetchType.LAZY)
- public String getGrade() {
- return grade;
- }
-
- public void setGrade(String grade) {
- this.grade = grade;
- }
-
- @OneToMany(mappedBy="publisher", cascade=CascadeType.ALL, fetch=FetchType.LAZY)
- public List<Magazine> getMagazines() {
- return magazines;
- }
- public void setMagazines(List<Magazine> magazines) {
- this.magazines = magazines;
- }
- }
- import javax.persistence.CascadeType;
- import javax.persistence.Entity;
- import javax.persistence.Id;
- import javax.persistence.IdClass;
- import javax.persistence.JoinColumn;
- import javax.persistence.ManyToOne;
-
- @Entity
- @IdClass(Magazine.MagazineId.class)
- public class Magazine {
- @Id
- private String isbn;
-
- @Id
- private String title;
-
- @ManyToOne(cascade=CascadeType.ALL)
- @JoinColumn(name = "publisherId", referencedColumnName = "id")
- private Publisher publisher;
-
- public String toString() {
- StringBuffer sb = new StringBuffer();
- sb.append("isbn: " + isbn);
- sb.append(", title: " + title);
- return sb.toString();
- }
-
- public String getIsbn() {
- return isbn;
- }
-
- public void setIsbn(String isbn) {
- this.isbn = isbn;
- }
-
- public String getTitle() {
- return title;
- }
-
- public void setTitle(String title) {
- this.title = title;
- }
-
- public Publisher getPublisher() {
- return publisher;
- }
-
- public void setPublisher(Publisher publisher) {
- this.publisher = publisher;
- }
-
- public static class MagazineId {
- private String isbn;
- private String title;
-
- public MagazineId() {
- }
-
- public MagazineId(String isbn, String title) {
- this.isbn = isbn;
- this.title = title;
- }
-
- public boolean equals(Object obj) {
- if(this == obj) {
- return true;
- }
- if(!(obj instanceof MagazineId)) {
- return false;
- }
-
- MagazineId rhs = (MagazineId)obj;
- boolean b1 = (isbn == rhs.isbn || (isbn != null && isbn.equals(rhs.isbn)));
- boolean b2 = (title == rhs.title || (title != null && title.equals(rhs.title)));
- return b1 && b2;
- }
-
- public int hashCode(){
- int h1 = (isbn == null ? 0 : isbn.hashCode());
- int h2 = (title == null ? 0 : title.hashCode());
- return h1 ^ h2;
- }
-
- public String toString() {
- StringBuffer sb = new StringBuffer();
- sb.append("isbn: " + isbn);
- sb.append(", title: " + title);
- return sb.toString();
- }
- }
- }
- Publisher p1 = new Publisher();
- p1.setId(id);
- p1.setName("publisher1");
- p1.setGrade("excellent");
- p1.setMagazines(new ArrayList<Magazine>());
- Magazine m1 = new Magazine();
- m1.setIsbn("isbn1");
- m1.setTitle("title1");
- m1.setPublisher(p1);
- p1.getMagazines().add(m1);
- Magazine m2 = new Magazine();
- m2.setIsbn("isbn2");
- m2.setTitle("title2");
- m2.setPublisher(p1);
- p1.getMagazines().add(m2);
- EntityManagerem = entityManagerFactory.createEntityManager();
- em.getTransaction().begin();
- em.persist(p1);
- em.getTransaction().commit();
5.6.2 public void remove(Object entity);
remove方法用于删除被管理的entity。在下一次的flush或者commit时,这个entity会从datastore中删除。这个方法只能在一个活跃的事务环境中调用。它的行为如下:
- 如果是新entity,那么会被忽略。
- 如果是已经被管理的entity,那么它会被删除。
- 如果这个entity之前被remove,那么会被忽略。
- 如果这个entity的状态是detached,那么会导致IllegalArgumentException。
5.6.3 public void refresh(Object entity);
refresh方法用于确保entity的persistent state和datastore中的persistent state同步。它的行为如下:
- 如果是新entity,那么会被忽略。
- 如果是已经被管理的entity,那么刷新它的persistent state。
- 如果这个entity之前被remove,那么会被忽略。
- 如果这个entity的状态是detached,那么会导致IllegalArgumentException。
5.6.4 public Object merge(Object entity);
有些情况下,你需要编辑一个处于detached状态的entity,然后重新将这个entity纳入到EntityManager的管理中,并将对其
persistent
state的修改更新到datastore中。Merge方法返回该entity受管理的一份拷贝。这个方法只能在一个活跃的事务环境中调用。它的行为如
下:
- 如果是新entity,那么会创建该entity的一份拷贝,并将这份拷贝纳入EntityManager的管理。
- 如果是已经被管理的entity,那么会被忽略。
- 如果这个entity之前被remove,那么会导致IllegalArgumentException。
- 如
果这个entity的状态是detached,如果EntityManager中已经管理了具有相同的identity的entity
B,那么会将原始entity的persistent state拷贝到entity
B中;否则会创建该entity的一份拷贝,并将这份拷贝纳入EntityManager的管理。
以下是个简单的例子:
- p1.setName("publisher2");
- EntityManager em = entityManagerFactory.createEntityManager();
- em.getTransaction().begin();
- em.merge(obj);
- em.getTransaction().commit();
5.6.5 public void lock (Object entity, LockModeType mode);
lock方法通过指定的LockModeType 来对entity加锁。LockModeType 有以下两个可选值:
- READ: 在其它事务中可以读取,但是不能更新。
- WRITE: 在其它事务中不可以读取或更新。在当前事务提交后,无论被WRITE lock锁定的entities是否改变,其version将会自动增加。
5.6.6 Detach and Attach
除了JPA定义的detach和attach API之外,OpenJPA还支持更多的特性。例如OpenJPAEntityManager提供以下方法:
- public Object detach(Object pc);
- public Object[] detachAll(Object... pcs);
- public Collection detachAll(Collection pcs);
以上的detach方法返回给定entity的detached状态的拷贝。在detach在当前事务中被修改过的entity之前,flush方法会被
调用,以便将这些修改保存到datastore中。由于detached
entity不能访问datastore,因此有时候需要在detach之前加载一些persistent state(例如某些lazy
fetch字段)。尽管可以通过手工编码完成,OpenJPA也提供了一些特性来自动完成类似的工作。例如DetachState,它有以下可选值:
- loaded: 保持已经加载的字段,对于没有加载的字段则保持不变。这是缺省值。
- fetch-groups: 根据fetch group来决定需要加载的字段。
- all: 加载所有的字段。
继续5.6.1中的例子:
- EntityManager em = entityManagerFactory.createEntityManager();
- Publisher p2 = (Publisher)em.find(Publisher.class, id);
- em.close();
- System.out.println(p2.toString());
以上例子中p2在em.close()之后变成detached状态。由于采用了缺省的DetachState,因此没有加载lazy fetch字段grade和magazines。输出如下:
id: 1, name: publisher1, grade: null, magazines[]
- em = entityManagerFactory.createEntityManager();
- Publisher p4 = (Publisher)em.find(Publisher.class, id);
- p4.getGrade();
- p4.getMagazines();
- em.close();
- System.out.println(p4.toString());
以上例子中p4在em.close()之后变成detached状态。虽然采用了缺省的DetachState,但是由于程序中显式访问了lazy fetch字段,所以grade和magazines被加载,并且在detach之后仍然可以访问。输出如下:
id: 1, name: publisher1, grade: excellent, magazines[isbn: isbn1, title: title1; isbn: isbn2, title: title2]
- em = entityManagerFactory.createEntityManager();
- ((OpenJPAEntityManager)em).setDetachState(DetachStateType.ALL);
- Publisher p5 = (Publisher)em.find(Publisher.class, id);
- em.close();
- System.out.println(p5.toString());
以上例子中p5在em.close()之后变成detached状态。由于采用了 DetachStateType.ALL,因此所有的lazy fetch字段在detach之前都自动被加载,并且在detach之后仍然可以访问。输出如下:
id: 1, name: publisher1, grade: excellent, magazines[isbn: isbn1, title: title1; isbn: isbn2, title: title2]
- em = entityManagerFactory.createEntityManager();
- ((OpenJPAEntityManager)em).setDetachState(DetachStateType.FETCH_GROUPS);
- ((OpenJPAEntityManager)em).getFetchPlan().addFetchGroup("detail");
- Publisher p6 = (Publisher)em.find(Publisher.class, id);
- em.close();
- System.out.println(p6.toString());
以上例子中p6在em.close()之后变成detached状态。由于采用了 DetachStateType.
FETCH_GROUPS,而Publisher中名为"detail"的fetch
group定义了要额外加载grade和magazines,因此grade和magazines被加载,并且在detach之后仍然可以访问。输出如
下:
id: 1, name: publisher1, grade: excellent, magazines[isbn: isbn1, title: title1; isbn: isbn2, title: title2]
6 Query
6.1 JPQL Queries
6.1.1Query Basics
- public Query createQuery(String jpql);
EntityManager上的这个方法用来根据给定的JPQL创建Query。
- public List getResultList();
Query上的这个方法用来得到query的执行结果,以下是个简单的例子:
- EntityManager em = entityManagerFactory.createEntityManager();
- Query q = em.createQuery("SELECT x FROM Magazine x");
- List<Magazine> results = (List<Magazine>) q.getResultList();
- em.close();
JPQL query的from子句中定义了query内部命名空间。可以将任意的标识符赋值给entities,之后就可以在query的任意位置上通过标识符引用entities。 在from子句中,关键字as是可选的。例如以下两个JPQL等效:
- SELECT x FROM Magazine x
- SELECT x FROM Magazine AS x
当查询entities的时候,关键字object也是可选的,例如select x 和 SELECT OBJECT(x)
是等效的。JPQL中的关键字不是大小写敏感的,而entity、identifier和member
names是大小写敏感的。以下是一些JPQL的例子:
- SELECT x FROM Magazine x WHERE x.TITLE = 'JDJ'
- SELECT x FROM Magazine x WHERE x.title = 'JDJ' OR x.title = 'JavaPro'
- SELECT x FROM Magazine x WHERE x.price > 3.00 AND x.price <= 5.00
- SELECT x FROM Magazine x WHERE x.price <> 3.00
- SELECT x FROM Magazine x WHERE (x.price > 3.00 AND x.price <= 5.00) OR x.price = 7.00
- SELECT x FROM Magazine x WHERE x.price > 3.00 AND (x.price <= 5.00 OR x.price = 7.00)
- SELECT x FROM Magazine x WHERE x.price >= 3.00 AND x.price <= 5.00
- SELECT x FROM Magazine x WHERE x.price BETWEEN 3.00 AND 5.00
- SELECT x FROM Magazine x WHERE x.title LIKE 'J%'
- SELECT x FROM Magazine x WHERE x.title LIKE 'J__'
- SELECT x FROM Magazine x WHERE x.title IN ('JDJ', 'JavaPro', 'IT Insider')
- SELECT x FROM Magazine x WHERE x.articles is empty
- SELECT x FROM Magazine x WHERE x.publisher is null
- SELECT x FROM Magazine x WHERE NOT(x.price = 10.0)
6.1.2 Relation Traversal
可以通过类似Java的语法来遍历对象间的关系。例如Magazine中有个Publisher类型的属性publisher,那么可以通过以下方式编写JPQL query:
- SELECT x FROM Magazine x WHERE x.publisher.name = 'Random House'
以上的遍历中假设关系不是null。在SQL术语中类似于inner join。如果你希望包含为null 的关系,那么可以如下指定:
- SELECT x FROM Magazine x WHERE x.publisher.name = 'Random House' or x.publisher is null
也可以在query中遍历集合字段,但是必须首先在from子句中定义遍历相关的identification variable,例如:
- SELECT x FROM Magazine x, IN(x.articles) y WHERE y.authorName = 'John Doe'
IN() 类型的语法可以使用inner join关键词,例如以下两个JPQL等效:
- SELECT x FROM Magazine x, IN(x.articles) y WHERE y.authorName = 'John Doe'
- SELECT x FROM Magazine x inner join x.articles y WHERE y.authorName = 'John Doe'
6.1.3 Fetch Joins
JPQL query中可以指定一个或多个join fetch来指定哪些字段被pre-fetched,以5.6.1中的Publisher和Magazine为例:
- em = entityManagerFactory.createEntityManager();
- Query q2 = em.createQuery("SELECT x FROM Publisher x WHERE x.id = 1");
- List<Publisher> r2 = (List<Publisher>)q2.getResultList();
- em.close();
- for(Iterator<Publisher> iter = r2.iterator(); iter.hasNext(); ) {
- System.out.println(iter.next().toString());
- }
以上代码执行后的输出是:
id: 1, name: publisher1, grade: null, magazines[]
- em = entityManagerFactory.createEntityManager();
- Query q3 = em.createQuery("SELECT x FROM Publisher x join fetch x.grade join fetch x.magazines WHERE x.id = 1");
- List<Publisher> r3 = (List<Publisher>)q3.getResultList();
- em.close();
- for(Iterator<Publisher> iter = r3.iterator(); iter.hasNext(); ) {
- System.out.println(iter.next().toString());
- }
以上代码执行后的输出是:
id: 1, name: publisher1, grade: excellent, magazines[isbn: isbn1,
title: title1; isbn: isbn1, title: title1; isbn: isbn2, title: title2;
isbn: isbn2, title: title2]
6.1.4 JPQL Functions
JPQL 支持一系列预定义的标量函数,例如:
- SELECT x FROM Magazine x WHERE CONCAT(x.title, 's') = 'JDJs'
- SELECT x FROM Magazine x WHERE SUBSTRING(x.title, 1, 1) = 'J'
- SELECT x FROM Magazine x WHERE TRIM(BOTH 'J' FROM x.title) = 'D'
- SELECT x FROM Magazine x WHERE LOWER(x.title) = 'jdj'
- SELECT x FROM Magazine x WHERE UPPER(x.title) = 'JAVAPRO'
- SELECT x FROM Magazine x WHERE LENGTH(x.title) = 3
- SELECT x FROM Magazine x WHERE LOCATE('D', x.title) = 2
- SELECT x FROM Magazine x WHERE ABS(x.price) >= 5.00
- SELECT x FROM Magazine x WHERE SQRT(x.price) >= 1.00
- SELECT x FROM Magazine x WHERE MOD(x.price, 10) = 0
6.1.5 Polymorphic Queries
JPQL from子句中的entity class不仅包含这个类本身,而且还包含这个类及其子类。以下是个关于polymorphic query的简单例子:
- @Entity
- @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
- @DiscriminatorColumn(name="Class", discriminatorType=DiscriminatorType.STRING)
- public abstract class BankingAccount {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- protected int id;
- }
-
- @Entity
- @DiscriminatorValue("CC")
- public class CreditCard extends BankingAccount {
- @Basic
- private BigInteger limitAmount;
-
- public String toString() {
- StringBuffer sb = new StringBuffer();
- sb.append("credit card, limit amount: ").append(limitAmount);
- return sb.toString();
- }
- }
-
- @Entity
- @DiscriminatorValue("DC")
- public class DebitCard extends BankingAccount {
- @Basic
- private BigInteger balanceAmount;
-
- public String toString() {
- StringBuffer sb = new StringBuffer();
- sb.append("debit card, balance amount: ").append(balanceAmount);
- return sb.toString();
- }
- }
假设目前数据库中的数据如下:
- select * from bankingaccount;
- +
- | id | Class | limitAmount | balanceAmount |
- +
- | 5 | CC | 1000 | NULL |
- | 6 | DC | NULL | 2000 |
- +
- EntityManager em = entityManagerFactory.createEntityManager();
- Query q1 = em.createQuery("SELECT x FROM BankingAccount x");
- List<BankingAccount> r1 = (List<BankingAccount>)q1.getResultList();
- for(Iterator<BankingAccount> iter = r1.iterator(); iter.hasNext(); ) {
- System.out.println(iter.next().toString());
- }
- em.close();
以上代码的执行结果如下:
credit card, limit amount: 1000
debit card, balance amount: 2000
6.1.6 Query Parameters
JPQL支持两种类型的query参数:named parameters 或 positional parameters。在一个JPQL query中不能同时使用两种类型的参数。以下是两个简单的例子:
- Query q = em.createQuery("SELECT x FROM Magazine x WHERE x.title = ?1 and x.price > ?2");
- q.setParameter(1, "JDJ").setParameter(2, 5.0);
- List<Magazine> results = (List<Magazine>) q.getResultList();
-
- Query q = em.createQuery("SELECT x FROM Magazine x WHERE x.title = :titleParam and x.price > :priceParam");
- q.setParameter("titleParam", "JDJ").setParameter("priceParam", 5.0);
- List<Magazine> results = (List<Magazine>) q.getResultList();
6.1.7 Query Hints
JPQL支持通过name/value对来指定query hints。例如:
- Query q = em.createQuery("select m from Magazine m where ... ");
- q.setHint("openjpa.hint.OptimizeResultCount", new Integer(2));
- q.setHint("openjpa.FetchPlan.ReadLockMode","WRITE");
- List r = q.getResultList();
6.1.8 Ordering
JPQL支持根据一个或者多个字段对查询结果进行排序,例如:
- SELECT x FROM Magazine x order by x.title asc, x.price desc
6.1.9 Aggregates
JPQL支持min、max、avg和count等聚集函数(也被称为列函数)。以下是一些例子:
- Query q = em.createQuery("SELECT AVG(x.price) FROM Magazine x");
- Query q = em.createQuery("SELECT SUM(DISTINCT x.price) FROM Magazine x");
- Query q = em.createQuery("SELECT MAX(x.price) FROM Magazine x WHERE x.title = 'JDJ'");
6.1.10 Named Queries
JPQL支持通过NamedQuery 和NamedQueries 这两个annotation来声明命名query。以下是一个例子:
- @Entity
- @NamedQueries({
- @NamedQuery(name="magsOverPrice",
- query="SELECT x FROM Magazine x WHERE x.price > ?1"),
- @NamedQuery(name="magsByTitle",
- query="SELECT x FROM Magazine x WHERE x.title = :titleParam")
- })
- public class Magazine {
- ...
- }
-
- Query q = em.createNamedQuery("magsOverPrice");
- q.setParameter(1, 5.0f);
- List<Magazine> results = (List<Magazine>) q.getResultList();
-
- Query q = em.createNamedQuery("magsByTitle");
- q.setParameter("titleParam", "JDJ");
- List<Magazine> results = (List<Magazine>) q.getResultList();
6.1.11 Delete By Query
Query可以用来高效地删除对象(不必将每个对象查询之后再进行删除,而是直接进行批量删除)。以下是个简单的例子:
- Query q = em.createQuery("DELETE FROM Subscription s WHERE s.subscriptionDate < :today");
- q.setParameter("today", new Date());
- int deleted = q.executeUpdate();
executeUpdate方法的返回值是删除对象的个数。
6.1.12 Update By Query
跟delete类似,Query也可以用来高效地更新对象。以下是个简单的例子:
- Query q = em.createQuery("UPDATE Subscription s SET s.paid = :paid WHERE s.subscriptionDate < :today");
- q.setParameter("today", new Date());
- q.setParameter("paid", true);
- int updated = q.executeUpdate();
6.1.13 Subquery
目前JPA规范支持在WHERE子句和HAVING子句中使用子查询,未来可能会支持FROM子句中使用子查询。以下是一些例子:
- SELECT DISTINCT auth FROM Author auth WHERE EXISTS (SELECT spouseAuthor FROM Author spouseAuthor WHERE spouseAuthor = auth.spouse)
- SELECT auth FROM Author auth WHERE auth.salary >= ALL(SELECT a.salary FROM Author a WHERE a.magazine = auth.magazine)
- SELECT goodPublisher FROM Publisher goodPublisher WHERE goodPublisher.revenue < (SELECT AVG(p.revenue) FROM Publisher p)
- SELECT mag FROM Magazine mag WHERE (SELECT COUNT(art) FROM mag.articles art) > 10
6.1.14 JPQL Constructor Expressions
在SELECT子句中可以通过使用constructor来返回一个或多个实例。指定的类不必是entity class,例如:
- SELECT NEW com.company.PublisherInfo(pub.id, pub.revenue, mag.price) FROM Publisher pub JOIN pub.magazines mag WHERE mag.price > 5.00
6.2 SQL Queries
JPA支持通过Query接口的以下两个方法使用SQL查询:
- public Query createNativeQuery(String sqlString, Class resultClass);
- public Query createNativeQuery(String sqlString, String resultSetMapping);
OpenJPA也支持使用存储过程。OpenJPA假定任何不以SELECT开头的SQL为存储过程的调用。
在指定resultClass的情况下,sqlString必须查询指定resultClass的 primary key 列、
discriminator 列 (如果存在) 和version column (如果存在)。JPA使用以上三个列的信息来判断object
identity、区分查询子类和检查并发修改。 以下是个简单的例子:
- Query query = em.createNativeQuery("SELECT isbn, title, price, vers FROM Magazine WHERE price > 5 AND price < 10", Magazine.class);
- List<Magazine> results = (List<Magazine>) query.getResultList();
- for (Magazine mag : results)
- processMagazine(mag);
在指定resultSetMapping的情况下,resultSetMapping引用一个定义好的SqlResultSetMapping,例如:
- @Entity
- @SqlResultSetMapping(name="srsm1",
- entities={@EntityResult(entityClass=Publisher.class),
- @EntityResult(entityClass=Magazine.class)}
- )
- public class Publisher {
- …
- }
-
- EntityManager em = entityManagerFactory.createEntityManager();
- Query q13 = em.createNativeQuery("SELECT p.id, p.name, m.isbn, m.title FROM Publisher AS p, Magazine as m WHERE p.id = 1 AND p.id = m.publisherId", "srsm1");
- List r13 = q13.getResultList();
- em.close();
- for(Iterator iter = r13.iterator(); iter.hasNext(); ) {
- Object objects[] = (Object[])iter.next();
- System.out.println("publisher: " + objects[0] + ", magazine: " + objects[1]);
- }
当查询结果不仅包含entity,而且包含value type的时候,可以在SqlResultSetMapping中指定@ColumnResult,例如:
- @Entity
- @SqlResultSetMapping(name="srsm2",
- entities={@EntityResult(entityClass=Publisher.class)},
- columns={@ColumnResult(name="count")}
- )
- public class Publisher {
- …
- }
-
- EntityManager em = entityManagerFactory.createEntityManager();
- Query q14 = em.createNativeQuery("SELECT p.id, count(*) AS count FROM Publisher AS p LEFT JOIN Magazine as m ON p.id = m.publisherId GROUP BY p.id", "srsm2");
- List r14 = q14.getResultList();
- em.close();
- for(Iterator iter = r14.iterator(); iter.hasNext(); ) {
- Object objects[] = (Object[])iter.next();
- System.out.println("publisher: " + objects[0] + ", count: " + objects[1]);
- }
7 Inheritance
对象使用引用以便关联到其它对象;关系型数据库表之间采用外键来描述表的关系。在关系型数据库中通常没有自然且有效的方法来描述类的继承关系。JPA通过Inheritance annotation提供了几种继承策略,它有以下属性:
- InheritanceType
strategy:用来声明继承策略。可选值是InheritanceType.SINGLE_TABLE、
InheritanceType.JOINED和InheritanceType
.TABLE_PER_CLASS。缺省值是InheritanceType.SINGLE_TABLE。
关于Inheritance的更多内容,可以参考Hibernate实战by Christian Bauer, Gavin King。
7.1 Single Table
InheritanceType.SINGLE_TABLE 策略为类的继承体系采用同一个表。表名是基类的名称。例如:
- @Entity
- @Inheritance(strategy=InheritanceType.SINGLE_TABLE)
- public class Base {
- @Id
- private int id;
-
- @Basic
- private String baseName;
- }
-
- @Entity
- public class Derived1 extends Base {
- @Basic
- private String derived1Name;
- }
-
- @Entity
- public class Derived2 extends Base {
- @Basic
- private String derived2Name;
- }
使用MappingTool建立的表结构如下:
- mysql> describe base;
- +
- | Field | Type | Null | Key | Default | Extra |
- +
- | id | int(11) | NO | PRI | NULL | auto_increment |
- | baseName | varchar(255) | YES | | NULL | |
- | DTYPE | varchar(255) | YES | MUL | NULL | |
- | derived1Name | varchar(255) | YES | | NULL | |
- | derived2Name | varchar(255) | YES | | NULL | |
- +
- EntityManager em = entityManagerFactory.createEntityManager();
- em.getTransaction().begin();
- Base base = new Base();
- base.setBaseName("base");
- em.persist(base);
- Derived1 d1 = new Derived1();
- d1.setBaseName("derived1's base");
- d1.setDerived1Name("derived1");
- em.persist(d1);
- Derived2 d2 = new Derived2();
- d2.setBaseName("derived2's base");
- d2.setDerived2Name("derived2");
- em.persist(d2);
- em.getTransaction().commit();
- em.close();
以上代码执行后,数据库中base表的数据如下(其中DTYPE列由OpenJPA自动插入,用于区分不同的class,关于Discriminator的详细用法请参考OpenJPA User's Guide):
- mysql> select * from base;
- +
- | id | baseName | DTYPE | derived1Name | derived2Name |
- +
- | 1 | base | Base | NULL | NULL |
- | 2 | derived1's base | Derived1 | derived1 | NULL |
- | 3 | derived2's base | Derived2 | NULL | derived2 |
- +
7.1.1 Advantages
InheritanceType.SINGLE_TABLE
策略的优势在于简单且性能高(因为不需要使用连接查询等)。如果类的继承体系中,子类和父类间的差异主要在于行为,同时子类之间以及子类和父类之间的属性
差异不大(例如子类不增加属性或者增加的属性数目比较少),那么适用于这个策略。
7.1.2 Disadvantages
这个策略导致规范化级别降低。由于类继承体系中的每个类的属性都要映射到表的一列,因此当类的继承体系变的复杂的时候,表也随之变大。子类中属性对应的列必须声明为nullable。
7.2 Joined
InheritanceType.JOINED策略为类继承体系中的每个类创建不同的表。每个表只包含类中定义的列,因此在load一个子类的时
候,JPA实现需要同时查询子类映射的表,以及通过关联查询所有的父类映射的表。PrimaryKeyJoinColumn
annotation用来指定子类映射的表如何关联到父类映射的表。它有以下属性:
- String name: 子类映射表中的列名。如果只有一个identity filed,那么缺省使用这个field对应的列名。
- String referencedColumnName: 父类映射表中用来关联的列名。如果只有一个identity filed,那么缺省使用这个field对应的列名。
- String columnDefinition: 数据库中列的数据类型。只有当JPA vendor支持通过metadata创建表的时候,这个属性才被使用。
以下是个简单的例子:
- @Entity
- @Inheritance(strategy=InheritanceType.JOINED)
- public class Base {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private int id;
-
- @Basic
- private String baseName;
- }
-
- @Entity
- @PrimaryKeyJoinColumn(name="id", referencedColumnName="id")
- public class Derived1 extends Base {
- @Basic
- private String derived1Name;
- }
-
- @Entity
- @PrimaryKeyJoinColumn(name="id", referencedColumnName="id")
- public class Derived2 extends Base {
- @Basic
- private String derived2Name;
- }
使用MappingTool建立的表结构如下:
- mysql> describe base;
- +
- | Field | Type | Null | Key | Default | Extra |
- +
- | id | int(11) | NO | PRI | NULL | auto_increment |
- | baseName | varchar(255) | YES | | NULL | |
- +
-
- mysql> describe derived1;
- +
- | Field | Type | Null | Key | Default | Extra |
- +
- | id | int(11) | NO | PRI | | |
- | derived1Name | varchar(255) | YES | | NULL | |
- +
-
- mysql> describe derived2;
- +
- | Field | Type | Null | Key | Default | Extra |
- +
- | id | int(11) | NO | PRI | | |
- | derived2Name | varchar(255) | YES | | NULL | |
- +
- EntityManager em = entityManagerFactory.createEntityManager();
- em.getTransaction().begin();
- Base base = new Base();
- base.setBaseName("base");
- em.persist(base);
- Derived1 d1 = new Derived1();
- d1.setBaseName("derived1's base");
- d1.setDerived1Name("derived1");
- em.persist(d1);
- Derived2 d2 = new Derived2();
- d2.setBaseName("derived2's base");
- d2.setDerived2Name("derived2");
- em.persist(d2);
- em.getTransaction().commit();
- em.close();
以上代码执行后,数据库中base表的数据如下:
- mysql> select * from base;
- +
- | id | baseName |
- +
- | 1 | derived2's base |
- | 2 | derived1's base |
- | 3 | base |
- +
-
- mysql> select * from derived1;
- +
- | id | derived1Name |
- +
- | 2 | derived1 |
- +
-
- mysql> select * from derived2;
- +
- | id | derived2Name |
- +
- | 1 | derived2 |
- +
7.2.1 Advantages
InheritanceType.
JOINED策略的优势在于数据库表中没有冗余字段,因此规范化级别比较高;当有新的子类加入到类的继承体系中时,已有表的schema无须修改。如果类
的继承体系中,子类和父类间的差异不在于行为,同时子类间的属性差异比较大,那么适用于这个策略。
7.2.2 Disadvantages
由于在查询的时候需要进行关联,那么查询的速度会比其它方式慢。此外可能需要多个插入和更新语句来处理多个表。
7.3 Table Per Class
InheritanceType.TABLE_PER_CLASS策略为类继承体系中的每个类创建不同的表。和
InheritanceType.JOINED策略不同的是,每个表中包含所有的子类和父类中定义的所有列。因此在load一个子类的时候,JPA实现只
需要同时查询子类映射的表。
以下是个简单的例子:
- @Entity
- @Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
- public class Base {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private int id;
-
- @Basic
- private String baseName;
- }
-
- @Entity
- public class Derived1 extends Base {
- @Basic
- private String derived1Name;
- }
-
- @Entity
- public class Derived2 extends Base {
- @Basic
- private String derived2Name;
- }
使用MappingTool建立的表结构如下:
- mysql> describe base;
- +
- | Field | Type | Null | Key | Default | Extra |
- +
- | id | int(11) | NO | PRI | NULL | auto_increment |
- | baseName | varchar(255) | YES | | NULL | |
- +
-
- mysql> describe derived1;
- +
- | Field | Type | Null | Key | Default | Extra |
- +
- | id | int(11) | NO | PRI | NULL | auto_increment |
- | baseName | varchar(255) | YES | | NULL | |
- | derived1Name | varchar(255) | YES | | NULL | |
- +
-
- mysql> describe derived2;
- +
- | Field | Type | Null | Key | Default | Extra |
- +
- | id | int(11) | NO | PRI | NULL | auto_increment |
- | baseName | varchar(255) | YES | | NULL | |
- | derived2Name | varchar(255) | YES | | NULL | |
- +
- EntityManager em = entityManagerFactory.createEntityManager();
- em.getTransaction().begin();
- Base base = new Base();
- base.setBaseName("base");
- em.persist(base);
- Derived1 d1 = new Derived1();
- d1.setBaseName("derived1's base");
- d1.setDerived1Name("derived1");
- em.persist(d1);
- Derived2 d2 = new Derived2();
- d2.setBaseName("derived2's base");
- d2.setDerived2Name("derived2");
- em.persist(d2);
- em.getTransaction().commit();
- em.close();
以上代码执行后,数据库中base表的数据如下:
- mysql> select * from base;
- +
- | id | baseName |
- +
- | 1 | base |
- +
-
- mysql> select * from derived1;
- +
- | id | baseName | derived1Name |
- +
- | 1 | derived1's base | derived1 |
- +
-
- mysql> select * from derived2;
- +
- | id | baseName | derived2Name |
- +
- | 1 | derived2's base | derived2 |
- +
7.3.1 Advantages
对于已知class类型的实例来说,这个策略十分有效。跟InheritanceType.JOINED策略类似,当有新的子类加入到类的继承体系中时,已有表的schema无须修改。
7.3.2 Disadvantages
这个策略在处理多态关系的时候会存在很多限制,此时某个引用(或者集合中的引用)可能指向任何子类的实例。由于无法使用关联查询,因此在查询的时候可能需要使用多个SQL语句或者使用UNION。
8 Object Locking
8.1 Configuring Default Locking
如何使用lock对load时的性能有重要的影响。OpenJPA通过openjpa.ReadLockLevel和
openjpa.WriteLockLevel来配置缺省的事务读写lock
level。这些缺省配置只适用于非乐观事务;在乐观事务中,OpenJPA缺省不进行lock。在尝试获取lock时,可以通过
openjpa.LockTimeout配置最长的等待时间(缺省值-1指定没有限制),超过这个时间后OpenJPA会抛出异常。配置方式如下:
- <property name="openjpa.ReadLockLevel" value="none"/>
- <property name="openjpa.WriteLockLevel" value="write"/>
- <property name="openjpa.LockTimeout" value="30000"/>
8.2 Configuring Lock Levels at Runtime
在每个事务开始时,OpenJPA初始化EntityManager的缺省lock levels和time
out。在运行时可以通过EntityManager的FetchPlan 接口修改这些配置。也可以通过Query相关的fetch
plan修改Query级别上的配置。这些运行时的修改可以在乐观事务中使用,但是不能在事务之外进行锁定。以下是个简单的例子:
-
- em.getTransaction().begin();
- Query q = em.createQuery("select s from Stock s where symbol = :s");
- q.setParameter("s", symbol);
-
- OpenJPAQuery oq = OpenJPAPersistence.cast(q);
- FetchPlan fetch = oq.getFetchPlan ();
- fetch.setReadLockMode(LockModeType.WRITE);
- fetch.setLockTimeout(3000);
- Stock stock = (Stock) q.getSingleResult();
-
-
- fetch = OpenJPAPersistence.cast(em).getFetchPlan();
- fetch.setReadLockMode(null);
- Market market = em.find(Market.class, marketId);
-
- stock.setPrice(market.calculatePrice(stock));
- em.getTransaction().commit();
8.3 Lock Manager
OpenJPA 内部使用org.apache.openjpa.kernel.LockManager 处理locking相关的实际工作。可以通过openjpa.LockManager 属性进行配置,它有以下选项。
8.3.1 pessimistic
这个选项是org.apache.openjpa.jdbc.kernel.PessimisticLockManager
的一个别名。它使用SELECT FOR UPDATE (或其它等效的)语句锁定跟entity实例对应的数据库行,并且不区分read
locks和write locks,也就是说所有的locks都是write locks。例如:
- <property name="openjpa.LockManager" value="pessimistic(VersionCheckOnReadLock=true,VersionUpdateOnWriteLock=true)"/>
8.3.2 none
这个选项是org.apache.openjpa.kernel.NoneLockManager的一个别名。它不进行任何锁定。例如:
- <property name="openjpa.LockManager" value="none"/>
8.3.3 version
这个选项是org.apache.openjpa.kernel.VersionLockManager的一个别名。它不进行排他锁定;相反,在事务结束
时,它通过校验version来确保被read locks锁定的对象的一致性。无论被write
locks锁定的对象是否被修改,其version都会被累加。为了避免脏读,事务的隔离级别应该至少为"read committed"以上。
8.4 Rules for Locking Behavior
OpenJPA的隐含锁定行为有以下规则:
- 在事务中,当第一次读取某个对象的persistent state的时候,OpenJPA使用fetch plan中当前的read lock
level锁定这个对象。未来对这个对象上的lazy persistent state的读取也采用相同的read lock
level(无论fetch plan中的lock level是否改变)。
- 在事务中,当第一次修改某个对象的persistent state的时候,OpenJPA使用该对象第一次被读取时的write lock
level(无论fetch plan中的lock level是否改变)。如果对象在之前没有被读取过,那么使用当前的write lock
level。
- 当使用persistent relation field 访问某个对象的时候,这个对象在load过程中被当前fetch plan中的lock level锁定,而不是持有这个field的对象所"记住"的那个lock level。
- 在事务中,每次访问某个对象的时候,这个对象会被当前的read lock level重新锁定,并且这个read lock level会被该对象"记住"(规则1,2)。
- 如果显式地通过locking APIs锁定某个对象,那么这些操作都是再次锁定,并且这个lock level会被该对象"记住"(规则1,2)。
- 如果某个对象已经被锁定,那么尝试使用低级别的lock level再次锁定该对象会被忽略,也就是说在事务中,lock level不能被降低。
8.5 Known Issues and Limitations
出于性能的考虑和数据库的限制等,locking有以下限制:
- 在乐观事务中,OpenJPA通常会直到flush或commit前才真正开始事务。当使用pessimistic lock
manager时,OpenJPA必须在乐观事务中锁定某个对象时开始事务。这因为pessimistic lock
manager需要使用数据库的lock。此时OpenJPA会以INFO级别在openjpa.Runtime category中输出一条日志。
- 出于性能的考虑,OpenJPA只保证在从datastore中得到了某个对象的persistent
state之后才进行锁定。这意味这其它事务可能在锁定前修改其persistent
state。可以通过在锁定后refresh这个对象来确保这个对象被成功锁定。当使用pessimistic lock
manager的时候,这个情况只在无法使用SELECT FOR UPDATE(或其它等效的)语句的时候,例如某些数据库不支持SELECT FOR
UPDATE中使用join。此时OpenJPA会以INFO级别在openjpa.Runtime category中输出一条日志。
9 Enhancement
OpenJPA使用enhancer来支持运行时性能优化、延迟加载、脏检查等功能。被enhancer修改过的字节码是Java
debugger兼容的,而且很好地保留了stack trace中的行号。唯一的需要注意的是,如果采用property
access,那么修改后的getter和setter方法名,在stack track中或者step-through时,会加上"pc"前缀。
9.1 Enhancing at Build Time
Enhancer可以在build的时候被调用。如果对已经加强过的class再次进行加强,那么不会对class文件做更多的修改。可以在命令行上通过PCEnhancer类进行加强,例如:
- java org.apache.openjpa.enhance.PCEnhancer Magazine.java
以下是几个可选的命令行参数:
- -directory/-d <output directory>: 输出的class文件的路径。
- -enforcePropertyRestrictions/-epr <true/t | false/f>: 如果entity使用property access,但是却没有遵守相关限制的时候,是否抛出异常。缺省是false。
- -addDefaultConstructor/-adc <true/t | false/f>:
JPA规范要求所有的persistence class必须提供一个无参构造函数。这个标志指示enhancer在persistence
class没有提供无参构造函数的时候,是否创建一个protected型的无参构造函数。缺省是true。
- -tmpClassLoader/-tcl <true/t | false/f>: 是否使用临时classloader加载persistence class。 缺省是true。
9.2 Enhancing on Deployment
在entities被部署到container的时候,Java EE 5
规范包含hooks来自动加强这些entities。 因此,如果使用Java EE
5兼容的应用服务器,OpenJPA会自动在运行时加强entities。如果某些entites使用了build时的enhancement,那么
OpenJPA运行时enhancer会识别并略过这些已经加强过的entities。
9.3 Enhancing at Runtime
在类被载入到JVM的时候,可以使用OpenJPA agent来加强被载入的persistence class。OpenJPA
agent是在应用的main方法执行之前被调用的classes。OpenJPA agent使用JVM
hooks来拦截并加强被载入的包含persistence metadata的类。在每一个被载入的类中查找persistence
metadata可能会减慢应用的初始化速度。可以设置persistent class list来指示agent只查找包含在persistent
class list中的类。以下是使用OpenJPA agent的几个例子:
- java -javaagent:/home/dev/openjpa/lib/openjpa.jar com.xyz.Main
- java -javaagent:/home/openjpa/lib/openjpa.jar=addDefaultConstructor=false com.xyz.Main
可以使用OpenJPA's plugin syntax来为agent传递配置。此外也支持以下的选项:
- addDefaultConstructor: 输出的class文件的路径。
- enforcePropertyRestrictions: 如果entity使用property access,但是却没有遵守相关限制的时候,是否抛出异常。缺省是false。
- scanDevPath: 是否检查classpath来查找persistence class。如果没有指定persistent class list,同时设置这个标志为true,那么OpenJPA会检查每一个被载入到JVM的类。
- classLoadEnhancement: 指定是否使用load-time class enhancement。缺省是true。
- runtimeRedefinition: 指定是否使用class redefinition。缺省是true。
9.4 Omitting the OpenJPA enhancer
OpenJPA并不要求必须运行enhancer。如果没有运行enhancer,OpenJPA使用以下几种可能的替代方法。
9.4.1 Java 6 class retransformation
如果使用Java 6,那么OpenJPA会自动检测并尝试动态注册ClassTransformer来重定义persistence
class。同时OpenJPA也会为persistence
class创建子类。当执行query或者遍历关系的时候,OpenJPA会返回子类的对象,因此instanceof操作符仍然会正确工作,但是
o.getClass() 会返回子类类型。
9.4.2 Java 5 class redefinition
如果使用Java 5,并指定了OpenJPA agent,那么OpenJPA会使用Java 5 class redefinition
来重定义没有被agent加强的persistence
class。由于agent缺省会进行加强,因此这只有在为agent设置classLoadEnhancement
为false的时候有效(或者其它特殊情况)。
9.4.3 State comparison and subclassing
在以上情况外,OpenJPA会创建persistence class的子类。然而在有些情况下,OpenJPA无法在访问psersistence state的时候得到通知。
如果使用property
access,那么OpenJPA会为从数据库中查询得到的对象自动创建子类,并返回这个子类作为代理。这个子类中的getter和setter方法中增
添了额外的代码以便通知OpenJPA所有对persistence
state的访问。对于你自己创建的对象,由于无法使用子类代理,因此会在脏检查的时候使用state比较的方式。在这种方式下要额外保存该对象的一个
snap shot以便用于比较。
如果使用field access,那么OpenJPA无法跟踪对psrsistence
state的访问。因此OpenJPA会在脏检查的时候使用state比较的方式,代价是性能上的降低和内存使用的增加。此外,single-
valued fields (one-to-one, many-to-one, and any other non-collection or
non-map field that has a lazy loading
configuration)上的延迟加载的相关配置也被忽略,因为OpenJPA无法跟踪最这些field的访问。
10 Miscellaneous Features
10.1 Restoring State
JPA规范要求不应该使用rolled back objects,但是这些对象在OpenJPA中是有效的。可以通过配置openjpa.RestoreState 属性来控制是否将对象的状态回滚到事务前的状态。它有以下可选值:
- none: 不回滚对象状态,但是对象变成hollow,在下次访问的时候会重新加载。
- immutable: 回滚不可变的状态,可变的状态变成hollow,在下次访问的时候会重新加载。
- all: 回滚所有状态。
10.2 Typing and Ordering
当为对象的属性加载persistent
state的时候,OpenJPA会检查这个属性在声明时或者无参构造函数中被赋值的类型。如果这个类型比声明的类型更精确,那么OpenJPA会用这个
类型。如果你在初始化某个属性的时候使用了comparator,那么OpenJPA也会在每次加载persistent
state的时候使用这个comparator。例如:
- public class Company {
-
-
- private Collection employeesBySal = new TreeSet(new SalaryComparator());
- private Map departments;
-
- public Company {
-
- departments = new TreeMap();
- }
- }
10.3 Proxies
10.3.1 Smart Proxies
在运行时,OpenJPA可以通过代理跟踪entity 实例的属性是否被修改过,以便更有效地更新数据库。当设计persistent
class的时候,应该尽可能将collection
field映射成java.util.Set、java.util.TreeSet或java.util.HashSet。Smart
proxy对这些类型可以进行更好的优化。
10.3.2 Large Result Set Proxies
当遍历persistent collection 或者map属性的时候,ORM的缺省行为是把所有persistent
state载入到内存中。然而,如果载入的数据量过大,那么可能会降低性能。OpenJPA为这些large result
set属性使用特殊的代理。它并不在内存中缓存任何数据,相反它会访问数据库来获得相关的结果。例如large result set
collection的contains方法会导致在数据库中执行类似于SELECT COUNT(*)
WHERE的查询。类似地,在每次试图获得iterator的时候,OpenJPA会使用当前的large result
set配置来执行特定的query。在调用iterator.next方法的时候,OpenJPA会按需载入结果对象。此时需要通过
OpenJPAPersistence.close方法来释放资源,例如:
- import org.apache.openjpa.persistence.*;
-
- @Entity
- public class Company {
-
- @ManyToMany
- @LRS
- private Collection<Employee> employees;
-
- ...
- }
-
- Collection employees = company.getEmployees();
- Iterator itr = employees.iterator();
- while (itr.hasNext()) {
- process((Employee) itr.next());
- }
- OpenJPAPersistence.close(itr);
Large result set 属性只能被声明成java.util.Collection
或者java.util.Map;它不能包含externalizer;Large result set proxy不能从一个entity
实例转移到另外一个entity 实例中,例如以下代码会导致提交时的一个错误:
- Collection employees = company.getEmployees()
- company.setEmployees(null);
- anotherCompany.setEmployees(employees);
10.3.3 Custom Proxies
OpenJPA通过org.apache.openjpa.util.ProxyManager接口管理代理。其缺省的实现是org.apache.openjpa.util.ProxyManagerImpl。它有以下配置属性:
- TrackChanges: 是否使用smart proxy,缺省是true。
- AssertAllowedType: 在向collection或map中加入元素的时候,如果跟metadata 中声明的类型不符,是否抛出异常,缺省是false。
以下是个简单的例子:
- <property name="openjpa.ProxyManager" value="TrackChanges=false"/>
缺省的proxy manager可以代理 Collection, List, Map, Queue, Date, or Calendar 等类上的标准方法,也可以代理定制类型,但是这些定制类型必须复合以下条件:
- Custom container types必须有一个公共的无参构造函数,或者一个公共的以Comparator为参数类型的构造函数。
- Custom date types必须有一个公共的无参构造函数,或者一个公共的以long为参数 类型(代表当前的时间)的构造函数。
- Other
custom
types必须有一个公共的无参构造函数,或者公共的拷贝构造函数。如果没有公共的拷贝构造函数,那么在进行拷贝的时候,首先会创建一个对象B,然后通过
以A上getter方法的返回值为参数,调用B的setter方法。 因此你要确保通过这种方式,B是A的完整拷贝。
如果某个custom classes无法满足以上条件,那么OpenJPA允许你定义你自己的proxy class和proxy manager。
10.4 Externalization
OpenJPA 支持通过custom field mappings 来完全控制entity
class的field在datastore中怎样被保存、查询和加载。然而一个更轻量级的方法是使用externalization
。Externalization
支持通过指定某些方法来控制保存和加载的方式。需要注意的是,不能在@EmbeddedId字段上使用externalization。
OpenJPA使用org.apache.openjpa.persistence.Externalizer
annotation指定将某个属性转换为external
value的方法名。如果指定一个non-static方法,那么OpenJPA会假定目标对象是被Externalizer标记的属性对象;如果指定一
个static方法,
那么被OpenJPA会把被Externalizer标记的属性对象作为一个方法的参数。每个方法也可以接受一个StoreContext
型的参数。方法的返回值指定了缺省的external
类型。假设希望将某个CustomType类型的属性转换成String类型,那么可以采用以下的方法:
Method | Extension |
public String CustomType.toString() | @Externalizer("toString") |
public String CustomType.toString(StoreContext ctx) | @Externalizer("toString") |
public static String AnyClass.toString(CustomType ct) | @Externalizer("AnyClass.toString") |
public static String AnyClass.toString(CustomType ct, StoreContext ctx) | @Externalizer("AnyClass.toString") |
OpenJPA使用org.apache.openjpa.persistence.Factory annotation指定根据external
value初始化某个属性的方法名。
如果指定static方法,那么这个方法必须返回这个属性类型的一个实例。这个方法可以接受StoreContext
类型的一个参数。如果没有指定factory annotation,那么这个属性的class必须包含以external
form为参数的构造函数,否则会抛出一个异常。假设希望将String类型转换成某个CustomType类型,那么可以采用以下的方法:
Method | Extension |
public CustomType(String str) | none |
public static CustomType CustomType.fromString(String str) | @Factory("fromString") |
public static CustomType CustomType.fromString(String str, StoreContext ctx) | @Factory("fromString") |
public static CustomType AnyClass.fromString(String str) | @Factory("AnyClass.fromString") |
public static CustomType AnyClass.fromString(String str, StoreContext ctx) | @Factory("AnyClass.fromString") |
OpenJPA使用org.apache.openjpa.persistence.ExternalValues
annotation指定external value的转换。 其value
pairs用于指定Java和datastore的类型。如果datastore的类型不同于Java类型,那么通过
org.apache.openjpa.persistence.Type annotation
指定datastore类型。如果externalized属性不是标准的persistent
type,那么必须用org.apache.openjpa.persistence.Persistent
annotation显式地进行标记。如果externalized
属性是可变的,而且不是collection、map和date类型,那么OpenJPA无法进行脏检查。可以手动进行标记为dirty,或者使用
custom field proxy。以下是个简单的例子:
- @Entity
- public class Externalization {
- @Id
- @GeneratedValue(strategy = GenerationType.IDENTITY)
- private Integer id;
-
- @Persistent
- @Externalizer("getName")
- @Factory("forName")
- private Class clazz;
-
- @Persistent
- @Factory("Externalization.stringToURL")
- @Externalizer("Externalization.urlToString")
- private URL url;
-
- @Basic
- @ExternalValues({"SMALL=5", "MEDIUM=8", "LARGE=10"})
- @Type(int.class)
- private String size;
-
- public static URL stringToURL(String s) {
- try {
- return new URL(s);
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
- }
- }
-
- public static String urlToString(URL url) {
- return url.toExternalForm();
- }
-
- public String toString() {
- StringBuffer sb = new StringBuffer();
- sb.append("id: ").append(id);
- sb.append(", clazz: ").append(clazz);
- sb.append(", url: ").append(url);
- sb.append(", size: ").append(size);
- return sb.toString();
- }
- }
-
- EntityManager em = entityManagerFactory.createEntityManager();
- em.getTransaction().begin();
- Externalization e1 = new Externalization();
- e1.setClazz(Externalization.class);
- e1.setUrl(new URL("http://www.abc.com"));
- e1.setSize("MEDIUM");
- em.persist(e1);
- em.getTransaction().commit();
- em.close();
-
- em = entityManagerFactory.createEntityManager();
- Query q = em.createQuery("select m from Externalization m where m.url = 'http://www.abc.com'");
- List<Externalization> r1 = (List<Externalization>)q.getResultList();
- for(Iterator iter = r1.iterator(); iter.hasNext(); ) {
- System.out.println(iter.next().toString());
- }
- em.close();
以上代码执行完毕后数据库中externalization表的数据如下:
- mysql> select * from externalization;
- +
- | id | clazz | size | url |
- +
- | 1 | Externalization | 8 | http://www.abc.com |
- +
10.5 Fetch Groups
Fetch groups是一组同时被加载的属性。之前介绍的Fetch Type指定了如何通过annotations指定某个属性是eagerly还是 lazily 加载。Fetch groups 则是提供了动态指定加载方式的能力。.
10.5.1 Custom Fetch Groups
OpenJPA缺省fetch
group中的属性都会被eagerly加载。此外可以通过org.apache.openjpa.persistence.FetchGroup
annotation定义named fetch
groups以便在运行时激活它。在加载的时候,OpenJPA会eagerly加载所有被激活的fetch
groups中的属性。FetchGroup annotation有以下属性:
- String name: fetch group 的全局名。以下名字被OpenJPA保留:default 、values、 all、none以及任何以jdo、jpa或openjpa开头的名字。
- FetchAttribute[] attributes: fetch group中persistent fileds或者properties数组。
- String[] fetchGroups: 包含在次fetch group中的其它fetch groups名。
org.apache.openjpa.persistence.FetchAttribute annotation的属性如下:
- String name: persistent field 或者 property 名。
- recursionDepth: eager-fetch 的递归深度,缺省是1(-1无限制)。
Entity class的某个属性可以包含在任何的fetch group中,它也可以声明其load fetch
group。缺省情况下,OpenJPA在第一次访问lazy-loaded属性的时候访问数据库。然而如果你直到当你访问某个lazy-loaded属
性A的时候,你很可能也同时访问lazy-loaded 属性B和C,因此在一次数据库访问中同时加载A、B和C是更有效的。
通过org.apache.openjpa.persistence.LoadFetchGroup annotation为某个属性指定load fetch group。以下是个简单的例子:
- import org.apache.openjpa.persistence.*;
-
- @Entity
- @FetchGroups({
- @FetchGroup(name="detail", attributes={
- @FetchAttribute(name="publisher"),
- @FetchAttribute(name="articles")
- })
- })
- public class Magazine {
-
- @ManyToOne(fetch=FetchType.LAZY)
- @LoadFetchGroup("detail")
- private Publisher publisher;
- }
10.5.2 Custom Fetch Group Configuration
OpenJPAEntityManager接口和OpenJPAQuery接口可以用于访问
org.apache.openjpa.persistence.FetchPlan。FetchPlan通过以下方法维护当前活跃的fetch
groups和maximum fetch depth:
- public FetchPlan addFetchGroup(String group);
- public FetchPlan addFetchGroups(String... groups);
- public FetchPlan addFetchGroups(Collection groups);
- public FetchPlan removeFetchGrop(String group);
- public FetchPlan removeFetchGroups(String... groups);
- public FetchPlan removeFetchGroups(Collection groups);
- public FetchPlan resetFetchGroups();
- public Collection<String> getFetchGroups();
- public void clearFetchGroups();
- public FetchPlan setMaxFetchDepth(int depth);
- public int getMaxFetchDepth();
Maximum fetch
depth指定了加载时遍历对象的深度,缺省值是-1(无限制)。如果MaxFetchDepth是1,那么OpenJPA将会加载目标实例和它的直接
relations;
如果MaxFetchDepth是2,那么OpenJPA会加载目标实例、直接relations和直接relations上的relations;如果
MaxFetchDepth是-1,那么OpenJPA会加载目标实例以及所有的relatons直到到达对象图的边缘。当然以上的加载过程依赖于被加载
属性是否是eagerly load,以及当前活跃的fetch group。以下是个简单的例子:
- OpenJPAQuery kq = OpenJPAPersistence.cast(em.createQuery(...));
- kq.getFetchPlan().setMaxFetchDepth(3).addFetchGroup("detail");
- List results = kq.getResultList();
10.5.3 Per-field Fetch Configuration
除了基于per-fetch-group 的配置外,OpenJPA也支持基于per-field的配置。通过以下方法将特定的field包含到当前的fetch plan中。
- public FetchPlan addField(String field);
- public FetchPlan addFields(String... fields);
- public FetchPlan addFields(Class cls, String... fields);
- public FetchPlan addFields(Collection fields);
- public FetchPlan addFields(Class cls, Collection fields);
- public FetchPlan removeField(String field);
- public FetchPlan removeFields(String... fields);
- public FetchPlan removeFields(Class cls, String... fields);
- public FetchPlan removeFields(Collection fields);
- public FetchPlan removeFields(Class cls, Collection fields);
- public Collection<String> getFields();
- public void clearFields();
需要注意的是,以上方法中的field必须定义在指定的类中,而不是在其父类中。如果field
publisher被定义在Publication中,而不是其子类Magazine中,那么必须用 addField
(Publication.class, "publisher"),而不是addField (Magazine.class,
"publisher")。出于性能上的考虑,OpenJPA并不对class name / field name
对进行验证。如果指定了不存在的class name / field name 对,那么会被OpenJPA忽略。以下是个简单的例子:
- OpenJPAEntityManager kem = OpenJPAPersistence.cast(em);
- kem.getFetchPlan().addField(Magazine.class, "publisher");
- Magazine mag = em.find(Magazine.class, magId);