- はじめに
- モデル
- 構成
- Payara 起動クラス
- persistence.xml
- web.xml
- ベースエンティティ
- Owner
- Pet
- Visit
- Repository
- OwnerRepository の実装
- PetRepository の実装
- VisitRepository の実装
- まとめ
はじめに
Spring MVC ベースのサンプル・アプリケーション Spring Petclinic を JavaEE MVC 1.0 に移植してみます。
MVC 1.0 は Early Draft 段階ではありますが、JavaEE 7 環境でもそれなりに動かすことができます。ここでは、なるべく JavaEE 標準機能をそのままに JavaEE Petclinic を作っていきます。
モデル
モデルは Owner
が Pet
を持っていて、Pet
は来院の履歴として Visit
を持つ といった単純なものです。
構成
Payara micro を使います。MVC 1.0 の RI は ozark 1.0.0-m02 が出ているのでこれを使います。データベースは H2 を使います。
Spring Petclinic ではビューテンプレートに Thymeleaf を使っているので JavaEE Petclinic でも Thymeleaf を使うことにします。
ということで依存は以下の定義になります。
dependencies { compile 'fish.payara.extras:payara-micro:4.1.1.161' compile 'org.glassfish.ozark:ozark:1.0.0-m02' compile 'org.thymeleaf:thymeleaf:3.0.3.RELEASE' compile 'org.webjars:bootstrap:3.3.6' compile 'org.webjars:jquery:2.2.4' compile 'org.webjars:jquery-ui:1.11.4' compile 'com.h2database:h2:1.4.191' }
Ozark の Thymeleaf extension もありますが、2.0系だったり Fragments が上手く扱えないなどの問題があるため、自作することにします。
build.gradle の全体像は以下のようになります。
plugins { id 'java' id 'war' id 'application' } sourceCompatibility = targetCompatibility = '1.8' mainClassName = 'code.javaee.sample.petclinic.Main' repositories { jcenter() } dependencies { compile 'fish.payara.extras:payara-micro:4.1.1.161' compile 'org.glassfish.ozark:ozark:1.0.0-m02' compile 'org.thymeleaf:thymeleaf:3.0.3.RELEASE' compile 'org.webjars:bootstrap:3.3.6' compile 'org.webjars:jquery:2.2.4' compile 'org.webjars:jquery-ui:1.11.4' compile 'com.h2database:h2:1.4.191' } task explodedWar(type: Copy) { into "$buildDir/exploded" with war } war { archiveName = 'petclinic.war' rootSpec.exclude('**/payara/**') rootSpec.exclude('**/payara*.jar') dependsOn explodedWar } task uber(type: JavaExec) { dependsOn war classpath = sourceSets.main.runtimeClasspath main = 'fish.payara.micro.PayaraMicro' args '--deploy', war.archivePath.path, '--outputUberJar', "$buildDir/uber.jar" }
Payara 起動クラス
最初に Payara 起動用の main メソッドを作成しておきましょう。
public class Main { public static void main(String[] args) throws Exception { org.h2.tools.Server.createWebServer().start(); org.h2.tools.Server.createTcpServer("-tcpAllowOthers").start(); File war = (args != null && args.length > 0) ? new File(args[0]) : new File("build/libs/petclinic.war"); PayaraMicro micro = PayaraMicro.getInstance(); micro.setNoCluster(true); PayaraMicroRuntime instance = micro.bootStrap(); instance.deploy(war); } }
最初にH2の起動をしています。
続いて PayaraMicro に war をデプロイしています。
persistence.xml
JPA を使うので persistence.xml も作成しておきます。
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.1" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"> <persistence-unit name="PetClinicPU" transaction-type="JTA"> <jta-data-source>java:app/PetClinicDataSource</jta-data-source> <exclude-unlisted-classes>false</exclude-unlisted-classes> <properties> <property name="javax.persistence.schema-generation.database.action" value="create"/> <property name="javax.persistence.sql-load-script-source" value="META-INF/sql/load_script.sql"/> <property name="eclipselink.logging.level" value="FINE"/> <property name="eclipselink.logging.parameters" value="true"/> <property name="eclipselink.cache.shared.default" value="false"/> </properties> </persistence-unit> </persistence>
javax.persistence.sql-load-script-source
で SQLファイルを指定しておくと、起動時にSQLを実行してくれます。後ほど初期データ投入用のSQLファイルを作成します。
無用なトラブル(fetch で取得した対象の更新がキャッシュに反映されないなど)を避けるために eclipselink.cache.shared.default
でキャッシュを無効化しておきます。
web.xml
データソースの登録は DataSource Resource Definition で web.xml に書いておけば起動時に登録されます。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <data-source> <name>java:app/PetClinicDataSource</name> <class-name>org.h2.jdbcx.JdbcDataSource</class-name> <url>jdbc:h2:tcp://localhost/./build/PetClinicDB;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1</url> <user>sa</user> <password>sa</password> </data-source> </web-app>
パスワードとか書いてしまっているので商用環境では使えませんが、アプリケーションサーバ固有の定義方法(Glassfish であれば glassfish-resources.xml を WEB-INF ディレクトリーに配備)よりかはポータビリティに優れます。
ベースエンティティ
JPAで定義します。最初に エンティティの基底になる BaseEntity
を準備します。
@MappedSuperclass public class BaseEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; public boolean isNew() { return this.id == null; } // ... }
合わせて名前付きの NamedEntity
を準備します。
@MappedSuperclass public class NamedEntity extends BaseEntity { @Column(name = "name") private String name; // ... }
Owner
Person
を継承した Owner
エンティティです。
@MappedSuperclass public class Person extends BaseEntity { @Column(name = "first_name") @NotEmpty private String firstName; @Column(name = "last_name") @NotEmpty private String lastName; // ... } @Entity @Table(name = "owners") public class Owner extends Person { @Column(name = "address") @NotEmpty private String address; @Column(name = "city") @NotEmpty private String city; @Column(name = "telephone") @NotEmpty @Digits(fraction = 0, integer = 10) private String telephone; @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") private Set<Pet> pets; // ... }
Pet
Pet
エンティティです。
@Entity @Table(name = "pets") public class Pet extends NamedEntity { @Column(name = "birth_date") @Temporal(TemporalType.DATE) @NotNull private Date birthDate; @ManyToOne @JoinColumn(name = "type_id") @NotNull private PetType type; @ManyToOne @JoinColumn(name = "owner_id") private Owner owner; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name="pet_id", referencedColumnName="id") private Set<Visit> visits = new LinkedHashSet<>(); // ... }
PetType
は NamedEntity
の子供でクラス定義だけです。
@Entity @Table(name = "types") public class PetType extends NamedEntity { }
Visit
Visit
エンティティです。
@Entity @Table(name = "visits") public class Visit extends BaseEntity { public static final String findByPetId = "Visit.findByPetId"; @Column(name = "visit_date") @Temporal(TemporalType.TIMESTAMP) private Date date; @NotEmpty @Column(name = "description") private String description; @Column(name = "pet_id") private Integer petId; // ... }
Repository
Spring Petclinic では Spring Data を使っているのでインターフェース定義だけしておき、実装はランタイム時に自動生成されます。
ここでは JPA の標準機能を使うので、実装は自作することにします。
最初に Repository
インターフェース用意します。
public interface Repository<T, ID extends Serializable> { }
OwnerRepository
を EJB のローカルインターフェースとして作成します。
@Local public interface OwnerRepository extends Repository<Owner, Integer> { void save(Owner owner); Owner findById(int ownerId); Collection<Owner> findByLastName(String lastName); }
PetRepository
も同様です。
@Local public interface PetRepository extends Repository<Pet, Integer> { List<PetType> findPetTypes(); Pet findById(Integer id); void save(Pet pet); }
VisitRepository
も同様ですね。
@Local public interface VisitRepository extends Repository<Visit, Integer> { void save(Visit visit); List<Visit> findByPetId(Integer petId); }
OwnerRepository の実装
先程作成した Repository の実装を作成します。
最初に NamedQueries 定義をしておきます。
@Entity @Table(name = "owners") @NamedQueries({ @NamedQuery(name = findByLastName, query = "SELECT DISTINCT owner FROM Owner owner left join fetch owner.pets WHERE owner.lastName LIKE :lastName"), @NamedQuery(name = findById, query = "SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id") }) public class Owner extends Person { public static final String findByLastName = "Owner.findByLastName"; public static final String findById = "Owner.findById"; //... }
OwnerRepository の実装は以下のようになります。
@Stateless public class OwnerRepositoryImpl implements OwnerRepository { @PersistenceContext(unitName = "PetClinicPU") private EntityManager em; @Override public void save(Owner owner) { if (owner.isNew()) { em.persist(owner); } else { em.merge(owner); } } @Override public Owner findById(int ownerId) { return em.createNamedQuery(Owner.findById, Owner.class) .setParameter("id", ownerId).getSingleResult(); } @Override public Collection<Owner> findByLastName(String lastName) { return em.createNamedQuery(Owner.findByLastName, Owner.class) .setParameter("lastName", lastName + "%").getResultList(); } }
PetRepository の実装
後ほどプルダウンで使う PetType
に NamedQuerie 定義します。
@Entity @Table(name = "types") @NamedQueries({ @NamedQuery(name = findPetTypes, query = "SELECT ptype FROM PetType ptype ORDER BY ptype.name") }) public class PetType extends NamedEntity { public static final String findPetTypes = "Pet.findPetTypes"; }
PetRepository は以下のようになります。
@Stateless public class PetRepositoryImpl implements PetRepository { @PersistenceContext(unitName = "PetClinicPU") private EntityManager em; @Override public List<PetType> findPetTypes() { return em.createNamedQuery(PetType.findPetTypes, PetType.class).getResultList(); } @Override public Pet findById(Integer id) { return em.find(Pet.class, id); } @Override public void save(Pet pet) { if (pet.isNew()) { em.persist(pet); em.flush();em.clear(); } else { em.merge(pet); } } }
VisitRepository の実装
同様に NamedQuerie 定義します。
@Entity @Table(name = "visits") @NamedQueries({ @NamedQuery(name = findByPetId, query = "SELECT visit FROM Visit visit WHERE visit.petId = :petId") }) public class Visit extends BaseEntity { public static final String findByPetId = "Visit.findByPetId";
VisitRepository は以下のようになります。
@Stateless public class VisitRepositoryImpl implements VisitRepository { @PersistenceContext(unitName = "PetClinicPU") private EntityManager em; @Override public void save(Visit visit) { if (visit.isNew()) { em.persist(visit); } else { em.merge(visit); } } @Override public List<Visit> findByPetId(Integer petId) { return em.createNamedQuery(Visit.findByPetId, Visit.class) .setParameter("petId", petId).getResultList(); } }
まとめ
MVC の話に入れませんでしたが、下準備ができました。
次回から Controller
を作成していきましょう。