JPAによるリレーション: @OneToManyと@ManyToOne


ユーザ認証ができたので、これまでに作った掲示板を改良してみます。名前とコメントが入力できるようになっていますが、名前をCommentのフィールドにするのではなく、CommentとUserを関連付けます。

ユーザはコメントを複数書くことができるので、UserとCommentは1対多の関係となります。ユーザごとにコメント表示できるようにし、またコメントがどのユーザのものかを表示できるようにするため、双方向の関係を持つようにします。

このような関係(リレーション)を定義するにはEntityにアノテーションをつけます。

Entity

まず、UserにCommentを格納するフィールドを追加します。Userが1でCommentが多と定義するには@OneToManyをつけます。

  • 30行目: @OneToManyアノテーションで「1対多」であることを表す。アノテーションの引数であるmappedByについては後述。
  • 31行目: 「多」となるEntityをListで格納できるようにする。

Commentは「多」なので@ManyToOneアノテーションをつけます。

  • 21行目: @MaynToOneアノテーションで「多対1」であることを表す。
  • 22行目: 「1」となるEntityを格納できるようにする。

それぞれのEntityにアノテーションを付けたので双方向の関係(UserからCommentを、CommentからUserを取得できる)になります。

これに合わせてControllerとViewも修正します。

Controller

Controllerでは、コメントのPOST時にUserを取得し、Commentにセットしてから保存します。

View

Viewでは、名前の入力欄を削除し、表示される名前はUserのusernameに変更します。

  • 10行目: commentからuserを取得しそのフィールドであるusernameを表示する。
  • 13行目: 名前の入力欄は削除し、textの入力欄だけにする。

これで、UserとCommentを関連付けられました。

データベースにはどのように保存されているのか

@OneToManyと@ManyToOneをつかって双方向の1対多のリレーションを設定した場合、RDMSではどのように保存されているのでしょうか。 H2コンソールを使って中身を覗いてみましょう。

Userが保存されるUSERSテーブルは次のようになります。

Dbuser

UserにcommentListは見当たりません。

Commentが保存されるCOMMENTテーブルは次のようになります。

Dbcomment

UserのIDが格納されるカラムができています。

このようなテーブルになっているのは@OneToManyにmappedByがついているためです。

通常、@OneToManyをつけた場合は、Listを格納するために専用のテーブル(この場合、UserのIDとCommentのIDの対応が保存されるテーブル)が作成されます。@ManyToOneの場合は上記のようにテーブルにカラムが追加されます。これだと情報を二重にもってしまうことになります。また、UserにCommentのListを保存してsaveする必要があります。これでは、コードも煩雑になり、コードにミスがあればデータベースに不整合がおこりかねません。

これを防ぐために、@OneToManyと@ManyToOneの双方向のリレーションを設定する場合は、@OneToManyがつけられたEntityにアノテーションの引数でmappedByを指定し、参照するデータをもう一方の@ManyToOneがつけられたEntityのものにします。

今回の場合は、UserのcommentListは(UserのIDとCommentのIDの対応が保存されるテーブルからデータを取得するのではなく)CommentのUserフィールドの情報から取得していることになります。

スケジューリング(定期的に処理する)


サーバ側で定期的に処理を行いたい場合、広く使われているのがCronですが、Springではスケジューリングに関する機能があります。このスケジューリング機能を使ってみましょう(参考: 28.4 Annotation Support for Scheduling and Asynchronous Execution)。

まず、スケジューリング機能を有効にします。@EnableSchedulingアノテーションを使います。

次に、定期的に実行したいメソッドに@Scheduledアノテーションをつけます。システムにDIされるオブジェクト(例えばコントローラー)ならばなんでもかまいません。ここでは、@Serviceを指定したクラスにします。

  • 9行目: fixedRateで実行間隔を指定する。
  • 14行目: cronでcrontabファイルと同様の記法で実行間隔を指定する。

これで定期的に実行されます。

scheduled tasks

ユーザ認証: ログインしたユーザの情報をコントローラで取得する


ログインしたユーザの情報をコントロールで取得してみましょう。

ユーザ名はServletのリクエスト情報を表すHttpServletRequestクラスのオブジェクトのgetRemoteUserメソッドで取得できます。このユーザ名からUserクラスのオブジェクトを取得します。

コントローラは次のようになります。

  • 1行目: 引数にHttpServletRequestを指定することによってそのオブジェクトが取得できる。
  • 2行目: getRemoteUserメソッドでユーザ名を取得する。
  • 3行目: getOneメソッドは、JpaRepositoryに最初から用意されているメソッドで、IDからインスタンスを取得することができる。
  • 4行目: addAttributeメソッドで、userをビューに送る。String以外のオブジェクトでもビューに送ることができる。

ビューは次のようにします。

  • 12,14行目: コントローラーからビューに送られてきたオブジェクトはgetterが設定されていればフィールドを取得することができます。usernameとpasswordはLombokによってgetUsername、getPasswordというgetterが生成されているため、${user.username}、${user.password}で値を取り出すことができます。

http://localhost:8080/userにアクセスすると(ログインしていない場合はログイン画面になり、ログインすると)ユーザ情報が表示されます。

User

ユーザ認証: ユーザ情報をデータベースに保存する


単純なログインでは、ユーザ情報をソースコードに記述し、実行時はAuthenticationManagerBuilderによって作られるメモリ上のデータベースに保存するようになっています。これではユーザの追加のたびにソースコードの修正が必要になってしまうので、ユーザ情報をデータベースに置くように変えていきましょう(参考: 3.5. Authentication)。また、ブラウザからサインアップできるようなインターフェースを作成します。

JDBC Authentication

WebSecurityConfig.javaを次のように修正します。

  • 19行目: /signupもユーザ認証無しでアクセスできるようにする。
  • 25,26行目: データベースを使うのでDataSourceをDIします。
  • 30行目: AuthenticationManagerBuilderのjdbcAuthenticationメソッドでデータベースからユーザ情報を読み取ることを指定します。dataSourceメソッドで利用するDataSourceを指定します。

3.5.2. JDBC Authenticationでは、withDefaultSchemaメソッドを実行しています。これは、ユーザ情報用のテーブルを用意してくれる機能ですが、ここではこの機能を使わずEntityを自分で作成することにします。

EntityとRepository

ユーザ情報を格納するEntityと、Entityを扱うRepositoryを作成します。

  • 11行目: AuthenticationManagerBuilderはテーブル名が「USERS」となっているテーブルから取得するためテーブル名を明示的に指定する(@Tableアノテーションを指定しない場合、テーブル名はクラス名と同じになる)。
  • 14〜23行目: username、password、enabledという3つのフィールド(テーブルのcolumn)が必要となる。Lombokでgetter/setterを生成する。

Userと同様に、Authorityを作成する。

View

Sign up

Controller

  • 1〜4行目: RepositoryをDIする。
  • 6〜9行目: @RequestParamでmethodを指定するとメソッド毎に処理を分けることができる。GETの場合はviewを指定するだけ。
  • 11〜23行目: POSTの場合は送られてきたパラメータを使ってUserとAuthorityのインスタンスを生成し、データベースにセーブする。

これは最も単純な場合なので、すでに同じユーザ名で登録されていた場合の処理がされていない、パスワードが平文で保存されている等いろいろ修正しなければなりませんが、とりあえずはこれでユーザ情報を登録して認証に使えるようになります。

Lombokの利用


Lombokは所謂「boilerplate」なコードを自動生成することができるライブラリです。IDEでもコード(例えばgetter/setter)を自動生成することができますが、コードの追加・変更・削除に応じて自動生成し直す必要があり、少々面倒です。Lombokを使うことによってboilerplateなコードを書かなくてすむようになります。LombokのメリットについてはJavaでIDEのアクセッサ生成よりlombokを使ったほうがいい理由が参考になります。

IntelliJの設定

Lombok Pluginをインストールします。メニューから[Preferences]→[Plugins]→[Browse repositories]からインストールできます。

プロジェクトの設定では[Compiler]→[Annotation Processors]の[Enable annotation processing]をonにします。

Preferences

build.gradle

Lombokを利用するプロジェクトのbuild.gradleには次の行をdependenciesに追加します。

これでLombokが使えるようになります。

※上記ではcompileでLombokを利用するようにしていますが、実行時にLombokは必要ないので、本来はmavenにおけるprovidedと同様の動作となるようにするのが適切です。ここでは(手を抜いて)compileで指定しています。mavenのprovidedと同様の動作にするには、例えばWhich is the proper Gradle plugin to support ‘provided’ method?に書かれている方法等があります。

ユーザ認証: 単純なログイン


ユーザ認証を行ってみましょう(参考: Getting Started · Securing a Web Application)。

spring-boot-starter-security

Springにはセキュリティ関係の機能をサポートするSpring Securityがあり、またSpring Bootで利用する場合はspring-boot-starter-securityを使うことができます。build.gradleのdependenciesに追加します。

ログインフォーム

まずログインフォームを作り、アクセスできるようにしておきます。

テンプレートは、/loginにusernameとpasswordを送るようにします。

コントローラでは/loginにアクセスがあったときに上記のテンプレートが表示されるようにします。Getting Started · Securing a Web Applicationでは、WebMvcConfigurerAdapterを使ってビューを追加していますが、どちらでも構いません。

WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapterを継承した設定用クラスを作り、overrideしたconfigureメソッドで設定を行います。

  • 10行目: 設定用クラスであることを指定する。
  • 11行目: Spring Securityのウェブ用の機能を利用することを指定する。
  • 14〜18行目: configureメソッドをoverrideし、ここで設定を行う。16行目ではトップページ(/)は認証無しでアクセス可、それ以外は認証が必要なことを設定している。複数のページを認証なしでアクセス可とする場合は「antMatchers(“/”, “/sample”)」のように複数指定することができる。17行目ではログインフォームのURLが/loginで、ここへのアクセスも全てのユーザがアクセス可能としている。
  • 20〜27行目: GlobalAuthenticationConfigurerAdapterを継承したクラスのinitメソッドで認証するユーザとパスワードを設定する。

ログインの確認

トップページ(/)以外のページにアクセスすると/loginにリダイレクトされます。

Login

ユーザ名・パスワードがあっていれば、元のURLに移動します。ユーザ名・パスワードが間違っている場合はエラーメッセージが表示されます。

Login error

Spring-Loadedの利用


一般に、Servlet環境での開発は、プログラムを変更したら、再デプロイをして動作を確認します。しかし再デプロイは時間がかかるため開発効率がよくありません。このため再デプロイすることなくプログラムの追加・変更を適用することができる「hot swap」や「hot deploy」と呼ばれる手法が使われることがあります。

Spring-LoadedはSpringアプリケーションのhot deployを実現するライブラリです。Spring-Loadedを利用するにはgradle.buildを次のようにします(参考: 67.6.1 Configuring Spring Loaded for use with Gradle and IntelliJ)。

  • 7行目: Spring-Loadedの利用を指定する。
  • 12行目: gradleのideaプラグインを利用する。
  • 18〜23行目: IDEAによる出力をgradleの出力先に合わせる。

Intellijの設定では、「Make Project Automatically」をonにします。

これによりbootRunで実行中にプログラムを書き直すと起動中のプログラムも修正されます。

Spring Bootの利用


これまで行ってきたように、ウェブアプリケーションの開発では様々なライブラリ/フレームワークを使います。grailsやmaven等のツールを用いることにより様々なライブラリ等が簡単に利用可能となりますが、それでも設定ファイルの作成等、面倒な作業が多く、ミスの元になったりします。

「Spring Boot」は「Spring Frameworkを使ってさらに簡単にウェブアプリケーションを作る仕組み」です。Spring Bootを使うことで標準的な設定がなされたウェブアプリケーションのベースを利用することができます。ただし、これまで紹介してきた設定の知識が不要になる、というわけではありません。自動設定(というか定義済み設定)が利用できるというだけで、自動で設定できない部分はこれまでと同様に設定する必要があります。

具体的には「Starter」を利用することになります。使いたい機能のStarterを加えることでライブラリの導入や設定が行われます。用意されているStarterの一覧は12.4 Starter POMsにまとめられています。

それでは、ここまで(掲示板の作成まで)と同等の環境をSpring Bootで構築してみましょう。

Gradle

プロジェクトはこれまでと同様にGradleを利用して作成し、build.gradleを次のように編集します(参考: 9.1.2 Gradle installation)。

  • 1〜8, 11行目: Spring Bootのgradleプラグインを利用する。
  • 21〜26行目: 利用するstarterの指定。ここではJetty(Tomcatを使わない)、Thymeleaf、Spring Data JPAを利用するように指定している。
  • 27行目: H2を利用する。

起動用のクラス

Spring Bootはウェブアプリケーションを実行する環境も用意することができます(これまでと同様にwarファイルを生成することももちろんできます)。Gradleの設定ではJettyを利用するように設定しました。Jettyを起動してウェブアプリケーションを実行するクラスを作成します(参考: 13.2 Locating the main application class)。

  • 8行目: このクラスが設定用のクラスであることを指定する。
  • 9行目: 自動設定を有効にする。
  • 10行目: このクラスのパッケージ内のクラスをDIの対象にする。
  • 13行目: ウェブアプリケーションを起動する。

設定ファイル

Spring Bootは設定が自動でされることが特徴ですが、最低限の設定はしなければなりません。またデフォルトの設定を変更したい場合もあります。その設定の方法の1つとして、application.propertiesを使う方法があります(参考: Appendix A. Common application properties)。

resourcesディレクトリにapplication.propertiesを作成します。

ここでは、データベースファイルの置き場所、ユーザ名、パスワード、ドライバを設定し、HibernateのDDL自動生成は、create-drop(削除して再作成)としている。

ディレクトリ構成

これまではWEB-INFというディレクトリがありましたが、Spring Bootのデフォルトではここは使いません。Thymeleafのテンプレートのデフォルトはresources/templatesに置きます。

Spring Bootのディレクトリ構成

実行

実行は次のようにgrablewから行います(Windowsの場合はgradlew.bat)。事前に「chmod +x grablew」として実行可能としておきます。

./gradlew bootRun

IntelliJのTerminalから実行するのがよいでしょう。

grablew bootRun

Started Applicationと表示されたら、実行中です。終了するにはキーボードからCtrl+Cを押します。

文字化け対策: CharacterEncodingFilterの利用


文字化けは様々な原因が考えられますが、Springの環境ではCharacterEncodingFilterを使うことで文字化けをなくすことができる場合があります。

CharacterEncodingFilterはweb.xmlで設定します。

掲示板を作る: Viewの作成


最後にViewを作って完成です。

Controllerの返り値として「bbs/index」を指定しているので、templatesディレクトリの中にbbsフォルダを作ってその中にindex.htmlを作成します。

  • 10行目: th:eachを使うとリストから1つずつ取り出して、要素が生成される。この場合commentListから取り出した値はcommentとして参照できる。comment.nameでcomment.getName()が実行される。
  • 12行目: actionの代わりにth:actionを指定すると@{}を使ってURLを記述することができる。@{}を使うと、contextに応じたpathに書き換えてくれる。

実行例

BBS

H2コンソールで確認すると、idが自動的に割り当てられているのがわかります。

H2console2