SIerっぽいWEBアプリをAngularJS+JavaEEで組んでみたですが
今回はよくあるマスタメンテナンス画面について。
よくある内容を目指していて、ざっくり以下の仕様です。
- 画面上部が検索条件、モーダルダイアログのコード参照ボタンがある。
- 画面下部に検索結果の明細が表示される、明細行中のボタンで次画面遷移。
こんな感じの画面ですね。
ソースはこちらです。
https://github.com/ko-aoki/angularJS_practice/tree/forQiita
設定(的なもの)
- main.js
$routeProviderで、マスタメンテナンス画面で使用するコントローラと、URLに対応するテンプレート情報を記述しています。
(RequireJSを使用しています。)
※設定としているのですが、実際は初期化処理なんかも$routeProvider
で記述できます。
require.config({
paths: {
//略
},
shim: {
//略
}
});
require([
'angular',
'app',
'domReady',
//ファイルを追加したらここに追記
//略
'controllers/mntMstUserCtrl',
'controllers/mntMstUserRegCtrl',
'controllers/mntMstUserRegConfirmCtrl',
'controllers/codeDeptCtrl',
'directives/csngPage',
'directives/csngCodeDept'
],
function (angular, app, domReady) {
'use strict';
app.config(['$routeProvider',
function($routeProvider) {
//ルートの定義。画面を追加したらここで追記
//略
//ユーザマスタメンテナンス画面
$routeProvider.when('/mntMstUser', {templateUrl: 'partials/mntMstUser.html', controller: 'MntMstUserCtrl'});
//ユーザマスタメンテナンス画面(登録)
$routeProvider.when('/mntMstUserReg', {templateUrl: 'partials/mntMstUserReg.html',
controller: 'MntMstUserRegCtrl'
});
//ユーザマスタメンテナンス画面(修正)
$routeProvider.when('/mntMstUserReg/:mstUserId', {templateUrl: 'partials/mntMstUserReg.html',
controller: 'MntMstUserRegCtrl'
});
//ユーザマスタメンテナンス画面(確認)
$routeProvider.when('/mntMstUserRegConfirm', {templateUrl: 'partials/mntMstUserRegConfirm.html',
controller: 'MntMstUserRegConfirmCtrl'
});
//部門コード参照画面
$routeProvider.when('/codeDept', {templateUrl: 'partials/codeDept.html',
controller: 'CodeDeptCtrl'
});
$routeProvider.otherwise({redirectTo: '/login'});
}
]);
domReady(function() {
angular.bootstrap(document, ['MyApp']);
$('html').addClass('ng-app: MyApp');
});
}
);
テンプレートファイル
- mntMstUser.html
検索条件のng-modelは"cond"と1階層、情報を付与しています。これにより、明細とのデータを区別しています。
明細はng-repeatで。JSTLのc:forEachで可能なことはいける感じ。
明細内のボタンで、ng-click="delete(rec)"
なんて書き方で明細情報を渡せるのは驚きました。
※登録、削除は未実装です。。
<link href="css/common-app.css" rel="stylesheet" type="text/css" />
<link href="css/mntMUser.css" rel="stylesheet" type="text/css" />
<h1 style="font-size:16px;">ユーザマスタメンテ</h1>
<div id="app" style="z-index: 1">
<div csng-code-dept></div>
<div id="message">
<ul>
<li ng-repeat="message in messages">
<p>{{message}}</p>
</li>
</ul>
</div>
<div id="header">
<div id="condition" class="content-wrap">
<div id="finderDiv">
<table border="1">
<tr>
<th>ユーザ名
</th>
<td>
<input type="text" ng-model="cond.userNm"/>
</td>
</tr>
<tr>
<th>組織1
</th>
<td>
<input type="text" ng-model="cond.deptId1"/>
<input type="button" ng-click="findDept(1)" value="部門検索"/>
<span id="lblDeptNm1">{{$scope.cond.deptNm1}}</span>
</td>
</tr>
<!-- 略 -->
<tr>
<th>ロール
</th>
<td>
<select id="roleId"
ng-model="cond.roleId" ng-options="role.roleId as role.roleNm for role in cond.roles">
</select>
</td>
</tr>
</table>
<button type="button" ng-click="find()" id="find">検索</button>
<button type="button" ng-click="register()" id="register">新規登録</button>
</div>
</div>
</div>
<!-- 略 -->
<div id="recordsDiv">
<table id="recordsTable" border="1" >
<tbody >
<tr ng-repeat="rec in recs">
<td class="recUserId">
<span>{{rec.userId}}</span>
</td>
<td class="recUserNmM">
<span>{{rec.usernmSei}}{{rec.usernmMei}}</span>
</td>
<!-- 略 -->
<tr>
<td class="recCmd">
<button class="modify" ng-click="modify(rec)" >変更</button>
<button class="delete" ng-click="delete(rec)" >削除</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
コントローラファイル
- mntMstUserCtrl.js
最初の2行はRequireJSの定義で、依存関係を明確化しています。
1行目でmain,jsで定義したファイル、2行目でその変数化。
$scope.recs = [];
以下、$scope.find
までベタで初期化処理書いてますが、多分もっといい書き方あるんでしょうね。。
初期化処理でやりたいことは、コンボボックスの内容取得と、遷移後画面から戻ってきた場合の値復帰です。
コメント//戻る
以下でサービスを利用して、自画面の値をすでに持っていたら使用する、といったロジックになります。
コントローラ間のデータ受け渡しに自前のサービスdtoSrv
を使用しています。
そもそも検索-選択-入力-確認-登録といった1ユースケースにつき、1コントローラにしたほうがいいのかもしれません。
「検索」ボタン押下の$scope.find
ではJAX-RSリソースにアクセスして明細行を取得しています。
明細行内の「変更」ボタン押下処理$scope.modify
では画面遷移前の画面情報の保持と、画面遷移処理です。
検索条件内の「部門検索」ボタン押下処理の$scope.findDept
はjQueryでコード検索のdivにクラスの付け替えをして、コード検索画面の表示をしています。z-index指定のdivでモーダルダイアログとしたのですが、本当に別ウィンドウを起動したい場合は、別途考慮が必要ですね。。
$scope.$on('CodeDeptOK', function (event, rec)
はコード参照画面の「OK」押下処理で、コード参照画面から自画面への値渡し処理です。
その3で紹介しているカスタムディレクティブのコントローラからイベント通知されています。
define(['jquery', 'controllers', 'angularResource', 'services/dtoSrv'],
function($, controllers) {
controllers.controller('MntMstUserCtrl', ['$scope', '$http', '$location', '$resource', 'dtoSrv',
function ($scope, $http, $location, $resource, dtoSrv) {
$scope.recs = [];
$scope.cond = {};
var resource = $resource('webresources/mntMstUser/:userNm,:deptId1,:deptId2,:roleId',
{
'userNm': '@userNm',
'deptId1': '@deptId1',
'deptId2': '@deptId2',
'roleId': '@roleId'
}
);
resource.get({}, function(data) {
$scope.cond.roles = data.roles;
});
//戻る
if (dtoSrv.getData('MntMstUser') != null) {
$scope.recs = dtoSrv.getData('MntMstUser').recs;
$scope.cond = dtoSrv.getData('MntMstUser').cond;
}
//検索
$scope.find = function find() {
resource.get(
{
'userNm': $scope.cond.userNm,
'deptId1': $scope.cond.deptId1,
'deptId2': $scope.cond.deptId2,
'roleId': $scope.cond.roleId
}, function(data) {
if (data.result === "error") {
$scope.messages = data.messages;
} else {
$scope.recs = data.recs;
$scope.cond.roles = data.roles;
}
}
);
};
//登録
$scope.register = function register(rec) {
$location.path("mntMstUserReg");
};
//修正
$scope.modify = function modify(rec) {
dtoSrv.setData('MntMstUser', {'cond':$scope.cond, 'recs':$scope.recs})
$location.path("mntMstUserReg/" + rec.mstUserId);
};
//部門検索
$scope.findDept = function findDept(kind) {
//TODO jQuery排除するか
$("#codeDept").removeClass("hide");
$("#codeDept").addClass("show");
};
//部門検索「OK」押下
$scope.$on('CodeDeptOK', function (event, rec) {
$scope.cond.deptId1 = rec.pDeptId;
$scope.cond.deptNm1 = rec.pDeptNm;
$scope.cond.deptId2 = rec.deptId;
$scope.cond.deptNm2 = rec.deptNm;
$("#codeDept").removeClass("show");
$("#codeDept").addClass("hide");
});
}]
)
}
);
サービスファイル
- dtoSrv.js
コントローラ間の値受け渡し用のサービス。
コントローラでも述べたのですが、そもそもコントローラ作成の粒度を画面単位にしているのがどうなのかという疑問はあります。sastrutsなんかとは一致するので移植しやすいかもですが。
内容はJavaでいうところのPOJOですね。
define(['services'],
function(services) {
services.factory('dtoSrv', [
function() {
var data = [];
return {
getData: function (key) {
return data[key];
},
setData: function (key, value) {
data[key] = value;
}
};
}]);
});
#JAX-RSリソースファイル
- MntMstUserResource.java
ここからサーバサイド=Javaソースです。
リクエストを受け付けてビジネスロジック(サービス)を呼び出しています。
@Produces
指定で、呼び出し元のJavaScriptファイル(mntMstUserCtrl.js
)にJSONを返しています。
@Path("mntMstUser")
public class MntMstUserResource {
@Inject
private MntMstUserService mntMstUserService;
public MntMstUserResource() {
}
/**
* ユーザの検索処理を行います.
* @param userNm
* @param deptId1
* @param deptId2
* @param roleId
* @return
*/
@GET
@Path("{userNm: .*},{deptId1: .*},{deptId2: .*},{roleId: .*}")
@Produces("application/json")
public MntMstUserForm getUsers(
@PathParam("userNm") String userNm,
@PathParam("deptId1") String deptId1,
@PathParam("deptId2") String deptId2,
@PathParam("roleId") String roleId) {
MntMstUserForm form = mntMstUserService.getUsers(userNm, deptId1, deptId2, roleId);
return form;
}
//略
- MntMstUserServiceImple.java
リポジトリを使用してデータ取得し、formに値を設定しています。
public class MntMstUserServiceImple implements MntMstUserService{
@Inject
private MstUserRepository mstUserRep;
@Inject
private MstRoleRepository mstRoleRep;
public MntMstUserServiceImple() {
}
/**
* ユーザ情報を取得します.
* @param userNm
* @param deptId1
* @param deptId2
* @param roleId
* @return
*/
public MntMstUserForm getUsers(String userNm, String deptId1, String deptId2, String roleId) {
MntMstUserForm form = new MntMstUserForm();
form.setRecs(mstUserRep.find(userNm, deptId1, deptId2, roleId));
form.setRoles(mstRoleRep.findAll());
form.setResult("ok");
return form;
}
- MstUserRepositoryImpl.java
検索条件からJPQLのクエリ文字列を作成しています。
検索結果は他表と結合しているのですが、それはEntityクラス間の関係で表現しています。
改めて見返して、JPQL面倒ですね。。
勉強のためにnativeQuery、JPQL、Criteriaを試しましたが、
できるだけCriteriaを使用したい感じ。
public class MstUserRepositoryImpl implements MstUserRepository{
@PersistenceContext(name = "common-app-javaee7PU")
private EntityManager em;
//略
@Override
public List<MstUser> find(String userNm, String deptId1, String deptId2, String roleId) {
String sql = "SELECT u FROM MstUser as u";
ArrayList<String> wheres = new ArrayList<String>();
if (!userNm.isEmpty()) {
wheres.add(" CONCAT(u.usernmSei,u.usernmMei) LIKE :userNm ") ;
}
if (!deptId1.isEmpty()) {
wheres.add(" u.mstDeptId.pDeptId = :deptId1");
}
if (!deptId2.isEmpty()) {
wheres.add(" u.mstDeptId.deptId = :deptId2");
}
if (!roleId.isEmpty()) {
wheres.add(" u.roleId.roleId = :roleId");
}
if (!wheres.isEmpty()) {
String where= " WHERE " + StringUtils.join(wheres, " AND ");
sql += where;
}
Query query = em.createQuery(sql, MstUser.class);
if (!userNm.isEmpty()) {
query.setParameter("userNm", "%" + userNm + "%");
}
if (!deptId1.isEmpty()) {
query.setParameter("deptId1", deptId1);
}
if (!deptId1.isEmpty()) {
query.setParameter("deptId2", deptId2);
}
if (!roleId.isEmpty()) {
query.setParameter("roleId", roleId);
}
return query.getResultList();
}
}
//略
- MstUser.java
ユーザマスタのエンティティクラス。
ロールマスタ、部門マスタに外部キー指定した状態で、NetBeansの自動生成を行うとそれぞれのエンティティクラスがメンバになります。
MstUserのエンティティを取得すると、紐づくロールマスタ、部門マスタのエンティティも取得されることになります。
ここで、現場で外部キーなんて使ってないよ!結合するには必ずリレーション作成しないといけないのかと思ったのですが、そうでもなく。
public class MstUser implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Basic(optional = false)
@NotNull
@Column(name = "MST_USER_ID")
private Long mstUserId;
@Basic(optional = false)
@NotNull
@Size(min = 1, max = 10)
@Column(name = "USER_ID")
//略
@JoinColumn(name = "ROLE_ID", referencedColumnName = "ROLE_ID")
@ManyToOne
private MstRole roleId;
@JoinColumn(name = "MST_DEPT_ID", referencedColumnName = "MST_DEPT_ID")
@ManyToOne
private MstDept mstDeptId;
//略
}
- SampleRepository.java
Criteraの場合、こんな感じで内部結合できるみたい。
MUser.deptId = MDept.deptIdの関係です。
ここは、もうちょっと突っ込んで調べたいですな。
(2014/09/04 やや関連の別記事を書きました)
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery cq = cb.createQuery();
Root userRoot = cq.from(MUser.class);
Root deptRoot = cq.from(MDept.class);
cq.multiselect(userRoot, deptRoot);
cq.where(cb.equal(userRoot.get("deptId"), deptRoot.get("mDeptPK").get("deptId")));
Query query = em.createQuery(cq);
List<Object[]> result = query.getResultList();
#まとめ
まずは動かしたというレベルですが、フロントの要求が複雑になるほど、AngularJS使用のメリットは活かせそうです。
むしろ、JPAで実装するにあたって日本語情報が少ないことに手間取りました。
AngularJSは初めから諦めて英語圏の情報収集するのですが、JPA2.0ってJavaEE6のタイミングだから2009年リリースなのに、なんでここまでと。
それで、軽く調べるとJPAは表結合したかったらリレーションの作成が前提になっているように見えてしまう。
正直、自分が関わったプロジェクトで外部キーを使用していたのは1割に満たないので、移行するとなったら大変だ。。と思いながらコーディングしてました。
最後に記述したように、Entity間にリレーション持たせなくても結合する術はあるようなので、そっちを採用することになりそうです。
つづきます。次はコード参照画面/ページ検索。