React事始め
以前vue-cliで作成したプロジェクトをreact化してみたので、それを使って基本的なことはできるようにしておきたいと思います。
最初のコンポーネントを追加する
まずは表示だけしてみる
vue-cliで作成したプロジェクトをreact化した続きから進めていきたいと思います。まず最初のコンポーネントを追加してみたいと思います。 ‘src/components/first-component.js'を追加して以下の内容にしてください。
import React, { Component } from 'react'; export default class FirstComponent extends Component { constructor(props) { super(props) } render() { return ( <div> <h1>Hello, React!</h1> </div> ); } }
特に値を受け取って表示するわけでもない上記コンポーネントをまずは表示できるようにしてみたいと思います。'main.js'を以下の内容にして'npm run dev'で動作が確認できます。
import React from 'react' import { render } from 'react-dom' import FirstComponent from './components/first-component' render( <FirstComponent /> , document.getElementById('app') );
stateの値を表示してみる
今度はstateの値を表示できるようにしてみたいと思います。Reactではstateに似た概念としてpropsがありますがこれは親から受け取るプロパティはpropsにセットされ自分自身のコンポーネントで使う値はstateにセットするというもので、コンポーネント初期化時に呼び出されるconstructor(props)で初期化を行います。
import React, { Component } from 'react'; export default class FirstComponent extends Component { constructor(props) { super(props); this.state = { copyText: '', }; } textFromInput(e) { this.setState({ copyText: e.target.value }); } render() { return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" hintText="to text" value = { this.state.copyText } readOnly="readonly" /><br /> </div> ); } }
propsで引き継いだ値を子コンポーネントで利用する
まずpropTypesの宣言周りで必要になるのでbabelにpresetプラグインを有効化します。現在0から1,2,3と機能が異なるpresetが用意されていて具体的にどれを使えばよくわかっていないのですが、vue-cliでプロジェクトを作成した時に入っていたpreset-2を指定しておきたいと思います。.babelrcを以下のように修正します。
{ "presets": [ "es2015", "react", "stage-2"] }
それから、親からの値を受け取る側のコンポーネントを準備します。以下の"src/components/child-component.js"を作成します。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class ChildComponent extends Component { static propTypes = { copyText: PropTypes.string.isRequired } static defaultProps = { copyText: 'init val' } constructor(props) { super(props); } render() { const { copyText } = this.props; return ( <div> <label>{ copyText }</label> </div> ); } }
次に値を受け渡す側"src/components/first-componet.js"を以下のように修正します。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import ChildComponent from './child-component'; export default class FirstComponent extends Component { static propTypes = { copyText: PropTypes.string.isRequired } static defaultProps = { copyText: 'copy text' } constructor(props) { super(props); this.state = { copyText: 'copy text', }; } textFromInput(e) { this.setState({ copyText: e.target.value }); } render() { const { copyText } = this.state; return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" placeholder="to text" value = { this.state.copyText } readOnly="readonly" /><br /> <ChildComponent copyText={ copyText }/> </div> ); } }
renderの呼び出しで新しく作成したChildComponentコンポーネントのプロパティ'copyText'にテキスト入力した値が入るようになっています。ChildComponenは受け取ったプロパティをラベルでそのまま出すようにしておりまして動かしてみるとそれが確認できるかと思います。
ちなみに今回新しく追加したChildComponentは親コンポーネントのrenderメソッドで直接タグを記入していましたが、以下のようなコンポーネントを返すメソッドを用意しておいてrender内で呼び出すようにすることでも同様の動きになります。
renderItem(copyText){ return ( <ChildComponent copyText={ copyText }/> ) }
呼び出しサンプル render() { const { copyText } = this.state; return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" placeholder="to text" value = { this.state.copyText } readOnly="readonly" /><br /> {this.renderItem(copyText)} </div> ); }
表示する内容を条件によって切り替える必要があるけどコンポーネントを分けるまでもない場合で、renderメソッド内をごちゃごちゃさせたくないという場合があるのでしたらこの方が良い場合もあるかもしれませんが、ルールを決めておかないと逆にわかりづらくなりそうなので注意が必要です。この辺りが柔軟そうなのは助かりそうではあります。
react-eduxを利用する
Reduxでコンポーネンの値を変更してみる
先ほどは親コンポーネントのプロパティを直接子コンポーネントに渡して連携していました。今度はReduxのフレームワークを使用してコンポーネント間の連携を行っていきたいと思いまして、reactであればreact-reduxというモジュールが公式から出ているのでこちらを利用したいと思います。
まずReduxについての簡単な概要ですが、single-page-applicationの誕生により以前よりも多くの状態を管理する必要が出てきていましてFluxというフレームワーク
ではデータの流れを一方通行にしてしまうことで、例えばコンポーネント間でのデータのやりとりでそれぞれのコンポーネントが実データを更新する処理を行っているのであれば管理しづらくなるので
それを打開するため状態を更新する場合は共通のアクションを呼び出すなどしてデータを一方通行にするといった方法が出てきています。ReduxというのはFluexの実装の一つという位置づけのようなもので
厳密なFluxよりかはReactから扱いやすいように変更が加えられたものとなっています。
npm install –save react-redux npm install –save redux-thunk npm install babel-plugin-transform-decorators-legacy –save-dev
react-redux, redux-thunkについてはreact-redux関連のモジュールと想像がつくと思います。babel-plugin-transform-decorators-legacyについてはreduxが生成するstateをコンポーネントに関連付けるのに使用する@connetアノテーションで必要になります。
まず.babelrcを修正してbabel-plugin-transform-decorators-legacyを有効にするようにしたいと思います。
{ "presets": [ "es2015", "react", "stage-2"], "plugins": ["transform-decorators-legacy"] }
次に"src/define/action/sample-action-define.js"に今回追加するアクションの定数定義を追加します。
export const CHANGE_TEXT = 'CHANGE_TEXT'
“src/reducers/sample-reducer.js"にstate更新に使用するreducerを追加します。これはコンポーネント側がアクションを呼び出して来た場合にここでstateの更新を行います。
import { CHANGE_TEXT } from '../define/action/sample-action-define' const initialState = { text : 'init text' }; export default function sampleReducer(state = initialState, action) { switch (action.type) { case CHANGE_TEXT: return Object.assign({}, state, { text: action.text}) default: return state } }
それから"src/reducers/index.js"にreducerをマージするためのメソッドを追加します。今回使用するReducerは一つだけなのであまり恩恵を感じないですが、複数のReducerが必要になる場合はこういったようにマージするメソッドがあった方が良さそうです。
import { combineReducers } from 'redux' import sampleReducer from './sample-reducer' const rootReducer = combineReducers({ sampleReducer }) export default rootReducer
Reducerを呼び出すアクションを"src/actions/sample-action.js"に追加します。アクション自体はコンポーネントがディスパッチという関数を使うことで呼び出すことができます。
import * as types from '../define/action/sample-action-define' export function change_text(text) { return { type: types.CHANGE_TEXT, text: text } }
stateを管理する大元であるstoreを"src/store/store-config.js"に作成します。applyMiddleware後にfinalCreateStoreやっているところでstoreを生成しています。applyMiddlewareについてはreactにおけるミドルウェアの機能を使う時に必要になるもので例えばログ出力や他サーバにリクエストを投げる場合などに利用されます。今回はミドルウェアを使用することもないのでapplyMiddlewareを使わずにcreateStoreだけでも良いはずですが、後からミドルウェアを使うことを想定し先にこの書き方にしておきます。module.hotの判定式の内部ではreduxを使う場合にホットリロードを行うための設定となっておりまして、これがない場合は開発車モードで起動中にファイルを保存するたびにstateが初期化されるのを防ぐためにあった方が良さそうなものです。
import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import rootReducer from '../reducers' export default function StoreConfig(preloadedState) { const finalCreateStore = applyMiddleware(thunk)(createStore); const store = finalCreateStore(rootReducer, preloadedState); if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { const nextReducer = require('../reducers').default store.replaceReducer(nextReducer) }) } return store }
Reactにreduxのstateを私て扱えるようにする。"src/main.js"が以下になるようにします。ここではreact-reduxモジュールのProviderコンポーネントにstoreを渡しています。
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import FirstComponent from './components/first-component' import StoreConfig from './store/store-config' const store = StoreConfig() render( <Provider store={store}> <FirstComponent /> </Provider > , document.getElementById('app') );
それではReduxのstateを利用する側である"src/components/first-component.js"を以下のように修正します。mapStateToPropsにはstateの値が、mapDispatchToPropsにはディスパッチするために必要となるメソッドが格納されています。@connect(mapStateToProps, mapDispatchToProps)のアノテーションでreduxのstoreを渡しています。propTypesのプロパティと、textに変更があった時に呼び出すメソッドの情報が入っていてRenderメソッドではこれを使用して描画を行っています。またテキスト入力を行った際に呼び出されるtextFromInput内で"this.props.change_text(e.target.value)“でアクションを呼び出した上でディスパッチしてReducerにより新しいstateが発行されます。
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import ChildComponent from './child-component'; import * as SampleAction from '../actions/sample-action'; function mapStateToProps(state) { const { text } = state.sampleReducer return { text: text }; } function mapDispatchToProps(dispatch) { return bindActionCreators( Object.assign({}, SampleAction), dispatch); } @connect(mapStateToProps, mapDispatchToProps) export default class FirstComponent extends Component { static propTypes = { change_text: PropTypes.func.isRequired, text: PropTypes.string } constructor(props) { super(props); } textFromInput(e) { this.props.change_text(e.target.value) } renderItem(text){ return ( <ChildComponent copyText={ text }/> ) } render() { const { text } = this.props; return ( <div> <h1>Hello, React!</h1> <input name="a" type="text" placeholder="from text" onChange = { this.textFromInput.bind(this) } /><br /> <input name="a" type="text" placeholder="to text" value = { text } readOnly="readonly" /><br /> <ChildComponent copyText={ text }/> </div> ); } }
これで動かしてみるとeslintにno-undefとか怒られるはずなので、.eslintrc.jsを以下のように修正し再度"npm run dev"で動画確認できるはずです。
'rules': { // allow paren-less arrow functions 'arrow-parens': 0, // allow async-await 'generator-star-spacing': 0, 'react/jsx-uses-vars': 1, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 'no-undef': 0, 'no-console': 0, }
子のコンンポーネントからアクションを呼び出してみる
子のコンポーネントにも直接storeで管理しているstateを関連付けて利用することができる。以下の修正を加えることでテキストのstateを空白にするアクションを呼び出すようにすることができる。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import * as SampleAction from '../actions/sample-action'; function mapStateToProps() { return {} } function mapDispatchToProps(dispatch) { return bindActionCreators( Object.assign({}, SampleAction), dispatch); } @connect(mapStateToProps, mapDispatchToProps) export default class ChildComponent extends Component { static propTypes = { change_text: PropTypes.func.isRequired, copyText: PropTypes.string.isRequired } static defaultProps = { copyText: 'init val' } constructor(props) { super(props); } clearText() { this.props.change_text("") } render() { const { copyText } = this.props; return ( <div> <label>{ copyText }</label><br /> <button onClick = { this.clearText.bind(this) }>クリア</button> </div> ); } }
今回は直接storeの値を関連付けるようにしているが、親コンポーネント側で呼び出すアクションなりを変更できるようにしたいのであればpropsとして親のコンポーネントから子のコンポーネントに直接渡せるようにしたら良さそうに思います。
表示について
リストを表示してみる
リストのデータを表示する場合は以下のようになります。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class ListComponent extends Component { static propTypes = { listData: PropTypes.array } static defaultProps = { listData: [{} ] } constructor(props) { super(props); this.state = { listData: [ {id: 1, name: "山田一郎"}, {id: 2, name: "田中二郎"}, {id: 3, name: "佐藤三郎"} ] } } render() { const listData = this.state.listData return ( <ul className="user-list"> {listData.map((user, i) => <div key={i}><li>{ user.name } </li></div> )} </ul> ); } }
renderの部分は以下のように書き換えることもできる
userRender(userList){ return ( <ul className="user-list"> {userList.map((user, i) => <div key={i}><li>{ user.name } </li></div> )} </ul> ) } render() { const listData = this.state.listData return ( <div> { this.userRender(listData) } </div> );
または以下のような書き方もできる
userRender(userList){ const userListView = [] userList.map((user, i) => userListView.push(<div key={i}><li>{ user.name } </li></div>) ) return ( <ul className="user-list"> { userListView } </ul> ) } render() { const listData = this.state.listData return ( <div> { this.userRender(listData) } </div> ); }
webpackのcss-loaderを使ってみる
react,webpackでの環境でスタイルを適用する方法は複数あるのですが、まずはhtmlのheadタグの中にstyleを書き込んですべてのコンポーネントが適用対象にするのがなじみ深いと思いますのでそれから試してみたいと思います。webpackのcss-loaderを使ってbootstrapを読み込むようにしたいと思います。適用するのは簡単でbootstrapからファイル一式をダウンロードしてきて'src/assets/bootstrap'にダウンロードしたすべてのファイルを写した上で"src/main.js"に以下のimportを追加するだけになっております。
import './assets/bootstrap/css/bootstrap.min.css'
これで動きを見てみるとheadタグの中にstyleが書き込まれているのが分かります。今回はcssで試しましたがsassやstylusもwebpack側で読み込んで使うことができます(別途loader用のプラグインインストールが必要になるかもしれないです)。
CSS-in-JSを試してみる
Reactではcssをstyle属性として扱っていましてCSS-in-JSはCSSの記法で書いたスクリプトをを直接style属性として扱えるようにするものとなっております。例えば"src/style/sample.css.js"が以下の内容だったとする場合
export default { ul: { listStyle: 'none', marginTop: '20px', padding: '0px', fontSize: '18px', }, span: { paddingLeft: '20px', } }
コンポーネント側では以下のようにインポートしてstyle属性を設定することができます。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import styles from '../style/sample.css.js'; export default class ListComponent extends Component { static propTypes = { listData: PropTypes.array } static defaultProps = { listData: [{} ] } constructor(props) { super(props); this.state = { listData: [ {id: 1, name: "山田一郎"}, {id: 2, name: "田中二郎"}, {id: 3, name: "佐藤三郎"} ] } } render() { const listData = this.state.listData return ( <ul style={styles.ul}> {listData.map((user, i) => <li key={i}><span>{ user.id }</span><span style={styles.span}>{ user.name } </span></li> )} </ul> ); } }
またjsのプロパティとして扱っているだけなのでcss用にファイルを分ける必要もなく、以下のようにスタイルを設定することもできます。
import React, { Component } from 'react'; import PropTypes from 'prop-types'; export default class ListComponent extends Component { static propTypes = { listData: PropTypes.array } static defaultProps = { listData: [{} ] } constructor(props) { super(props); this.state = { listData: [ {id: 1, name: "山田一郎"}, {id: 2, name: "田中二郎"}, {id: 3, name: "佐藤三郎"} ], style: { ul: { listStyle: 'none', marginTop: '20px', padding: '0px', fontSize: '18px', }, span: { paddingLeft: '20px', } } } } render() { const listData = this.state.listData const style = this.state.style return ( <ul style={style.ul}> {listData.map((user, i) => <li key={i}><span>{ user.id }</span><span style={style.span}>{ user.name } </span></li> )} </ul> ); } }
デザイナーではないので良くわからないのですが、基本的にはcss-loaderの機能だけでスタイルを調整して動的に変更したい場合とかがあったらCSS-in-JSを使うとかの方がシンプルで良さそうな気がしました。
routerを使ってみる
react-routerのバージョン変更による影響が大きいので実施しるタイミングによって設定が結構変わってきそうです。自分が試した時はv4がリリースされていたので以下のコマンドでモジュールをインストールしたところv4.1.1が入りました。
npm install –save react-router-dom
“src/main.js"を以下のように修正することで利用できます。
import React from 'react' import { render } from 'react-dom' import { Provider } from 'react-redux' import StoreConfig from './store/store-config' import './assets/bootstrap/css/bootstrap.min.css' import './assets/css/main.css' import { BrowserRouter, Route, Switch } from 'react-router-dom'; import FirstConponent from './components/first-component'; import ListComponent from './components/list-component'; const store = StoreConfig() render( <Provider store={store}> <BrowserRouter> <Switch> <Route exact path="/" component={FirstConponent} /> <Route exact path="/first" component={FirstConponent} /> <Route exact path="/list" component={ListComponent} /> </Switch> </BrowserRouter> </Provider > , document.getElementById('app') );
ここでは"http://localhost:8888/“及び"http://localhost:8888/first"でアクセスした時にFirstConmponentを表示し"http://localhost:8888/list"でアクセスした時にListConponentを表示する動きになります。urlからパラメータを受け取ったりテスト方法であったりは公式の方から確認いただければと思います。
ミドルウェアを使ってみる
それではReactを使う上で結構肝になりそうなミドルウェアを試してみたいと思います。まず簡単なログ出力を行ってみます。
ログを出力する
アクションが実行されるタイミングでミドルウェア側でconsole出力できるようにしたいと思います。まず"src/midleware/logger.js"を以下の内容で作成します。
const logger = function actionDebugMiddleware() { return next => action => { console.info(action.type, action); next(action); }; }; export default logger
次に"src/store/store-config.js"側でミドルウェアを使うように修正を加えます。
import { createStore, applyMiddleware } from 'redux' import thunk from 'redux-thunk' import logger from '../midleware/logger' import rootReducer from '../reducers' export default function StoreConfig(preloadedState) { const finalCreateStore = applyMiddleware(thunk, logger )(createStore); const store = finalCreateStore(rootReducer, preloadedState); if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { const nextReducer = require('../reducers').default store.replaceReducer(nextReducer) }) } return store }
これで動きを確認してみるとアクションがディスパッチして新しいstateを出力する直前でログ出力のミドルウェアがコンソール出力を行っているのが確認できます。ミドルウェア側ではnext(action)で次のアクションを呼び出すようにchainしているのですがここでactionの内容を修正したりすることで限定的ではありますがaopのように横断的な処理が行えるようです。
apiを呼び出してみる
次にミドルウェアを利用してapiを呼び出せるようにしてみましょう。今回はwebpackの静的コンテンツとしてjsonを作成しそれに対してgetのリクエストを投げて表示に反映したいと思います。まず以下のダミーデータを"static/dummy.json"のファイル名で追加します。
{"user_list":[ { "id":1, "name":"藤岡弘" }, { "id":2, "name":"佐々木剛" }, { "id":3, "name":"宮内洋" }, { "id":4, "name":"速水亮" }, { "id":5, "name":"岡崎徹" } ]}
次にapiを呼び出すミドルウェアを"src/midleware/api_caller.js"に追加します。今回はjqueryを使ってリクエストを投げます。
import {CALL_API} from '../define/action/sample-action-define' import $ from 'jquery' const api_caller = function actionApiCall() { function remoteService(next, remote){ $.ajax({ url: remote.url, dataType: remote.dataType, type: remote.type, data: remote.data, cache: false, scriptCharset: 'utf-8', success: data => { const new_action = remote.response_action(data) if(new_action.type == CALL_API){ remoteService(next, new_action.remote) }else{ next(new_action) } }, error: data => { console.info(data) } }); } return next => action => { if(action.type == CALL_API){ remoteService(next, action.remote) }else{ next(action) } }; }; export default api_caller
jqueryを使えるようにするため以下のコマンドを実行しておいてください。
npm install –save jquery
ここではアクションをディスパッチしてきた時のtypeがCALL_APIであったらajaxでリクエストを投げるようにしています。レスポンスが帰ってきた時に何をするかはディスパッチに受け取ったactionのremote属性に設定されているremote_responseを呼び出すようにしています。ここで使っている定数は"src/define/action/sample-action-define"で以下のように定義しています。
export const CHANGE_TEXT = 'CHANGE_TEXT' export const CALL_API = 'CALL_API' export const INIT_USER_LIST = 'INIT_USER_LIST'
次にミドルウェアとして呼び出せるように"src/store/store-config.js"を以下のように修正します。
const finalCreateStore = applyMiddleware(thunk, logger )(createStore); ↓ const finalCreateStore = applyMiddleware(thunk, logger, api_caller )(createStore);
また、今回api呼び出しで取得するユーザリストの情報はreduxのstateで管理するので以下のリデューサーを"src/reducers/userlist-reducer.js"に追加します。
import { INIT_USER_LIST } from '../define/action/sample-action-define'; const initialState = { user_list :[{id: '0', name: 'john doh'}] }; export default function userListReducer(state = initialState, action) { switch (action.type) { case INIT_USER_LIST: return Object.assign({}, state, action.data) default: return state } }
それから作成したreducerをcombineReducersで既存のものとマージして使えるようにします。
import { combineReducers } from 'redux' import sampleReducer from './sample-reducer' import userListReducer from './userlist-reducer' const rootReducer = combineReducers({ sampleReducer, userListReducer }) export default rootReducer
api呼び出しとapi受け取り後に新しいstateをディスパッチするためのアクションを"src/action/api/sample-api-action.js"に追加します。
import * as types from '../../define/action/sample-action-define' export function callApi(remote) { return { type: types.CALL_API, remote:remote} } export function user_list_init() { const response_action = function(data){ return {type: types.INIT_USER_LIST, data: data} } const data = {} return createRequestData( process.env.REQUEST_URL.USER_LIST_INIT, 'JSON', 'GET', data, response_action); } function createRequestData(url, dataType, type, data, response_action){ return { url: url, dataType:dataType, type:type, data: data, response_action: response_action, contentType: 'application/x-www-form-urlencoded; charset=UTF-8' } }
ここではリクエストを呼び出すのに使用するURLをwebpackの環境変数から取得しているのですが、apiのURLを環境変数から取得できるようにするため、"config/request.url.dev.json"を以下の内容で作成します。
{ "USER_LIST_INIT": "/static/dummy.json" }
これは開発時に使うapiのURLになりますので、実運用用のものは"config/request.url.dev.json"に記入しておいてください。それから"config/dev.env.js"でprodEnvにマージさせておいてください。prod.env.jsonも同様です。
var merge = require('webpack-merge') var prodEnv = require('./prod.env') var devUrl = require('./request.url.dev.json') module.exports = merge(prodEnv, { NODE_ENV: '"development"', REQUEST_URL: JSON.stringify(devUrl) })
ここのprodEnvは"build/webpack.dev.conf.js"で以下のように環境変数に追加しているので確認できます。
new webpack.DefinePlugin({ 'process.env': config.dev.env }),
これでURLを利用するときはprocess.env.REQUEST_URL.xxxxといった感じになるのがわかります。
あとapiを呼び出して利用する側のコンポーネントにも修正が必要なので"src/components/list-component.js"を以下のように修正します。apiの呼び出しはcomponentWillMountのタイミングで行っています。
import React, { Component } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import styles from '../style/sample.css.js'; import * as SampleApiAction from '../actions/api/sample-api-action' function mapStateToProps(state) { const { user_list } = state.userListReducer return { user_list: user_list } } function mapDispatchToProps(dispatch) { return bindActionCreators( Object.assign({}, SampleApiAction), dispatch); } @connect(mapStateToProps, mapDispatchToProps) export default class ListComponent extends Component { static propTypes = { user_list: PropTypes.array, callApi: PropTypes.func.isRequired, } static defaultProps = { user_list: [{}] } constructor(props) { super(props); this.state = {} } componentWillMount() { this.props.callApi(SampleApiAction.user_list_init()); } render() { const user_list = this.props.user_list console.info(user_list) return ( <ul style={styles.ul}> {user_list.map((user, i) => <li key={i}><span>{ user.id }</span><span style={styles.span}>{ user.name } </span></li> )} </ul> ); } }
これで"npm run dev"で起動してみるとapi呼び出しによる初期化が確認できるかと思います。
最後にこれまでの作業を行ってpackage.jsonは最終的に以下のようになりましたので、参考のため載せときます。
{ "name": "react_hands-on_project", "version": "1.0.0", "description": "A Vue.js project", "author": "arimuraterutoshiMac <arimuraterutoshi@192.168.11.6>", "private": true, "scripts": { "dev": "node build/dev-server.js", "start": "node build/dev-server.js", "build": "node build/build.js", "unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --single-run", "e2e": "node test/e2e/runner.js", "test": "npm run unit && npm run e2e", "lint": "eslint --ext .js,.vue src test/unit/specs test/e2e/specs" }, "dependencies": { "jquery": "^3.2.1", "react": "^15.5.4", "react-dom": "^15.5.4", "react-redux": "^5.0.4", "react-router-dom": "^4.1.1", "redux": "^3.6.0", "redux-thunk": "^2.2.0", "vue": "^2.2.6", "vue-router": "^2.3.1", "webpack": "^2.4.1", "webpack-dev-server": "^2.4.5" }, "devDependencies": { "autoprefixer": "^6.7.2", "babel-core": "^6.24.1", "babel-eslint": "^6.1.2", "babel-loader": "^6.4.1", "babel-plugin-istanbul": "^4.1.1", "babel-plugin-transform-decorators-legacy": "^1.3.4", "babel-plugin-transform-runtime": "^6.22.0", "babel-preset-env": "^1.3.2", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "babel-register": "^6.22.0", "chai": "^3.5.0", "chalk": "^1.1.3", "chromedriver": "^2.27.2", "connect-history-api-fallback": "^1.3.0", "copy-webpack-plugin": "^4.0.1", "cross-env": "^4.0.0", "cross-spawn": "^5.0.1", "css-loader": "^0.28.0", "eslint": "^3.19.0", "eslint-config-standard": "^6.2.1", "eslint-friendly-formatter": "^2.0.7", "eslint-loader": "^1.7.1", "eslint-plugin-html": "^2.0.0", "eslint-plugin-promise": "^3.4.0", "eslint-plugin-react": "^6.10.3", "eslint-plugin-standard": "^2.0.1", "eventsource-polyfill": "^0.9.6", "express": "^4.14.1", "extract-text-webpack-plugin": "^2.0.0", "file-loader": "^0.11.1", "friendly-errors-webpack-plugin": "^1.1.3", "glob-loader": "^0.3.0", "html-webpack-plugin": "^2.28.0", "http-proxy-middleware": "^0.17.3", "inject-loader": "^3.0.0", "karma": "^1.4.1", "karma-coverage": "^1.1.1", "karma-mocha": "^1.3.0", "karma-phantomjs-launcher": "^1.0.2", "karma-phantomjs-shim": "^1.4.0", "karma-sinon-chai": "^1.3.1", "karma-sourcemap-loader": "^0.3.7", "karma-spec-reporter": "0.0.30", "karma-webpack": "^2.0.2", "lolex": "^1.5.2", "mocha": "^3.2.0", "nightwatch": "^0.9.12", "opn": "^4.0.2", "optimize-css-assets-webpack-plugin": "^1.3.0", "ora": "^1.2.0", "phantomjs-prebuilt": "^2.1.14", "react-hot-loader": "^1.3.1", "react-redux": "^5.0.4", "rimraf": "^2.6.0", "selenium-server": "^3.0.1", "semver": "^5.3.0", "shelljs": "^0.7.6", "sinon": "^2.1.0", "sinon-chai": "^2.8.0", "url-loader": "^0.5.8", "vue-loader": "^11.3.4", "vue-style-loader": "^2.0.5", "vue-template-compiler": "^2.2.6", "webpack": "^2.3.3", "webpack-bundle-analyzer": "^2.2.1", "webpack-dev-middleware": "^1.10.0", "webpack-hot-middleware": "^2.18.0", "webpack-merge": "^4.1.0" }, "engines": { "node": ">= 4.0.0", "npm": ">= 3.0.0" }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8" ] }
github
検証を行ったプログラムはGitHub - teruuuuuu/react_hands-on_projectにあげておきましたので、こちらで確認できるようになっています。
PostgreSQLで階層データ取得
PostgreSQL8.4からの新機能であるWITH RECURSIVEが便利であったのでメモ
WITH RECURSIVEは階層構造のデータを再起的に取得するのに利用でき、例えば以下のようなテーブルとデータがあったとして
CREATE TABLE user_mst ( user_id bigserial NOT NULL PRIMARY KEY, family_name character(32), first_name character(32), boss_id bigint ); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 1, '田中', '一郎', -1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 2, '田中', '二郎', 1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 3, '田中', '三郎', 1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 4, '田中', '四郎', 1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 5, '田中', '五郎', 2); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 6, '山田', '六郎', -1); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 7, '山田', '七郎', 6); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 8, '山田', '八郎', 7); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 9, '山田', '九郎', 8); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 10, '山田', '十郎', 9); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 11, '山田', '十一郎', 10); INSERT INTO user_mst( user_id, family_name, first_name, boss_id) VALUES( 12, '佐藤', '十二郎', -1);
これに対して以下のSQLを実行すると、子供のデータを再起的に取得することができる。
WITH RECURSIVE user_hierarchy AS ( SELECT user_id FROM user_mst WHERE user_id = 1 UNION ALL SELECT user_mst.user_id FROM user_mst, user_hierarchy WHERE user_mst.boss_id = user_hierarchy.user_id ) SELECT * FROM user_mst WHERE user_id IN ( SELECT user_id FROM user_hierarchy ORDER BY user_hierarchy.user_id );
ここでのuser_idを変更して確認してみると確かにそうなっていることが確認できる。
それでは逆に親のデータを再起的に取得したい場合は、UNIONしているSELECTのWHERE条件で使っているuser_idとboss_idを入れ替えて以下のようにするだけである。
WITH RECURSIVE user_hierarchy AS ( SELECT user_id, boss_id FROM user_mst WHERE user_id = 10 UNION ALL SELECT user_mst.user_id, user_mst.boss_id FROM user_mst, user_hierarchy WHERE user_mst.user_id = user_hierarchy.boss_id ) SELECT * FROM user_mst WHERE user_id IN ( SELECT user_id FROM user_hierarchy ORDER BY user_hierarchy.user_id );
なかなか便利そう
Reactのプロジェクト作成
vue-cliによるプロジェクト作成
reactを始める時は最初のプロジェクト作成が障壁になりそうでreact-cliという公式が出しているツールを使えば開発環境を素早く構築することができますが、内部で利用しているwebpackの設定がどうなっているかなど良く分からなさそうで、使ってみると不便そうな気がしたので自分は試したことがありません。
そこで今回はvueでの開発に使用しているvue-cliを使用してプロジェクトを作成後、reactを利用できるようにしたいと思います。vue-cliの方が開発環境や、ビルドの設定の確認、編集が行いやすくなっておりますので、個人的にはこちらの方が扱いやすいのでこっちで試してみたいと思います。
vue-cliのインストール
npm install -g vue-cli
プロジェクト作成
vue init webpack my_project
cd my-project
npm install
一旦起動してみる
プロジェクト作成直後はvueのプロジェクトになっておりますが、とりあえず以下のコマンドでwebpackを起動して動作を確認してみたいと思います。
npm run dev
上記コマンド実行後にブラウザが開いてvue-jsのアイコンが表示された画面が表示されたかと思います。
vue-cliで作成したプロジェクトをreact化する
それではreactで開発できるようにしていきたいと思います。まずは必要なモジュールをインストールします
npm install –save react react-dom redux react-redux npm install –save-dev babel-loader babel-core babel-preset-react babel-preset-es2015
npm install –save-dev eslint@3.x babel-eslint@6
npm install –save-dev file-loader
npm install –save-dev glob-loader
npm install –save-dev eslint-plugin-react
それから以下の".babelrc"をプロジェクトのルートディレクトリに作成します。
{ "presets": [ "es2015", "react"] }
次にeslintでreactのプラグインを使うように以下の内容にします。
// http://eslint.org/docs/user-guide/configuring module.exports = { root: true, parser: 'babel-eslint', parserOptions: { sourceType: 'module' }, env: { browser: true }, // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style extends: [ "eslint:recommended", "plugin:react/recommended" ], // required to lint *.vue files plugins: [ 'html', 'react' ], // add your custom rules here 'rules': { // allow paren-less arrow functions 'arrow-parens': 0, // allow async-await 'generator-star-spacing': 0, 'react/jsx-uses-vars': 1, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 } }
あとは"src/main.js"を以下のないように変更したらReactでのコンポーネント初期化が確認できると思います。コンポーネントの初期化についてはes2015の機能であるconstで値の再代入をできないようにしております。es2015を使う場合は変数を定義する場合できるだけlet, constでスコープを制限したり再代入を禁止することで想定外の副作用を防げるようにしておいた方がよさそうです。
import React from 'React' import { render } from 'react-dom' const element = ( <div> <h1>Hello, React!</h1> </div> ) render( element , document.getElementById('app') );
それから"npm run dev"で起動すると思うのですがたくさん警告が表示されると思います。これについてインポートするフモジュール名が実際はreactなのにReactでしているのが原因で、モジュール名はケバブケースを用いるのが標準のようですhttps://github.com/AngularClass/angular2-webpack-starter/issues/926。 それでは以下のようにimport先のモジュール名を変更して再度"npm run dev"を実行してみると警告もなく動くのが確認できると思います。
import React from 'React' ↓ import React from 'react'
ビルドをする場合は"npm run build"を実行すればよく、設定などを変更したい場合はbuildディレクトリの中身を確認して変更していけばどうにかなりそうです。
ScalaでfoldLeftを使ってみる
foldLeftを使ってみる
foldLeft使用の覚書
list内の単語の集計を行って降順にする
これだけでリスト内の単語の集計とソートが行える
val countWordsList = List("a", "b", "c", "a", "d", "b", "e", "b") val wordAggregate = countWordsList.foldLeft(Map[String, Int]().withDefaultValue(0)){ (map, key) => map + (key -> (map(key) + 1)) }.toSeq.sortBy(_._2).reverse println(wordAggregate)
文字列の置換を行う
val replaceMap = Map("{$1}" -> "health", "{$2}" -> "wealth") val beforeText = "{$1} is better than {$2}" val afterText = replaceMap.foldLeft(beforeText){ (tex, map) => tex.replace(map._1, map._2) } println(afterText)
PostgreSQLの文字列関数を使ってみる
PostgreSQLの文字列関数を使ってみる
translate関数で文字を置換する
1文字ずつ置換先の文字を指定することができる
例えば以下のように指定することで全角数値を半角数値に変換できる
translate('13ndfhauo392805735678936', '0123456789','0123456789');
また置換先の文字を指定しない場合は文字を消すことができる(PostgreSQL以外の場合では動きが違うかもしれないです)。以下のように指定すると数値を除去して表示できる。
select translate( '13ndfhauo392805735678936', '0123456789', '');
これらを組み合わせると数値のみを表示するといったことができます。
select translate(translate('13ndfhauo392805735678936', '0123456789','0123456789'), translate( '13ndfhauo392805735678936', '0123456789', ''), '') ;
lpad,rpadで指定した文字数で表示する
lpadでは指定した文字数に足りない場合は右側に文字を埋めて表示する。以下のSQLでは5文字の長さで表示するように、足りない場合は'02'で文字埋めして表示する。
select lpad('12', 5, '02');
文字数を超過する場合は最初の文字を切り出して表示します。
select lpad('236789977', 5, '02')
左側に文字を超過する場合はrpadを使います。
select rpad('12', 5, '02');
PlayFramework事始め
PlayFramework 事始め
- PlayFramework 事始め
PlayFramework 事始め
playのインストール
公式から最新のactivatorをダウンロードしてbinディレクトリにパスを通しておく。
プロジェクトの作成
activatorからプロジェクト作成
新規プロジェクトの作成は"activator new"から行う
activator new
giter8からplayのプロジェクトを作成
giter8を使うことでsbt newでplayのプロジェクトを作成できる
sbt new playframework/play-scala-seed.g8
上記コマンドで作成したプロジェクトをInteliJに取り込んでみるとbuild.sbtでPlayのプラグインがcannot resolveになることがあります。 その場合は、以下の手順でInteliJのプロジェクトを作成します。
1.既存のtarget, .idea, .idea_modulesを削除 2.project/plugins.sbtに以下を追記 addSbtPlugin("com.github.mpeltonen" % "sbt-idea" % "1.6.0") 3."sbt gen-idea"を実行してプロジェクトファイルを作成。それからはInteliJで取り込んでみるとbuild.sbtで発生していたcannot resolveが消えていると思います。 それでもcannot resolveになる場合はもう一度.ideaを削除して開きなおしてみると直るかもしれないです。
コンソールの起動
cd プロジェクト名
activator
サーバを起動
[プロジェクト名] $ run
コンパイル
[プロジェクト名] $ compile
テスト
[プロジェクト名] $ test
デバッグ
ポート指定でデバッグ起動できる
activator -jvm-debug 9999
sbtの利用
Play コンソールは普通の sbt コンソールでもあるため、sbtの機能も利用することができます。 ソース更新のたびにコンパイルする
[プロジェクト名] $ ~ compile
サーバ稼働中ソース更新のたびにビルドする
[プロジェクト名] $ ~ run
ソースが更新されるたびにテストする
[プロジェクト名] $ ~ test
IDE
eclipseで開発する
project/plugins.sbtに以下のプラグインを追加する設定を行う
addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "4.0.0")
それからeclipseのプロジェクトファイルを生成する
activator eclipse
InteliJ IDEA
ファイルを開くでプロジェクトとして認識してくれる
Playアプリケーションの構造
app → アプリケーションソース └ assets → コンパイルされたアセットソース └ stylesheets → 通常は LESS CSS ソース └ javascripts → 通常は CoffeeScript ソース └ controllers → アプリケーションコントローラ └ models → アプリケーションビジネス層 └ views → テンプレート build.sbt → アプリケーションビルドスクリプト conf → 設定ファイル、および (クラスパス上の) その他のコンパイルされないリソース └ application.conf → メイン設定ファイル └ routes → ルート定義 dist → プロジェクト成果物に含める任意のファイル public → 公開アセット └ stylesheets → CSS ファイル └ javascripts → Javascript ファイル └ images → 画像ファイル project → sbt 設定ファイル群 └ build.properties → sbt プロジェクトの目印 └ plugins.sbt → Play 自身の定義を含む sbt プラグイン lib → 管理されていない依存ライブラリ logs → ログフォルダ └ application.log → デフォルトログファイル target → 生成物 └ resolution-cache → 依存性に関する情報 └ scala-2.10 └ api → 生成された API ドキュメント └ classes → コンパイルされたクラスファイル └ routes → routes から生成されたソース └ twirl → テンプレートから生成されたソース └ universal → アプリケーションパッケーs時 └ web → コンパイルされた web アセット test → 単体、および機能テスト用のソースフォルダ
コントローラーを追加してみる
まず簡単なコントローラを追加してみる
package controllers import javax.inject.{Inject, Singleton} import play.api.mvc.{Action, Controller} @Singleton class TestController @Inject() extends Controller{ def index = Action { request => Ok("Got request [" + request + "]") } }
play 2.4からDIが使われるようになっている、今回はDI対象のコンポーネントがないが、DI対象のコンポーネントがある場合は@Injectアノテーションの後に引数としてDI対象コンポーネントを渡すことができる。またクラス自体に@Singletonアノテーションが付与されておりシングルトンとして扱う事を明示しているが、PlayフレームワークがDIに対応する前は普通にobject型として定義するなどしていた。
また受け取るリクエストはrequestで指定することができるが、これはplayフレームワークのplay.api.mvc.Actionクラスがリクエストを受け取る際にapplyメソッドでリクエストをパースしているようである。
それからリクエストを受け取れるようにするためのルート定義をconf/routesに追加する
GET /test controllers.TestController.index
これで"http://localhost:9000/test"にアクセスしたら自作のコントローラが呼び出されるようになる。
リクエストを暗黙のパラメータとして扱う
受け取るリクエストを暗黙のパラメータにすることもできる。受け取ったリクエストのjsonをパースするときとかは暗黙のパラメータとして受け取って
package controllers import javax.inject.{Inject, Singleton} import play.api.mvc.{Action, Controller, RequestHeader} @Singleton class TestController @Inject() extends Controller{ def index = Action { implicit request => Ok(greeting("Hello")) } private def greeting(say: String)(implicit req: RequestHeader) = say + "," + req.remoteAddress }
jsonをリクエストとして受け取れるようにする
まずはplay標準のパーサでjsonのリクエストを受け取ってみるため以下のメソッドを追加する。
def jsonReq = Action(parse.json) { request => (request.body \ "name").asOpt[String].map { name => Ok("Hello " + name) }.getOrElse { BadRequest("Missing parameter [name]") } }
それからroutesを追加
POST /json controllers.TestController.jsonReq
次に以下のコマンドでリクエストを飛ばしjsonを受け取れている事を確認する
curl ‘http://localhost:9000/json’ -H ‘Content-Type: text/json’ –data-binary ‘{“name”: “jon doh”}’
jsonをcase classにセットしてみる
次にjsonのリクエストをcase classにセットする方法を試してみたいと思う。次のようにcaseクラスとmapping、Actionメソッドを定義しておく。
case class AddTodo(categoryId:Long, title:String, text:String) implicit val addTodoForm:Form[AddTodo] = Form ( mapping( "categoryId" -> longNumber, "title"-> text, "text"-> text )(AddTodo.apply)(AddTodo.unapply) ) def jsonReq = Action { request => try{ addTodoForm.bindFromRequest.fold( errors => {BadRequest("bad request")}, validForm => { Ok("categoryId: " + validForm.categoryId + " title:" + validForm.title + " text:" + validForm.text) } ) } }
それから以下のリクエストを投げることでjsonをcase classにセットできることが確認できる
curl ‘http://localhost:9000/json’ -H ‘Content-Type: text/json’ –data-binary ‘{“categoryId”: 1, “title”: “play test”, “text”: “text”}’
Formにセットする際のバリデーション処理は以下の公式を確認する
https://www.playframework.com/documentation/ja/2.4.x/ScalaForms
jsonを返してみる
jsonをレスポンスで返すのも簡単に行える。先ほどのjsonで受け取ったリクエストをそのまま返すようにして動作確認してみたいと思う。
まず先ほど利用したコントローラに以下のメソッドを追加しておく
implicit val todoWrite = new Writes[AddTodo] { def writes(todo: AddTodo) = Json.obj( "categoryId" -> todo.categoryId, "title" -> todo.title, "text" -> todo.text ) }
それから先ほどのコントローラのメソッドのレスポンスを以下のように変更する
def jsonReq = Action { request => try{ addTodoForm.bindFromRequest.fold( errors => {BadRequest("bad request")}, validForm => { Ok(Json.toJson(validForm)) } ) } }
これで受け取ったjsonをFormにセットしてそのまま返す動作が確認できたはずである。jsonのルート要素を変更したい場合は以下のようにすれば良い
Ok(Json.obj("todo" -> validForm))
セッションを使ってみる
以下のメソッドによりセッションごとでのカウントアップを行うことができる。
def count = Action { request => val nextCount = request.session.get("count") match { case Some(x) => x.toInt + 1 case None => 1 } Ok("counta: " + nextCount).withSession( request.session + ("count" -> nextCount.toString) ) }
ただしplayフレームワークはステートレスのフレームワークとなっておりplay側ではセッションを保持せずクライアント側でクッキーとして保持させる形になっている。クライアント側で自由に帰られては困るような情報はredisなど別で持たせるような仕組みにしておく必要がある。
htmlテンプレートを使ってみる
htmlテンプレートのtwirlを使って画面を描画してみる まず以下のコントローラを作成する
package controllers import javax.inject.{Inject, Singleton} import play.api.mvc.{Action, Controller} case class User(firstName: String, lastName: String) @Singleton class SampleController @Inject() extends Controller{ def index = Action { val user = User("john", "does") Ok(views.html.sample(user)) } }
それからapp/viewsにsample.scala.htmlのファイルを作成し、以下を書き込む
@(user: User) <span>this is twirl page</span> @defining(user.firstName + " " + user.lastName) { fullName => <div>Hello @fullName</div> }
これでtwirlのページが表示されるはずである。置換周りについて詳しくか以下を参照する。
https://www.playframework.com/documentation/ja/2.4.x/ScalaTemplates
DBにアクセスする
anromを使う
play2.5では既にplayの標準ではなくなっているけどanormを使ってDBにアクセスしてみる。
依存ライブラリの追加
まずbuild.sbtのdependenciesに以下を追加する
今回は動作確認なのでDBにはh2を使う
libraryDependencies ++= Seq( jdbc, "com.typesafe.play" %% "anorm" % "2.4.0", "com.h2database" % "h2" % "1.4.193" )
DB接続先設定
それからapplication.confにDBの接続先設定として以下を記入する。defaultはplayフレームワークで扱うDB名になる
db { # You can declare as many datasources as you want. # By convention, the default datasource is named `default` # https://www.playframework.com/documentation/latest/Developing-with-the-H2-Database default.driver = org.h2.Driver default.url = "jdbc:h2:mem:play" default.username = sa default.password = "" # You can turn on SQL logging for any datasource # https://www.playframework.com/documentation/latest/Highlights25#Logging-SQL-statements default.logSql=true }
初期データの投入
h2のコンソールにアクセスする場合はactivatorのコンソールで"h2-browser"を入力する
h2-browser
h2のコンソールで接続先のDBにはapplication.confで設定した"jdbc:h2:mem:play"を指定する。
それから今回使用するDB作成のSQLを実行する
create table request_text ( id bigint identity primary key, input_text char(256) ); insert into request_text(id, input_text) values ( null, 'one text'); insert into request_text(id, input_text) values ( null, 'two text'); insert into request_text(id, input_text) values ( null, 'three text'); insert into request_text(id, input_text) values ( null, 'four text');
今回はDBのデータを永続化させていなため次回play起動時にはデータがリセットされるので注意が必要
DBオブジェクト
defaultのDBを使えるようにするために以下のクラスを作成する。使用するときはコントローラ側でDIするようにする。
package db import javax.inject.Inject import play.api.db.DBApi @javax.inject.Singleton class DefaultDB @Inject()(dbapi: DBApi){ val db = dbapi.database("default") }
DAOの作成
次にDBアクセス用のDAOを作成する
package services.repository import javax.inject.Inject import dto.RequestText import play.api.db.Database import anorm.SqlParser.get import anorm.{SQL, ~} case class RequestText(id: Long, input_text: String) @javax.inject.Singleton class TextService @Inject()() { val textParser = { get[Long]("id") ~ get[String]("input_text") map { case id ~ input_text => RequestText(id, input_text) } } def addText(requestText: String)(implicit db: Database): Option[Long] = { insertText(requestText) } def insertText(requestText: String)(implicit db: Database) = { db.withConnection { implicit connection => SQL( """ insert into request_text (id, input_text) values ( (select nextval('text_id_seq')), {request_text} ) """ ).on( 'request_text -> requestText ).executeInsert() } } def getText()(implicit db: Database): Seq[RequestText] = { db.withConnection { implicit connection => SQL("select * from request_text").as(textParser.*) } } }
コントローラ側からDAOを利用する
それから、DBとDAOをDIして利用するコントローラも作成する
package controllers import javax.inject.{Inject, Singleton} import db.DefaultDB import dto.RequestText import play.api.data.Form import play.api.data.Forms._ import play.api.data.Forms.mapping import play.api.i18n.MessagesApi import play.api.mvc.{Action, Controller} import services.repository.TextService @Singleton class SampleController @Inject()(val messagesApi: MessagesApi,defaultDB: DefaultDB,textService: TextService) extends Controller{ val db = defaultDB.db def anormSample = Action { implicit request => db.withTransaction { tr => val texts:Seq[RequestText] = textService.getText()(db) Ok(views.html.sample(texts)) } } case class AddText(input_text:String) implicit val addTextForm:Form[AddText] = Form ( mapping( "input_text"-> text )(AddText.apply)(AddText.unapply) ) def anormAdd = Action { implicit request => addTextForm.bindFromRequest.fold( errors => {BadRequest("bad request")}, validForm => { db.withTransaction{ tr => textService.insertText(validForm.input_text)(db) } } ) Redirect("/anorm") } }
DBオブジェクトとDAOはDIさせて利用している
最後に動作確認ように以下のhtmlを準備したらanormの確認が行えるはず
@import dto.RequestText @(requestTexts: Seq[RequestText]) <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>anorm sample page</title> </head> <body> <span>this is anorm sample</span> <form method="post" action="anorm"> <input type="text" name="input_text" ></input><input type="submit" name="butto" value="送信" /> </form> <ul> @for(requestText <- requestTexts) { <li>@requestText.id: @requestText.input_text</li> } </ul> </body> </html>
今回はコントローラ側から暗黙のパラメータとしてDBを渡すようにしており、利用するDBを切り替える場合にも簡単に対応できる。トランザクション処理は"db.withTransaction"で行うことができる
ScalikeJDBCを使う
次にScalikeJDBCを試してみたいと思います。 まずbuild.sbtのlibDependenciesが以下になるようにします。
libraryDependencies ++= Seq( filters, "com.h2database" % "h2" % "1.4.193", "org.scalikejdbc" %% "scalikejdbc" % "2.5.1", "org.scalikejdbc" %% "scalikejdbc-config" % "2.5.1", "org.scalikejdbc" %% "scalikejdbc-play-initializer" % "2.5.1", "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % Test )
それからconf/application.confに以下を追加します。
scalikejdbc.global.loggingSQLAndTime.enabled=true scalikejdbc.global.loggingSQLAndTime.singleLineMode=false scalikejdbc.global.loggingSQLAndTime.logLevel=debug scalikejdbc.global.loggingSQLAndTime.warningEnabled=true scalikejdbc.global.loggingSQLAndTime.warningThresholdMillis=5 scalikejdbc.global.loggingSQLAndTime.warningLogLevel=warn play.modules.enabled += "scalikejdbc.PlayModule" # scalikejdbc.PlayModule doesn't depend on Play's DBModule play.modules.disabled += "play.api.db.DBModule"
Query DSLでSQLを実行してみる
それではまずは一覧表示できるようにしてみたいと思います。 以下のmodelを追加します。
import scalikejdbc._ case class RequestTexts(id: Long, input_text: String) { def save()(implicit session: DBSession = RequestTexts.autoSession): RequestTexts = RequestTexts.save(this)(session) def destroy()(implicit session: DBSession = RequestTexts.autoSession): Int = RequestTexts.destroy(this)(session) } object RequestTexts extends SQLSyntaxSupport[RequestTexts] { override val autoSession = AutoSession override val schemaName = Some("PUBLIC") override val tableName = "request_text" override val columns = Seq("id", "input_text") val r = RequestTexts.syntax("r") def apply(r: SyntaxProvider[RequestTexts])(rs: WrappedResultSet): RequestTexts = apply(r.resultName)(rs) def apply(r: ResultName[RequestTexts])(rs: WrappedResultSet): RequestTexts = new RequestTexts( id = rs.get(r.id), input_text = rs.get(r.input_text) ) def save(entity: RequestTexts)(implicit session: DBSession = autoSession): RequestTexts = { withSQL { update(RequestTexts).set( column.id -> entity.id, column.input_text -> entity.input_text ).where.eq(column.id, entity.id) }.update.apply() entity } def destroy(entity: RequestTexts)(implicit session: DBSession = autoSession): Int = { withSQL { delete.from(RequestTexts).where.eq(column.id, entity.id) }.update.apply() } def findAll()(implicit session: DBSession = autoSession): List[RequestTexts] = { withSQL(select.from(RequestTexts as r)).map(RequestTexts(r.resultName)).list.apply() } }
それからmodelを利用するコントローラを追加します。
package controllers import javax.inject._ import models.RequestTexts import play.api.data.Form import play.api.data.Forms.mapping import play.api.data.Forms._ import play.api.libs.json.{JsError, Json, Writes} import play.api.mvc._ import scalikejdbc.DB @Singleton class ScalikeJdbcController @Inject() extends Controller { // jsonの書き込み implicit val requestWrite = new Writes[RequestTexts] { def writes(request: RequestTexts) = Json.obj( "id" -> request.id, "input_text" -> request.input_text ) } def list = Action { implicit request => DB.readOnly { implicit session => Ok(Json.obj("root" -> RequestTexts.findAll())) } } }
あとはapiを利用できるようにするためconf/routesに以下を追加します。
GET /scalike controllers.ScalikeJdbcController.list
これで以下のコマンドを実行するとjsonのレスポンスが確認できると思います。 curl ‘http://localhost:9000/scalike’
id指定でselectする場合は以下のようにwhereのパラメータを渡すことができる
def findById(id: Long)(implicit session: DBSession = autoSession): Option[RequestTexts] = { withSQL {select.from(RequestTexts as r).where.eq(r.id, id) }.map(RequestTexts(r.resultName)).single.apply() }
SQLInterpolationでSQLを実行してみる
実行するSQLを自分で書きたい場合は以下のようになります。
def findByIdWithQuery(id: Long)(implicit session: DBSession = autoSession): Option[RequestTexts] = { sql"select id, input_text from request_text where id = ${id}" .map{rs => RequestTexts(rs.long("id"), rs.string("input_text"))}.single.apply }
insertを実行する
insert文は以下のようになります。
def create(entity: RequestTexts)(implicit session: DBSession = autoSession): RequestTexts = { val generatedKey = withSQL { insert.into(RequestTexts).namedValues( column.input_text -> entity.input_text ) }.updateAndReturnGeneratedKey.apply() RequestTexts( id = generatedKey, input_text = entity.input_text) }
slick3を使ってみる
slick3も使ってみたいと思います。
build.sbtが以下になるようにします。
libraryDependencies ++= Seq( filters, "com.h2database" % "h2" % "1.4.193", "com.typesafe.play" %% "play-slick" % "2.0.2", "com.typesafe.slick" %% "slick-codegen" % "3.1.1", "org.scalatestplus.play" %% "scalatestplus-play" % "2.0.0" % Test )
それからapplication.confでDBの接続先を設定します。Anorm、scalikeとは設定が違うので注意です。
slick.dbs.default.driver="slick.driver.H2Driver$" slick.dbs.default.db.driver=org.h2.Driver slick.dbs.default.db.url="jdbc:h2:mem:play" slick.dbs.default.db.user=sa slick.dbs.default.db.password=""
SourceCodeGeneratorでモデルを自動生成する
SourceCodeGeneratorを使ってモデルのクラスを自動で生成したいと思います。次のobjectを作成しておいてください。
package tool.codegen import slick.codegen.SourceCodeGenerator object SlickCodgeGen { def main(args: Array[String]): Unit = { codeGen } def codeGen: Unit ={ val slickDriver = "slick.driver.H2Driver" val jdbcDriver = "org.h2.Driver" val url ="jdbc:h2:mem:play" val user = "sa" val password = "" val outputFolder = "app" val pkg = "models" SourceCodeGenerator.main( Array( slickDriver, jdbcDriver, url, outputFolder, pkg, user, password ) ) } }
あとはcodeGenを呼び出せば良いのですが、プロジェクト作成時に作られるHomeControllerの最初の部分で呼び出すようにしてみます。
package controllers import javax.inject._ import play.api._ import play.api.mvc._ @Singleton class HomeController @Inject() extends Controller { import tool.codegen.SlickCodgeGen SlickCodgeGen.codeGen def index = Action { implicit request => Ok(views.html.index()) } }
これでhttp://localhost:9000/にアクセスするとモデルクラスが自動で生成されると思います。
テーブルのカラムのデフォルト値をnextval(‘自作seq’)などとしているとモデルクラスを作成するときにエラーになりました。その場合はカラムにauto incrementを付与するなどしてDB自体の機能で自動採番させれば大丈夫なようです。PostgreSQLならserial, bigserial型を使えば良さそうだと思います。
一覧表示してみる
それからmodelを利用するコントローラを追加します。
package controllers import play.api.mvc._ import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.db.slick._ import slick.driver.JdbcProfile import models.Tables._ import javax.inject.Inject import javax.inject.Singleton import slick.driver.H2Driver.api._ import play.api.libs.json._ @Singleton class SlickContorller @Inject()(val dbConfigProvider: DatabaseConfigProvider) extends Controller with HasDatabaseConfigProvider[JdbcProfile] { // jsonの書き込み implicit val requestWrite = new Writes[RequestTextRow] { def writes(request: RequestTextRow) = Json.obj( "id" -> request.id, "input_text" -> request.inputText ) } def list = Action.async { implicit rs => db.run(RequestText.sortBy(t => t.id).result).map{ texts => Ok(Json.obj("root" -> texts)) } } }
あとはapiを利用できるようにするためconf/routesに以下を追加します。
GET /slick controllers.SlickContorller.list
これで以下のコマンドを実行するとjsonのレスポンスが確認できると思います。 curl ‘http://localhost:9000/slick’
DatabaseForconfigで使用するDBを選択する
Slick3では使用するDBを変更することも簡単に行えます。まずapplication.confを以下のようにして新しいDB接続設定を追記してください。
mydb = { driver=org.h2.Driver url="jdbc:h2:mem:play" user=sa password="" }
それから一覧表示するコントローラを以下のように編集してください。
package controllers import play.api.mvc._ import play.api.libs.concurrent.Execution.Implicits.defaultContext import play.api.db.slick._ import slick.driver.JdbcProfile import models.Tables._ import javax.inject.Inject import javax.inject.Singleton import slick.driver.H2Driver.api._ import play.api.libs.json._ @Singleton class SlickContorller @Inject() extends Controller { val mydb = Database.forConfig("mydb") // jsonの書き込み implicit val requestWrite = new Writes[RequestTextRow] { def writes(request: RequestTextRow) = Json.obj( "id" -> request.id, "input_text" -> request.inputText ) } def list = Action.async { implicit rs => mydb.run(RequestText.sortBy(t => t.id).result).map{ texts => Ok(Json.obj("root" -> texts)) } } }
これで'http://localhost:9000/slick'にアクセスすると新しく用意した接続先が使えていることが確認できます。
ID指定で取得する
先ほどは一覧表示したので今回はidを指定してみたいと思います。コントローラに以下のidでフィルターをかけるメソッドを追加します。
def findById(id: Long) = Action.async {implicit rs => mydb.run(RequestText.filter(t => t.id === id).result).map{ text => Ok(Json.obj("root" -> text)) } }
それからroutesに以下を追加してメソッドを呼び出せるようにします。
GET /find/:id controllers.SlickContorller.findById(id :Long)
これで'curl ‘http://localhost:9000/find/2'のようにid指定でSQLを実行できるのを確認できます。
createを実行する
次にinsertを行ってみます。先ほどのコントローラーに以下を追加します。
case class Reuqest(input_text:String) // jsonの読み込み implicit val addRequestForm:Form[Reuqest] = Form ( mapping( "input_text"-> text )(Reuqest.apply)(Reuqest.unapply) ) def create = Action.async { implicit rs => addRequestForm.bindFromRequest.fold( error => { Future { BadRequest(Json.obj("result" ->"failure")) } }, form => { val requestTextRow = RequestTextRow(0, Option(form.input_text)) mydb.run(RequestText += requestTextRow).map { _ => Ok(Json.obj("result" -> "success")) } } ) }
それからroutesに以下を追加します。
POST /create controllers.SlickContorller.create
これで以下のコマンドを実行するとinsertが行えているのが確認できるはずです。
curl -H “Content-type: application/json” -XPOST -d ‘{“input_text”:“new text”}’ http://localhost:9000/create
updateを実行する
updateを実行する場合は以下のようになります。
def update (id: Long)= Action.async { implicit rs => addRequestForm.bindFromRequest.fold( error => { Future { BadRequest(Json.obj("result" ->"failure")) } }, form => { val requestTextRow = RequestTextRow(id, Option(form.input_text)) mydb.run(RequestText.filter(t => t.id === requestTextRow.id.bind).update(requestTextRow)).map { _ => Ok(Json.obj("result" -> "success")) } } ) }
それからroutesに以下を追加します。
POST /update/:id controllers.SlickContorller.update(id :Long)
これで以下のコマンドを実行するとupdateが行えているのが確認できるはずです。
curl -H “Content-type: application/json” -XPOST -d ‘{“input_text”:“update text”}’ http://localhost:9000/update/2
deleteを実行する
updateを実行する場合は以下のようになります。
def delete(id: Long) = Action.async {implicit rs => mydb.run(RequestText.filter(t => t.id === id).delete).map { _=> Ok(Json.obj("result" -> "success")) } }
それからroutesに以下を追加します。
POST /delete/:id controllers.SlickContorller.delete(id :Long)
これで以下のコマンドを実行するとdeleteが行えているのが確認できるはずです。
curl -H “Content-type: application/json” -XPOST -d ‘{}’ http://localhost:9000/delete/3
Javaでの文字化け検出について
文字化け検出
ファイル読み込み時などで文字化けが発生した時の検出方法について
例えばUTF-8でエンコードされたテキストファイルをShift-JISで読み込もうとした際に、該当する文字が存在しない場合は'0xFFFD'の文字に変換されます。
Javaは内部的にはUnicode(正確にはUTF-16らしい)実際Stringクラスのコメントを確認してみると"represents a string in the UTF-16 format in which supplementary characters"のコメントが確認できるので内部的にはUTF-16が使われているような感じです。(jdk1.8.0_73で確認)
それで文字化けが発生した時に使われる文字ですがひし形ないに?が表記されている�が使われると思いますが、UTF-16でこの文字は0xFFFDの文字コードで表せられるので、文字列内に"(char) 0xfffd"の文字が含まれているかのチェックするのが文字化けしているかの確認方法で一番簡単そうです