Spring Petclinic を JavaEE MVC 1.0 (JSR-371) で作る 〜 その1 〜


f:id:Naotsugu:20170405210338p:plain

はじめに

Spring MVC ベースのサンプル・アプリケーション Spring Petclinic を JavaEE MVC 1.0 に移植してみます。

MVC 1.0 は Early Draft 段階ではありますが、JavaEE 7 環境でもそれなりに動かすことができます。ここでは、なるべく JavaEE 標準機能をそのままに JavaEE Petclinic を作っていきます。

モデル

モデルは OwnerPet を持っていて、Pet は来院の履歴として Visit を持つ といった単純なものです。

f:id:Naotsugu:20170405210110p:plain

構成

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<>();
    // ...
}

PetTypeNamedEntity の子供でクラス定義だけです。

@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 を作成していきましょう。

etc9.hatenablog.com