SEチャンネル

ITについてできる限り書くメモ

Spring+MongoDBでWebアプリケーション入門(1/3)

Spring FrameworkとMongoDBを使用した、アンケートWebアプリ作成手順について解説する。アンケートサイトのような軽めのWebサイトではアンケート項目追加などデータ構造の変更が発生するため、今回はスキーマレスのMongoDBを使用している。単純に使ってみたかっただけです。

Spring自体が重厚長大なので、よりさっくりと作りたければ最近はやりのMEANスタックを利用するのが良いかもしれない。

githubに公開したらリンクを追加予定。

全体の設計

アプリを作りはじめる前に、作ろうとしているアプリの全体をきちんと決めておくことが重要となる。今回はそれほど画面数はないが、「だれが」「何をする」という観点でまとめると以下のようになる。

利用者 URL 何をするか
ユーザ /answer/{userid} ユーザがアンケートを回答するページ。
ユーザ /answer/completed アンケート回答後に表示されるページ。
管理者 /adminlogin 管理者用ログイン画面。
管理者 /admin/view 管理者がアンケートの結果一覧を参照する。

今回はユーザ向けの画面と、ファイル入出力で実装したDBのモックを作成する。作成後のプロジェクト構成は以下のようになる。

f:id:tkmtys:20150930083118p:plain

sponsor

View

まずはViewを作成する。とはいっても画面は後で必ず修正することになるので、最初は質問が2つの単純なものを用意する。アプリ作成後、必要があればスタイルシートやフォームを追加する。

answer.jsp

<p>アンケートへの回答よろしくお願いします。</p>
<form:form action="${pageContext.request.contextPath}/completed"
    method="post" modelAttribute="surveyForm">
            
    <form:input type="hidden" path="userid" />
            
    <p>性別</p>
    <p>あなたの性別を教えて下さい</p>
    <ul>
        <li><form:radiobutton path="survey1answer" label="男性" value="男性"/></li>
        <li><form:radiobutton path="survey1answer" label="女性" value="女性"/></li>
    </ul>

    <p>セミナーの感想</p>
    <p>セミナーの感想を教えて下さい</p>
    <ul>
        <li><form:radiobutton path="survey2answer" label="良かった" value="良かった"/></li>
        <li><form:radiobutton path="survey2answer" label="普通" value="普通"/></li>
        <li><form:radiobutton path="survey2answer" label="悪かった" value="悪かった"/></li>
    </ul>
            
    <input type="submit" value="送信" />
</form:form>

アンケート回答後に表示するjspは省略する。

Controller

次はController部分だ。機能ごとにControllerは分割したほうがよいため、管理者用とユーザ用でそれぞれControllerを作成する。ユーザ向け画面のコントローラは以下のようになる。

UserController

@Controller
public class UserController {

    @Autowired
    private SurveyService surveyService;
    
    @RequestMapping(value="/answer/{userid}")
    public ModelAndView answer(@PathVariable String userid) {
        
        if ( surveyService.hasAlreadyAnswered(userid) ) {
            return new ModelAndView("completed");
        }
        
        ModelAndView model = new ModelAndView("answer");
        SurveyForm form = new SurveyForm();
        form.setUserid(userid);
        model.addObject("surveyForm", form);
        return model;
    }
    
    @RequestMapping(value="/completed", method=RequestMethod.POST)
    public String completed (@ModelAttribute("surveyForm") SurveyForm form) {
        surveyService.insertSurveyAnswer(form);
        return "completed";
    }
    @RequestMapping(value="/completed", method=RequestMethod.GET)
    public String completed () {
        return "completed";
    }

}

/answer/{userid}がユーザがアンケートを回答しようとしてアクセスしてくるURLである。マッピングされているanswerメソッドでは、URLに含まれるuseridを使用して(これから作成する)SurveyService#hasAlreadyAnsweredメソッドでユーザが回答済みかどうかを問い合わせを行い、回答済みであればcompletedビューを、未回答であればanswerビューをそれぞれ表示している。answerビューを表示するときにはSurveyFormを利用してuseridを渡している。

ユーザが回答の送信ボタンを押した場合は/completedにPOSTのリクエストが送られてくる。この際はSurveyService#insertSurveyAnswerを使用して画面のフォームからわたって来たアンケート回答結果をDBに永続化してcompletedビューを表示している。

ちなみにFormは以下のようになる。

public class SurveyForm {
    private String userid;
    private String survey1answer;
    private String survey2answer;

    //getter/setterは省略
}

Service

SurveyService

public class SurveyService {

    @Autowired
    private SurveyAnswerRepository surveyAnswerRepo;
    
    public boolean hasAlreadyAnswered(String userid) {
        List<SurveyAnswerEntity> list = surveyAnswerRepo.selectByUserid(userid);
        return list.size() > 0;
    }
    
    public void insertSurveyAnswer(SurveyForm form) {
        SurveyAnswerEntity ent = new SurveyAnswerEntity();
        ent.setUserid(form.getUserid());
        ent.setDate(new Date().toString());
        ent.setSurvey1answer(form.getSurvey1answer());
        ent.setSurvey2answer(form.getSurvey2answer());

        surveyAnswerRepo.insertEntity(ent);
    }
}

hasAlreadyAnsweredメソッドはuseridのユーザがすでに回答済みかどうかを判断する。ロジックは簡単でRepositoryからuseridでselectした結果が0件以上かどうかで実装している。

一方、insertSurveyAnswerメソッドは逆に画面から入力された値であるFormの値をRepositoryに永続化しているだけである。

ここで注意しなければいけないのはFormはそのままRepositoryに渡してはいけない点だ。Formは画面(View)とController間でデータ授受を行うためのクラスであり、ServiceとRepository間はEntityあるいはDTOでデータを交換するべきである。もしRepository層でFormを使用していた場合、画面変更(Formの変更)が発生した際にRepositoryレイヤーまで影響が出てしまい、MVCに分けたメリットがなくなってしまう。

Repository

最後にRepositoryインターフェースである。今はファイル入出力でのモックを用意するが、今後MongoDBに切り替えられるようRepositoryインターフェースとRepositiryImplクラスを分けておく。

SurveyAnswerRepository

public interface SurveyAnswerRepository {

    public List<SurveyAnswerEntity> selectByUserid(String userid);
    public boolean insertEntity(SurveyAnswerEntity entity);
}

Serviceの箇所で説明したが、selectByUseridメソッドはDBからuseridでselectした結果を返却し、insertEntityメソッドはSurveyAnswerEntityをDBにinsertする。

ファイル入出力実装のモックは省略する。

Entityクラスは以下のようになる。

SurveyAnswerEntity

public class SurveyAnswerEntity {

    private String userid;
    private String date;
    private String survey1answer;
    private String survey2answer;

    //getter/setterは省略
}

Formとほぼ同じだが、Formにはなかった回答日時を表すdateが追加されている。

設定ファイル

context設定ファイルにServiceとRepositoryのbean定義を追加している。

servlet-context.xml

 <beans:bean id="surveyService"
        class="jp.tkmtys.survey.service.SurveyService" />
    <beans:bean id="surveyAnswerRepository"
        class="jp.tkmtys.survey.repository.SurveyAnswerRepoFileImpl" />

一旦ここまででDBはモックだがアプリとして動かせる状態にできた。次回はDBモックの部分をMongoDBで実装していく。

githubに公開したらリンクを追加予定。

sponsor