Quantcast
Channel: 文系プログラマによるTIPSブログ
Viewing all 140 articles
Browse latest View live

angular v6にアップデートした際に起きたエラーの対応

$
0
0

いつものです〜


f:id:treeapps:20170918135756p:plain

Local workspace file ('angular.json') could not be found.

$ yarn start
yarn run v1.3.2
$ ng serve --extract-css=true--proxy-config proxy.conf.json
Local workspace file ('angular.json') could not be found.
Error: Local workspacLocal workspace file ('angular.json') could not be found.e file ('angular.json') could not be found.
    at WorkspaceLoader._getProjectWorkspaceFilePath (/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/workspace-loader.js:37:19)
    at WorkspaceLoader.loadWorkspace (/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/workspace-loader.js:24:21)
    at ServeCommand._loadWorkspaceAndArchitect (/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:195:32)
    at ServeCommand.<anonymous>(/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:47:25)
    at Generator.next (<anonymous>)
    at /Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:7:71
    at new Promise (<anonymous>)
    at __awaiter (/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:3:12)
    at ServeCommand.initialize (/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:46:16)
    at Object.<anonymous>(/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/command-runner.js:87:23)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

angular v6になり、「.angular-cli.json」というファイルから「angular.json」に変更になり、jsonの構造も変わりました。

angular-cliのバージョンアップをする

$ brew upgrade angular-cli
==> Upgrading 1 outdated package, with result:
angular-cli 1.7.4 -> 6.0.0
==> Upgrading angular-cli
==> Downloading https://homebrew.bintray.com/bottles/angular-cli-6.0.0.high_sierra.bottle.tar.gz
######################################################################## 100.0%==> Pouring angular-cli-6.0.0.high_sierra.bottle.tar.gz
🍺  /usr/local/Cellar/angular-cli/6.0.0: 6,652 files, 54.9MB

.angular-cli.json を angular.json に変換する

以下のupdateコマンドでマイグレーションしてくれました。

$ ng update @angular/cli

しかし、私の場合sourceRootは「client」としていたのですが、マイグレーション結果は「src」となってしまっていました。これは手動で「client」と修正しました。

"projects": {"string-utility": {"root": "",
      "sourceRoot": "client",

angular/core, angular/material, rxjsのバージョンアップ

$ ng update @angular/core
$ ng update @angular/material
$ ng update rxjs

ng updateコマンドはpackage.jsonを更新するだけなので、別途node_modulesを最新化する必要があります。

$ ng update rxjs
UPDATE package.json (2795 bytes)

import { Observable } from 'rxjs/Observable'; がコンパイルエラー

rxjs v6には破壊的変更が入ったため、以下のようにimportを変更する必要があります。

import{ Observable }from'rxjs/Observable';import{ map }from'rxjs/operators/map';import"rxjs/add/observable/of";import{ Observable,of}from'rxjs';import{ map }from'rxjs/operators';

このimport変更をしたくない方は、「rxjs-compat」を追加する事でimportの変更無しに済ませる事ができます。

$ yarn add rxjs-compat

TS2322: Type 'Store<string>' is not assignable to type 'Observable<string>'.

ERROR in client/app/app.component.ts(71,5): error TS2322: Type 'Store<boolean>' is not assignable to type'Observable<boolean>'.

こんなコンパイルエラーが発生しました。以下のようにrxjs-compatを追加する事でコンパイルエラーが解消されました。

$ yarn add rxjs-compat

この記事を書いているタイミングでは@ngrx/storeのバージョンは「v5.2.0」で、v6はまだ出ておらず、v6.0.0-beta2が最新でした。

恐らくこれがv6になれば、rxjs-compatは不要になるのではないかと思われます。

Unknown option: '--extractCss'

Unknown option: '--sourcemaps'

これ系のエラーですが、今までngコマンドのオプションはキャメルケースだったのですが、v6からケバブケースに変更されたようです。

cliオプションで指定する場合はケバブケースで、angular.jsonで指定する場合はキャメルケースになったようです。

cliangular.json
--optimizationoptimization
--output-hashingoutputHashing
--source-mapsourceMap
--extract-cssextractCss
--named-chunksnamedChunks
--aotaot
--extract-licensesextractLicenses
--vendor-chunkvendorChunk
--build-optimizerbuildOptimizer
--service-workerserviceWorker
--ngsw-config-pathngswConfigPath

何も考えずにマイグレーションするとangular.json側にも設定があるし、package.jsonのngコマンドにも設定があるという重複状態になるので、どちらかに寄せた方が良さそうですね。

Configuration 'true' could not be found in project 'string-utility'.

$ ng build -vc=true
Configuration 'true' could not be found in project 'string-utility'.
Error: Configuration 'true' could not be found in project 'string-utility'.
    at Architect.getBuilderConfiguration (/Users/tree/go/src/string-utility/node_modules/@angular-devkit/architect/src/architect.js:106:23)
    at runSingleTarget (/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:138:89)
    at MergeMapSubscriber.rxjs_2.from.pipe.operators_1.concatMap.project (/Users/tree/go/src/string-utility/node_modules/@angular/cli/models/architect-command.js:143:127)
    at MergeMapSubscriber._tryNext (/Users/tree/go/src/string-utility/node_modules/rxjs/internal/operators/mergeMap.js:122:27)
    at MergeMapSubscriber._next (/Users/tree/go/src/string-utility/node_modules/rxjs/internal/operators/mergeMap.js:112:18)
    at MergeMapSubscriber.Subscriber.next (/Users/tree/go/src/string-utility/node_modules/rxjs/internal/Subscriber.js:103:18)
    at Observable._subscribe (/Users/tree/go/src/string-utility/node_modules/rxjs/internal/util/subscribeToArray.js:9:20)
    at Observable._trySubscribe (/Users/tree/go/src/string-utility/node_modules/rxjs/internal/Observable.js:177:25)
    at Observable.subscribe (/Users/tree/go/src/string-utility/node_modules/rxjs/internal/Observable.js:162:93)
    at MergeMapOperator.call (/Users/tree/go/src/string-utility/node_modules/rxjs/internal/operators/mergeMap.js:87:23)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

これ、ちょっと意味不明なエラーだったのですが、以下のように -vc を削除すると解消しました。(-vcって何のオプションだっけ・・・)

$ ng build --prod-vc=true↓
$ ng build --prod

xxx.bundle.js is not found

詳細は調べきれていませんが、今まで ng build --prod すると、xxx.bundle.js と出力されていたのが、 xxx.js となったようです。

vendor.bundle.js is not found

ng serveした時はあるのに、ng build --prodすると無くなってました。

以下のようにproduction設定のvendorChunkをtrueにするとvendor.jsが生成されるようになります。

{"projects": {"string-utility": {"architect": {"build": {"configurations": {"production": {"vendorChunk": true}}}}}}}

inline.bundle.js is not found

これは inline.js にすればよいのではなく、ファイル名が「inline.js」から「runtime.js」に変わったようです。

この辺をまとめると以下となります。

<scripttype="text/javascript"src="inline.bundle.js"></script><scripttype="text/javascript"src="polyfills.bundle.js"></script><scripttype="text/javascript"src="styles.bundle.js"></script><scripttype="text/javascript"src="vendor.bundle.js"></script><scripttype="text/javascript"src="main.js"></script><scripttype="text/javascript"src="runtime.js"></script><scripttype="text/javascript"src="polyfills.js"></script><scripttype="text/javascript"src="styles.js"></script><scripttype="text/javascript"src="vendor.js"></script><scripttype="text/javascript"src="main.js"></script>

GAE/Node.js Standard Environmentでスピンアップからテキストが返るまでの速度をゆる〜く確認する

$
0
0

appengineに待望のNode.js standard environmentが正式リリースされたので、早速計測してみました〜


f:id:treeapps:20170917230836p:plain

前回のあらすじ

www.bunkei-programmer.net

GAE/Javaは8になってもやはり初動が遅かったのだ。というかGAE/Java1.7とほとんど違いは無かったのだ。

www.bunkei-programmer.net

GAE/Goは(javaと比較すると)くっそ速かったのだ。

そしてGAE/Node.jsが出たので計測してみたのだ。

計測の仕方

前回のGAE/Java8の時と同様に、インスタンスを削除して、必ずスピンアップが発生する状態で計測します。
www.bunkei-programmer.net

環境

リージョンasia-northeast1
RuntimeNode.js v8
FrameworkExpress v4.16.3

計測に使用したソースコード

以下の公式サンプル(最小のhello world)を使用して計測しました。
github.com

javascriptは1ファイルで、以下のように非常に短いコードです。

'use strict';

// [START app]const express = require('express');

const app = express();

app.get('/', (req, res) => {
  res.status(200).send('Hello, world!').end();
});

// Start the serverconst PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`App listening on port ${PORT}`);
  console.log('Press Ctrl+C to quit.');
});
// [END app]

スピンアップしてフレームワークを初期化してテキストが返るまでの速度

前回やったのと同じです。インスタンスが0のスピンダウンした状態からリクエストを受け、実際にフレームワークがテキストを返し終えるまでの時間を計測します。

計測1回目

f:id:treeapps:20180613230902p:plain

計測2回目

f:id:treeapps:20180613230915p:plain

計測3回目

f:id:treeapps:20180613230925p:plain

計測4回目

f:id:treeapps:20180613230935p:plain

計測5回目

f:id:treeapps:20180613230946p:plain

結果と平均値まとめ

1回目2回目3回目4回目5回目平均値
0.809秒0.572秒0.539秒0.755秒0.583秒0.6516秒

結果は平均 0.6516秒となりました。

では今まで計測した、GAE/Gonalg + gin、GAE/Java7 + servletと比較してみましょう。

ランタイム1回目2回目3回目4回目5回目平均値
java8 + spark fw5.16秒5.33秒5.58秒5.07秒5.24秒5.276秒
java7 + servlet3.65秒3.40秒3.82秒3.78秒3.94秒3.718秒
go + gin0.506秒0.377秒0.601秒0.501秒0.494秒0.495秒
node.js + express0.809秒0.572秒0.539秒0.755秒0.583秒0.6516秒

golangが最速ではありますが、node.jsはgolangより0.2〜0.3秒程度遅いだけなので、十分なパフォーマンスが出ているように見えますね。

雑感

思ったよりGAE/Node.jsが高速で良かったです!

GAE/Node.jsは最近Googleが発表された軽量コンテナ環境のgVisorが使われているそうですが、特に遅い事もなく、非常に良い印象です。

www.publickey1.jp


現状私は以下の2サイトをGAEで運用していて、両サイトともGAE/Golangです。
www.tree-maps.com
www.string-utility.com

Golangなので、react・angularのSSRは捨てていました。しかし今回待望のNode.jsがリリースされたので、順次Node.jsに移行していこうと思っています。

平均0.2〜0.3秒程度遅くなるかもしれませんが、SSR可能になる事を考慮すると、その遅延は大した事ないかな?と想像しています。

いつ移行できるかまだ解りませんが、移行したら何らかの形で告知しようと思います!

macでredis serverをインストールせずにredis-cliのバイナリをビルドしてプロジェクトに組み込む

$
0
0

実は単独のバイナリとしてプロジェクトに組み込めます。


f:id:treeapps:20180418114029p:plain

redis-cliですが、macの場合はbrew install redis-cli等と試みてもインストールできません。

代わりにbrew install redisでredisサーバごとインストールすると、その中の1機能としてredis-cliが付いてきます。しかし、昨今のredis環境はほぼdocker化しており、ローカルにredisサーバ等はインストールしたくありません。

そこで、できるだけ標準の仕組みでredis-cliのバイナリを生成、プロジェクトに組み込めないか?と思い、実際に試してみたところ、できました。

環境

OSmacOS high sierra
redisバージョンv4

redis-cliのバイナリ生成手順

redisのソースコードをダウンロードする

公式ページからダウンロードURLをコピってきます。
Redis

$ wget http://download.redis.io/releases/redis-4.0.10.tar.gz
$ tar zxf redis-4.0.10.tar.gz

makeする

$ cd redis-4.0.10
$ make

makeが成功すると、srcディレクトリに各種バイナリが生成されます。

$ ll
total 584
-rw-r--r--    1 tree  wheel   158K  61320:0200-RELEASENOTES
-rw-r--r--    1 tree  wheel    53B  61320:02 BUGS
-rw-r--r--    1 tree  wheel   1.8K  61320:02 CONTRIBUTING
-rw-r--r--    1 tree  wheel   1.5K  61320:02 COPYING
-rw-r--r--    1 tree  wheel    11B  61320:02 INSTALL
-rw-r--r--    1 tree  wheel   4.1K  61320:02 MANIFESTO
-rw-r--r--    1 tree  wheel   151B  61320:02 Makefile
-rw-r--r--    1 tree  wheel    20K  61320:02 README.md
drwxr-xr-x   12 tree  wheel   408B  8201:46 deps
-rw-r--r--    1 tree  wheel    57K  61320:02 redis.conf
-rwxr-xr-x    1 tree  wheel   271B  61320:02 runtest
-rwxr-xr-x    1 tree  wheel   280B  61320:02 runtest-cluster
-rwxr-xr-x    1 tree  wheel   281B  61320:02 runtest-sentinel
-rw-r--r--    1 tree  wheel   7.4K  61320:02 sentinel.conf
drwxr-xr-x  198 tree  wheel   6.6K  8201:47 src ← ここ!
drwxr-xr-x   12 tree  wheel   408B  61320:02 tests
drwxr-xr-x   19 tree  wheel   646B  61320:02 utils

redis-cliバイナリだけ抽出する

redis-cliは以下に生成されています。

$ ll src/redis-cli
-rwxr-xr-x  1 tree  wheel   169K  8201:47 redis-cli

例えばこれを/tmpにコピーします。

$ cp src/redis-cli /tmp

ではこのバイナリを直接実行し、動作確認してみます。

$ ./redis-cli --version
redis-cli 4.0.10
$ ./redis-cli
127.0.0.1:6379> keys *
1)"TEST::com.example.api.repository.database.PrefectureRepository.findAll"2)"spring:session:sessions:825e37c4-fae1-4b86-affb-8b5b3398e9c5"

どうやらちゃんと動くようです。これでいちいちローカルにredisサーバをインストールしなくて済みますね。

しかもこのバイナリをプロジェクトに組み込んでしまえば、gitで全員に同じバージョンのredis-cliを配布する事も可能になります。

IT業界でプライベートで勉強するかどうかの理想と現実

$
0
0

例のシャッチョさんの第二弾的な記事が賑わっていたので、見てみました〜

f:id:treeapps:20170817135400p:plain

axia.co.jp

こちらの記事になります。

雑な概要

シャッチョさんの会社には昔、プライベートでは勉強しないAさんというエンジニアがいて、周りや後輩にどんどんスキル負けする事に悩んでいました。

その会社ではプライベートの勉強は強制しておらず、自分の人生なんだからプライベートの時間を家族との時間に費やすのも自由だぜ、と主張しています。

シャッチョさんは悩んでいる彼に対して「プライベートで勉強するしかないんじゃないか?」と言うと、「絶対に勉強したくないでござる」と返ってきましたとさ。めでたしめでたし。


んでんで、前回の記事同様に賛否両論が有るらしく、ブコメは光速で1000超えしているわけです。

多かった主張

文章を読んでいない・理解していないブコメ以外を見て、個人的に以下の意見が目に付きました。

  • 業務時間内に勉強させろ。
  • 業務時間内に勉強してはいけない。
  • Aさんは御社にマッチしていない。採用ミス。
  • 給料上げれば解決。
  • 勉強が必要なのはIT業界に限らない。
  • 努力しても報われないからやらない。

相反する意見も出てました。

この中で「今の日本では努力しても報われない」という意見ですが、最近僕らのKeisuke Honda選手が以下の発言をされています。


成功は保証はされないが、成長は保証されてる」との事です。

今回の記事に言い換えると「プライベートの勉強は給料アップに繋がる保証はないが、Aさんの成長は保証されてる」です。

Aさんは必敗する環境に悩む

Aさんの環境

  • 入社時に数ヶ月の研修有り。業務に必要な事なら、業務時間内に研修を受けさせて貰っている。
  • プライベートに勉強しないでござる

周りの人の環境

  • 入社時に数ヶ月の研修有り。業務に必要な事なら、業務時間内に研修を受けさせて貰っている。
  • プライベートに勉強するでござる


さてさて、Aさんと周りの人達は、業務時間内では全く同じ環境ですが、業務時間外では異なる環境です。

で、この環境でAさんは「負ける事に悩んでいる」と言いますが、プライベートでは絶対勉強したくありません。

環境差がプライベートの勉強有無しか無いので、どう考えてもその悩みを解消させる事ができず、シャッチョさんは困ったわけです。

誤解を生んでいる部分について

シャッチョさんの会社では残業0を掲げているのですが、この「残業0」と「プライベートで勉強」という矛盾する単語が誤解を生んでいるように見えました。

この誤解を生むポイントはAさんがプライベートで勉強する人との差に悩んでいるという点にあります。

もし悩んでいなければ、この件は「Aさんは人生の中で仕事よりプライベートに重きを置いたので、それで差が生まれました」となるので、シャッチョさんはAさんに対してエンジニアに向いてないなんて言わず、この記事を投稿する事もしなかったでしょう

しかしAさんは悩んでいて、Aさんと他の人の環境差がプライベートの勉強有無しか無いので、じゃあプライベートで勉強するしか無いじゃん?となり、無い物ねだりをするAさんに対してエンジニアに向いてないと発言しているのだと推測しています。

悩むくらいなら勉強しよう、それでも勉強したくないのだから、もうエンジニアに向いてないのでは?という事です。




この話はこれ以上話す事が無いのでここまでとします。

続いて、ブコメの中で「業務時間内外で勉強する」件について意見があったので、私も個人的な理想を書いてみます。

理想

業務時間内にどんどん勉強しよう

タスクが回せるのであれば、余った時間を好きな勉強に費やしましょう。

タスクが回せない(納期に間に合わない)状況で勉強の時間を割く事をOKとするかは、会社やマネージャーに要相談です。

また、最初から必ず業務時間内に勉強時間が確保できるスケジュールを顧客が容認できるなら、それがベストです。

業務時間外に勉強するという事

プライベートに勉強する事は一切強制しませんし、一切マイナス評価しません。会社の標準的な評価が正当に行われます。

ただし、プライベートに勉強した人のスキルが向上し、その努力が会社の利益に繋がった場合、プラス評価します

このプラス評価によって、プライベートに勉強した人と、勉強しなかった人には差が付きます。

現実

業務時間内に勉強する事への抵抗勢力

業務時間内に勉強するとは何事だ!」という意見の方がおり、その論理は「勉強は会社の業務内容に含まれていないから」というケースが多いです。

業務時間内に勉強させたからといって、それが会社への利益に繋がらないのであれば、会社は拒否反応を起こします。

確かに一理あります。

この意見について、私は以下のように思っています。

  • 業務時間とプライベートはキッチリ分け、自分の人生(プライベートに何をするか)を自分で決めた方が幸せである。
  • 業務時間内に勉強する事で、結果として会社の利益に繋がるのであれば、やらせた方がよい。
  • 新技術・新手法の研究等は、必ず良い結果が生まれるわけではないので、仮に何の成果も出なかったとしても許容する。

業務時間外に勉強する事

勉強辛い。超辛い。

はい。

人間は本当に好きな分野以外を勉強すると、強いストレスを感じ、やる気を失います。

この業界は近年細分化が進みすぎて、分野の数が増えていってます。すると、特定の言語で特定のプログラムを書く事は好きでも、そこから一歩でも外れるとやりたくなくなります。分野が増えているせいで、好きではない分野も自然に増えていき、やる気を失う機会も増えていきます。

なので、最初は意欲を持って勉強できますが、自分の好きな分野から外れる事を経験すると、一気に全体的に意欲を失い、いつしかプライベートで勉強する事自体をやめる(諦める)という人が増えていきます。

気になる分野の書籍を読んでも、好きな部分の章はあっという間に読了するのに、やや苦手・嫌いな部分の章が現れると、途端に拒否反応を示し、そこで読む事を終了してしまう事もあるのではないでしょうか。

それほど些細な事でモチベーションを喪失してしまう程、勉強というものは難しいと私は考えています。

会社の利益に繋がらない勉強

例えば今の会社では全文検索エンジンを勉強しても全く意味が無く、たとえプライベートで勉強しても、おちんぎんが増える事はありません。

しかし、全文検索エンジンを必要としている他の会社に行くと、おちんぎんが増えます。

これはもう単純に需要と供給の話なので、需要が無い場所で「努力したのにおちんぎん増えない!」と憤っても仕方ないのです。今の会社でその需要を生み出す・生み出して貰うか、需要がある会社に移動するとよいと思います。

ただし、僕らのKeisuke Honda選手の言葉通り、自分自身の成長には繋がるので、今すぐに評価されなくても、結果的に転職がしやすくなったり、新たな概念を理解し覚える事で、今書いているプログラムをより良く書く能力が付く可能性があります。

「報われないから努力しない!」という姿勢はそれらの機会の逃すので、(別の会社等で)プラス評価される機会を損失するので、個人的には勿体無いとは思いますね。

エンジニアに向いている・向いていない

エンジニアの向き・不向きについては正直全く解りません。私はまだ若輩なので「これができたら向いている」「これができないのは向いていない」を定義付ける自信は無いです。

実際、プライベートで勉強をする人が向いているかというとそうではなく、業務時間内の調査・実践だけで実力者になる方も大勢います。逆に、プライベートで勉強をしているのに、勉強の仕方が問題でなかなか実にならない方もいます。

中には最初から勉強好きな人しかこの業界に入ってはならない!等と過激な発言をされる方もいますが、前述のように実践だけ実力をつけてしまう方(恐らく元々素養があるか努力の仕方が上手い人)もいるので、わざわざ門戸を狭くして視野を狭め、色々な可能性を自ら排除してしまうのは勿体ないのではと思いますね。

また、給料据え置きでいいから勉強したくない!というのは全然有りで、その場合は技術要素が変化しにくく継続して仕事が有る、所謂長寿の保守案件オススメで、実際そういう方は沢山います。新しい事をやりたい人が集まる会社では誰もそういった仕事をやりたがらないので、実は意外と需要が有ります。

ただ、cobolの例があるとはいえ10年間同じ仕事をし続ける事が可能かは解らないので、5年単位とかで勉強はしておいた方が将来安心はできると思います。

勉強は超辛く継続する事が超難しい

大部分の人間は怠惰であり、勉強が嫌いです。

かなり極端な例ですが、仮に

業務時間内に好きなだけ勉強してよいぞ。アーロンチェア・40インチディスプレイ・Threadripper 32コアCPU・メモリ64G・シーケンシャルread 3,400MB/s SSD・勉強用のawsアカウント、無料の食堂、全部用意してやるぞ。

と言われたとします。

で、「あなたは本当に勉強できますか?

勉強ってそんな簡単にできて、そんな簡単に継続できるものなのか?という疑問がどうしても拭えません。↑のスーパースペックの環境を与えられても尚、勉強をする事ができない人が多いと思ってます。

給料が上がると勉強できる?

「おちんぎんを増やせば解決理論」ですが、「今まで業務時間内やプライベートで勉強していない人が、おちんぎんが増えたら勉強するようになるの?」という疑問が湧きます。

おちんぎんによって勉強の辛さを克服できる強き人ってそんなに多いでしょうか?

極端な例ですが、仮にお金を持っている企業が「月給100万円やるから、業務時間内に勉強し、それに見合う結果を出せ」みたいな事を言われて、本当に勉強して、本当に結果が出せるでしょうか。一部の元々素養がある人以外は脱落するのではないかと思います。

そういった事を考えると、おちんぎんを今より増やしたからといって、本当に勉強する事ができる人(勉強するフリをしたりせず知識を増やす)は増えにくいのでは?と思ってしまうわけです。

※ おちんぎんを上げる必要は無い、というお話ではありません。

雑感

シャッチョさんの記事のブコメを見ていて、もうとにかく

勉強はそんな簡単にできないし継続できません!

と感じました。

そんな簡単に勉強するモチベーションが噴出するなら、みんな勉強しまくって日本の経済が急回復してるよ!、と思ってしまいました。

大部分の人は、興味を持って勉強をするのは若い間だけで、ある程度年を取ると環境の変化やモチベーションの低下によって勉強をやめてしまいます。ネット上を見ると頻繁に勉強をして継続している方が目立ちますが、実社会では勉強しない人、勉強をやめてしまった人、勉強の頻度が非常に低い人、の方が割合として多いように感じます。

なので、どちらかというと勉強を始めて且つ継続するにはどうしたらいいか?等を考えていかないといけないと思います。まずその勉強の意欲が無ければ、たとえが業務時間内の勉強が許可されたとしても、エア勉強をしてしまったり、ソリティアしてるだけだったりすると思います。

今勉強を楽しいと感じていて、それを継続できている方がいれば、それはとても素晴らしい事です。これからもその意欲を失わず、周りの人に勉強の楽しい部分や継続するモチベーションの保ち方等を伝授していって欲しいですね。


ちなみに私は、興味のある分野の書籍を購入 → ちょっと読む → 面白いー! → 今日は仕事で疲れたから読まない → 積む、を繰り返してます・・・。更に勉強意欲に非常にムラがあるので、一時的に特定の分野に興味を持って一気に書籍を読み、その実践としてサイトを開発・公開する時もあれば、無気力に何もしない日が続く事もあります。勉強意欲・モチベーションの維持は本当に難しいものです。

macでchromeやsafariで真っ白なページが表示される事があるのはESETのWebアクセス保護が原因かも

$
0
0

君が犯人だったか〜

f:id:treeapps:20170829002500p:plain


何がきっかけなのか解りませんが、最近急にGoogle ChromeやSafariで真っ白なページが表示されるようになりました。

少し調べてみて原因の特定ができたので、メモしておきます。

起きている現象

  • Google Chrome・Safari・Firefoxの3種類のブラウザで真っ白なページが表示される「場合」がある。確実に真っ白になるわけではないが、リロードし続けると「何故か」表示される事がある。
  • キャッシュを全クリアしても解決しない。
  • Adblock Plusアドオンを入れている。
  • シークレットモードで表示しても真っ白になるので、どうやらアドオンが原因ではないようだ。

こういう状況でした。

シークレットモードにすると全てのアドオンが使用できなくなるのですが、シークレットモードでも真っ白なので、アドオンが原因である可能性は無くなりました。

もう完全にデータがぶっ壊れたかな?と思いましたが、全てのブラウザのデータが一斉に破損する事は少し考えにくく、色々調べてみた結果、どうやらESETセキュリティのWebアクセス保護が原因なのは確定という事がわかりました。

ではさっそくWebアクセス保護の問題の設定を解消していきましょう。

Webアクセス保護は無効にしない

Webアクセス保護を無効にすると、以下のように警告が表示されて嫌なので、今回は保護対象を変更する事で対応します。

f:id:treeapps:20181204011431p:plain

Webアクセス保護しつつ真っ白ページを解消する

ESETを起動し、左サイドナビの「設定」をクリックし、右ペインの「Webとメール」をクリックします。

f:id:treeapps:20181204011509p:plain


続いて、Webアクセス保護の「設定」ボタンをクリックします。

f:id:treeapps:20181204011523p:plain


HTTPプロトコルで使用するポートを「80,8080,3128,443」から「8080,3128」に変更します。変更後「すべて表示する」ボタンをクリックします。
80はhttpの事で、443はhttpsの事です。

f:id:treeapps:20181204011535p:plain


すると保存するかどうか聞かれるので、保存します。これで設定完了です!

f:id:treeapps:20181204011605p:plain

雑感

http(80)とhttps(443)をWebアクセス保護から外すという事は、実質Webアクセス保護しないと同じになってしまいますが、真っ白なページが表示されるよりはマシだと思われます。

真っ白なページが表示されるのは恐らく広告が原因だと想像していますが、はっきりした原因が解らないので、今回はとりあえず保護しない事で対応しました。

もっといい方法が有る可能性もあるので、もし情報をお持ちの方がいれば是非教えて下さい!

pythonのpipでnpmのnode_modulesのようにローカルインストールして実行する

$
0
0

少しトリッキーなので、忘れない内に手順をまとめておきます〜

f:id:treeapps:20180424130724p:plain

サーバ上でpythonのpipでインストールしたモジュールを動かしたいのだけれど、なんとグローバルのpip installが禁止されている・・・!!というかそもそも権限が与えられなくてインストールができない・・・!!!

等という地獄の環境の場合、npmのように、ローカルインストールできれば簡単に解決するのに、と思いますよね。pythonでも可能なのですが、知ってないと中々できるものではないので、まとめておきます。

環境

  • pythonがインストール済みである。
  • pipがインストール済みである。

バージョンを問わず、上記がインストールされている事が前提になります。

ローカルインストールって?

普通に「pip install fabric」などとすると、以下のような全アカウント共通で使用する、所謂グローバルな場所にインストールされてしまいます。

/Library/Python/2.7/site-packages
/Library/Python/2.6/site-packages

やりたいのは↑ではなく、pythonのディレクトリに依存しない、自由な場所にsite-packagesを配置したいのです。

という事で早速やっていきましょう。

ローカルインストール手順

pip installに「-t」オプションを付けてインストールする

ここではfabricをインストールしてみます。

pip install -t site-packages fabric

site-packagesというフォルダ名は別の名前でもいいのですが、ここはnpmで言うところのnode_modulesに相当するので、素直にsite-packagesとした方が全員に通じる名称になると思います。

実行する.pyを用意する

なんでもいいのですが、今回はfabricを例としてい挙げたので、fabricを実行するものを用意してみます。以下を「fabfile.py」として保存します。

#coding:utf-8from fabric import task
import shlex, subprocess

@taskdeftask1(c):
  subprocess.Popen("hostname")

.pyを実行する.shを用意する

pythonを直接実行するのではなく、シェルスクリプトを経由して実行します。

その際に特定の環境変数と、PATHにバイナリへのパスを追加する事で、pipのコマンドを実行する事が可能になります。以下を「run.sh」として保存します。

#!/bin/shcurrentDir=$(echo$(cd $(dirname $0)&& pwd))PYTHON_SITE_PACKAGES=${currentDir}/site-packages

# site-packagesの位置を一時的に変更するexport PYTHONPATH=$PYTHONPATH:${PYTHON_SITE_PACKAGES}# pipでローカルインストールしたバイナリへのパスを通すexport PATH=${PYTHON_SITE_PACKAGES}/bin:$PATH# ↑でパスを通したのでsite-packages/bin配下のバイナリが実行できるようになる
fab task1

これでローカルにインストールしたfabricのfabコマンドを実行する事が可能になります。

最終的なディレクトリ構成

tree:test tree$ tree . -L 2
.
├── fabfile.py
├── run.sh
└── site-packages
    ├── PyNaCl-1.3.0.dist-info
  ・・・略・・・
    ├── bin
  ・・・略・・・
    └── six.pyc

バイナリは以下のように配置されます。ここへパスを通せばいつものコマンドが実行できるわけです。

tree:test tree$ ll site-packages/bin/
total 24
-rwxr-xr-x  1 tree  staff   256B 121722:54 fab
-rwxr-xr-x  1 tree  staff   256B 121722:54 inv
-rwxr-xr-x  1 tree  staff   256B 121722:54 invoke

先程run.shで実行したfabコマンドは↑これです。

雑感

AWS Lambdaでpythonランタイムを選択した際に、lamdaに標準インストールされていないモジュールを追加インストールする場合もローカルインストールする事になるので、この辺は覚えておくと色々と便利そうです。lambdaの件は以前以下の記事を書いたので、合わせてご覧下さい。

www.bunkei-programmer.net

また、冒頭で述べたようにセキュリティが非常に厳しい環境で、自分のアカウントのオーナー・グループ権限内に閉じて実行する事ができ、不要になった際にフォルダごと削除すれば消えてくれるのは安心感があります。

更に、グルーバルを汚染せずに済むので色々な人に易しくなりますね。

spring-cloud-starter-awsがローカル環境でエラーになる場合の最低限の対応

$
0
0

全然解らない。俺たちは雰囲気でspring-cloud-starter-awsを使っている・・・・

f:id:treeapps:20171029033317p:plain

javaやkotlinでorg.springframework.cloud:spring-cloud-starter-awsを使う事がまあまああったりします。これを使うとawsのリージョンを自動取得してくれたり、awsフレンドリーな状態でawsのサービスを扱う事ができるようになります。

環境

  • java or kotlin
  • Spring boot(v1系、v2系のどらでも起きます)
  • build.gradleやpom.xmlにorg.springframework.cloud:spring-cloud-starter-awsを設定している
  • application.ymlにはspring-cloud-starter-awsの設定を何も記述していない

エラーを解消した最終的なローカル環境向けのapplication.yml設定

いきなり答えを記述すると、EC2以外の環境(ローカル環境等)の場合は、以下になります。

cloud:aws:stack: # CloudFormationのstack名を自動収集しないauto:falseregion: # EC2のmetadataを自動収集しないauto:falsestatic: ap-northeast-1


では、どんなエラーが起きて、どう解決していくかを見ていきます。

EC2からStack Nameを自動取得できないよエラー

spring-cloud-starter-awsをapplication.ymlの設定無しにそのまま使うと、ローカル環境で以下のエラーが発生します。

2019-01-2523:45:29,397 ERROR [main][org.springframework.boot.SpringApplication:858] Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.cloud.aws.core.env.ResourceIdResolver.BEAN_NAME': Invocation of init method failed; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'stackResourceRegistryFactoryBean' defined in class path resource [org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean]: Factory method 'stackResourceRegistryFactoryBean' threw exception; nested exception is java.lang.IllegalArgumentException: No valid instance id defined
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1745)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:576)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:846)
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:863)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:546)
	at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:67)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
	at com.example.admin.AdminApplicationKt.main(AdminApplication.kt:21)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'stackResourceRegistryFactoryBean' defined in class path resource [org/springframework/cloud/aws/autoconfigure/context/ContextStackAutoConfiguration.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean]: Factory method 'stackResourceRegistryFactoryBean' threw exception; nested exception is java.lang.IllegalArgumentException: No valid instance id defined
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:627)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:607)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1288)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1127)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:602)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeansOfType(DefaultListableBeanFactory.java:590)
	at org.springframework.cloud.aws.core.env.StackResourceRegistryDetectingResourceIdResolver.findSingleOptionalStackResourceRegistry(StackResourceRegistryDetectingResourceIdResolver.java:81)
	at org.springframework.cloud.aws.core.env.StackResourceRegistryDetectingResourceIdResolver.afterPropertiesSet(StackResourceRegistryDetectingResourceIdResolver.java:77)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1804)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1741)
	... 16 common frames omitted
Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.cloud.aws.core.env.stack.config.StackResourceRegistryFactoryBean]: Factory method 'stackResourceRegistryFactoryBean' threw exception; nested exception is java.lang.IllegalArgumentException: No valid instance id defined
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:185)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:622)
	... 31 common frames omitted
Caused by: java.lang.IllegalArgumentException: No valid instance id defined
	at org.springframework.util.Assert.notNull(Assert.java:198)
	at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.autoDetectStackName(AutoDetectingStackNameProvider.java:75)
	at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.afterPropertiesSet(AutoDetectingStackNameProvider.java:62)
	at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.<init>(AutoDetectingStackNameProvider.java:52)
	at org.springframework.cloud.aws.core.env.stack.config.AutoDetectingStackNameProvider.<init>(AutoDetectingStackNameProvider.java:56)
	at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration.stackResourceRegistryFactoryBean(ContextStackAutoConfiguration.java:71)
	at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration$$EnhancerBySpringCGLIB$$be0ef810.CGLIB$stackResourceRegistryFactoryBean$0(<generated>)
	at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration$$EnhancerBySpringCGLIB$$be0ef810$$FastClassBySpringCGLIB$$d3106a9b.invoke(<generated>)
	at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
	at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:363)
	at org.springframework.cloud.aws.autoconfigure.context.ContextStackAutoConfiguration$$EnhancerBySpringCGLIB$$be0ef810.stackResourceRegistryFactoryBean(<generated>)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.base/java.lang.reflect.Method.invoke(Method.java:566)
	at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
	... 32 common frames omitted

StackTraceの中の「No valid instance id defined」「stack.config」辺りを見ると何が起きているか解りますね。

No valid instance id defined

StackTraceからコードを追うと、以下のAssertに引っかかった事が解ります。

/** * Represents a stack name provider that automatically detects the current stack name based on the amazon elastic cloud * environment. */publicclass AutoDetectingStackNameProvider implements StackNameProvider, InitializingBean {

    privatefinal AmazonCloudFormation amazonCloudFormationClient;
    privatefinal AmazonEC2 amazonEc2Client;
    privatefinal InstanceIdProvider instanceIdProvider;
    ・・・略・・・

    private String autoDetectStackName(String instanceId) {

        Assert.notNull(instanceId, "No valid instance id defined");
        ・・・略・・・
        returnnull;
    }

順に上から見ていくと、InitializingBeanを継承していて、これはSpring起動時の初期化処理をするもので、更にクラス名が「Auto」と自動で取得をしにいこうとするものであると解ります。
続いてフィールドに「amazonCloudFormationClient」「amazonEc2Client」「instanceIdProvider」があります。この変数名から連想される事は、CloudFormationのスタックで使うEC2のインスタンスIDを自動で取得しようとするクラスである、と解ります。

cloud.spring.io

If the application runs inside a stack (because the underlying EC2 instance has been bootstrapped within the stack), then Spring Cloud AWS will automatically detect the stack and resolve all resources from the stack. Application developers can use all the logical names from the stack template to interact with the services. In the example below, the database resource is configured using a CloudFormation template, defining a logical name for the database instance.

いろいろ書いてますが、要は環境がどこであれ、Spring起動時にEC2の情報を自動で収集するからな〜、と言っています。ではここで「ローカル環境はEC2じゃないからEC2のインスタンスIDなんて無いんだが」という疑問にぶち当たり、案の定インスタンスIDを取得しようとして100%エラーが発生するわけです。

ここで重要なのは、Stack、つまり「CloudFormationが」という部分です。

という事は、Spring boot(spring-cloud-starter-aws)のCloudFormationの設定で、EC2情報を自動収集しないようにすれば解決しそうです。

EC2の情報を自動収集させないようにする

https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_cloudformation_configuration_in_spring_boot

propertyexampledescription
cloud.aws.stack.autotrueEnables the automatic stack name detection for the application.

Spring boot向けの設定に「cloud.aws.stack.auto」が有って初期値は「true」で、自動的にスタック名を収集する設定との事です。つまりこれをfalseにすれば自動収集が止まるわけです。

application.ymlの設定

f:id:treeapps:20190126201739p:plain
EC2でない環境でCloudFormationのStack名を収集しないapplication.ymlの設定の仕方

EC2でない環境のapplication.ymlが↑この設定にすれば自動収集は止まり、エラーにならなくなります。↑の画像では黄色く警告が出ており、コード補完もできないですが、ちゃんと設定自体は存在して有効になるのでご安心下さい。


しかし、これで終わりではありません・・・・

EC2からリージョン名が自動収集できないよエラー

cloud:aws:stack: # CloudFormationのstack名を自動収集しないauto:false

この設定でSpring bootを起動すると、今度は以下のエラーが起きます。

2019-01-2620:20:11,455 ERROR [main][org.springframework.boot.SpringApplication:858] Application run failed
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'sendMailService' defined in file [/Users/tree/github/kotlin-spring-boot-jooq-liquibase-thymeleaf-example/base/out/production/classes/com/example/base/service/SendMailService.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'javaMailSender' defined in class path resource [org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class]: Unsatisfied dependency expressed through method 'javaMailSender' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'amazonSimpleEmailService' defined in class path resource [org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:769)
    at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:218)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1308)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1154)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:846)
    at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:863)
    at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:546)
    at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:67)
    at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
    at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
    at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
    at com.example.admin.AdminApplicationKt.main(AdminApplication.kt:21)
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'javaMailSender' defined in class path resource [org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class]: Unsatisfied dependency expressed through method 'javaMailSender' parameter 0; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'amazonSimpleEmailService' defined in class path resource [org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:769)
    at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:509)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1288)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1127)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:538)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1244)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1164)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760)
    ... 19 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'amazonSimpleEmailService' defined in class path resource [org/springframework/cloud/aws/autoconfigure/mail/MailSenderAutoConfiguration.class]: Invocation of init method failed; nested exception is java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1745)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:576)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:498)
    at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320)
    at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
    at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:277)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1244)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1164)
    at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:857)
    at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:760)
    ... 33 common frames omitted
Caused by: java.lang.IllegalStateException: There is no EC2 meta data available, because the application is not running in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance
    at org.springframework.util.Assert.state(Assert.java:73)
    at org.springframework.cloud.aws.core.region.Ec2MetadataRegionProvider.getRegion(Ec2MetadataRegionProvider.java:39)
    at org.springframework.cloud.aws.core.config.AmazonWebserviceClientFactoryBean.createInstance(AmazonWebserviceClientFactoryBean.java:92)
    at org.springframework.cloud.aws.core.config.AmazonWebserviceClientFactoryBean.createInstance(AmazonWebserviceClientFactoryBean.java:44)
    at org.springframework.beans.factory.config.AbstractFactoryBean.afterPropertiesSet(AbstractFactoryBean.java:142)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1804)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1741)
    ... 44 common frames omitted

There is no EC2 meta data available

前述同様、対象コードを見てみます。

/** * {@link org.springframework.cloud.aws.core.region.RegionProvider} implementation that dynamically retrieves the * region with the EC2 meta-data. This implementation allows application to run against their region without any * further configuration. */publicclass Ec2MetadataRegionProvider implements RegionProvider {

    @Overridepublic Region getRegion() {
        Region currentRegion = getCurrentRegion();
        Assert.state(currentRegion != null, "There is no EC2 meta data available, because the application is not running " +
                "in the EC2 environment. Region detection is only possible if the application is running on a EC2 instance");
        return currentRegion;
    }

    protected Region getCurrentRegion() {
        try {
            InstanceInfo instanceInfo = EC2MetadataUtils.getInstanceInfo();
            return instanceInfo != null&& instanceInfo.getRegion() != null ? RegionUtils.getRegion(instanceInfo.getRegion()) : null;
        } catch (AmazonClientException e) {
            returnnull;
        }

    }
}

EC2のメタデータからインスタンス情報(Region等)を取得しようとするものですね。当然ローカル環境はEC2でないので、EC2インスタンス情報を取得しようとして100%エラーになるわけです。

という事は、Spring boot(spring-cloud-starter-aws)のリージョン設定で、EC2情報を自動収集しないようにすれば解決しそうです。

EC2メタデータを自動収集させないようにする

https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_configuring_region

propertyexampledescription
cloud.aws.region.autotrueEnables automatic region detection based on the EC2 meta data service
cloud.aws.region.staticeu-west-1Configures a static region for the application. Possible regions are (currently) us-east-1, us-west-1, us-west-2, eu-west-1, eu-central-1, ap-southeast-1, ap-southeast-1, ap-northeast-1, sa-east-1, cn-north-1 and any custom region configured with own region meta data

これです。どうやら初期値は自動収集になっているようです。ではapplication.ymlを以下のように設定して起動します。

application.ymlの設定
cloud:aws:stack: # CloudFormationのstack名を自動収集しないauto:falseregion: # EC2のmetadataを自動収集しないauto:false

この設定に変更して再起動すると、今度は以下のエラーが出ます。

2019-01-2620:36:46,619 ERROR [main][org.springframework.boot.SpringApplication:858] Application run failed
java.lang.IllegalArgumentException: Region must be manually configured or autoDetect enabled
	at org.springframework.cloud.aws.context.config.support.ContextConfigurationUtils.registerRegionProvider(ContextConfigurationUtils.java:65)
	at org.springframework.cloud.aws.autoconfigure.context.ContextRegionProviderAutoConfiguration$Registrar.registerBeanDefinitions(ContextRegionProviderAutoConfiguration.java:72)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.lambda$loadBeanDefinitionsFromRegistrars$1(ConfigurationClassBeanDefinitionReader.java:364)
	at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:684)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsFromRegistrars(ConfigurationClassBeanDefinitionReader.java:363)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:145)
	at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:117)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:327)
	at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:232)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:275)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:95)
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:691)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:528)
	at org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext.refresh(ReactiveWebServerApplicationContext.java:67)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:316)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248)
	at com.example.admin.AdminApplicationKt.main(AdminApplication.kt:21)

Region must be manually configured or autoDetect enabled(cloud.aws.region.staticで設定するか、cloud.aws.region.autoで自動設定しろよな)というエラーです。

両パラメータには相関があって、自動収集を停止するならばcloud.aws.region.staticを設定しないといけないのです。「static」は「自動収集しないで静的(決め打ち)で設定する」という意味合いですね。

ローカル環境なので、以下のようにして、自動収集しないで決め打ちにします。

cloud:aws:stack: # CloudFormationのstack名を自動収集しないauto:falseregion: # EC2のmetadataを自動収集しないauto:falsestatic: ap-northeast-1

これで無事起動します!

EC2以外の環境では上記のように自動収集をやめる設定とし、EC2環境の場合は自動収集設定でもよさそうですね。

AmazonSESは東京リージョンに対応してないんだが

cloud:aws:stack: # CloudFormationのstack名を自動収集しないauto:falseregion: # EC2のmetadataを自動収集しないauto:falsestatic: ap-northeast-1

これだと、東京リージョンに対応していないAmazonSESの場合、エラーになりそうですよね。

Amazon SES is not available in all regions of the Amazon Web Services cloud. Therefore an application hosted and operated in a region that does not support the mail service will produce an error while using the mail service. Therefore the region must be overridden for the mail sender configuration. The example below shows a typical combination of a region (EU-CENTRAL-1) that does not provide an SES service where the client is overridden to use a valid region (EU-WEST-1).

[>https://cloud.spring.io/spring-cloud-aws/spring-cloud-aws.html#_configuring_regions>]

「SESは全リージョンに対応してるわけではないから、cloud.aws.region.staticの設定をしても未対応リージョンだとエラーが出るから、別の方法でリージョンを上書きしてくれよな。」と言っています。サンプルとしてaws-config.xmlを使う例が載っています。↓こういうやつです。
https://github.com/eugenp/tutorials/blob/master/spring-cloud/spring-cloud-aws/src/main/resources/aws-config.xml
ここにSESだけ異なるリージョンを書いてもいいぞー、という事だそうです。

しかし、実際の開発では、local〜staging環境はオレゴンリージョン、production環境はバージニアリージョン、等と利用環境を分けて、負荷分散やバウンスレートの分けをキッチリする事が多いです。するとaws-config.xmlを環境毎に上書きしないといけないビルド設定が必要になるので、これは嫌です。

折角application.ymlが環境毎に設定を柔軟に変更できる機構があるのですから、それで変更したいですね。

AmazonSESのリージョンをソースコード側で変更する

f:id:treeapps:20190126212524p:plain

いきなりDeprecatedの洗礼を浴びます。amazonSimpleEmailService.setRegionは非推奨なので「AwsClientBuilder.setRegion(String)」を使えとのことです。

AwsClientBuilderはabstractクラスであり、実際はAwsClientBuilderを継承しているAmazonSimpleEmailServiceClientBuilderを使えという事になります。
github.com

で、AmazonSimpleEmailServiceClientBuilderの使い方は公式サイトにズバリそのものがあるので、省略します。
docs.aws.amazon.com

おまけ:ローカル開発時にML宛にメールを送信したくないんだが!

ローカルで開発していて、例えばhtmlメールを実装中だとします。テンプレートエンジンでif文やらfor文やらをゴリゴリ実装していくわけです。他にもtoやccが正常に分岐できているかもテストしたいですよね。

するとローカル環境で何度も実際にメール送信してテストしたくなりますね。メール送信先には社内MLが設定される事が多いと思いますが、テストメールが何十通・何百通も送信されると、MLを受信してる人がウザい・全くメールを見なくなる、という事になる可能性があります。

特にバッチで大量のhtmlメールを送信するテストをしたい時等は困ってしまいます。実装をミスって無限ループでSESにメールを送信してしまい、莫大な金額が請求されたりとか。

それを回避するため、私はローカル環境の場合はMailCatcher(仮想SMTPサーバ)を利用します。
mailcatcher.me

MailCatcherは仮想メールサーバなのですが、メールホストをMailCatcherに向けて送信すると、MailCatcherの画面上ではメールの受信フォルダに受信されますが、実際のTO・CC・BCCにメールが送信される事はありません。つまり、自分のローカル環境に完全に閉じたメールサーバが構築できるという優れものです。勿論テキストメール・htmlメール・マルチパートメールにも対応しています。

しかしこれをローカル環境に直接インストールしたくありません。そこでdockerです。ではdockerでMailCatcherを起動できるようにし、更にSpring bootからMailCatcherに向けてメールが送信できるようにしてみましょう。

docker-compose.yml

version:'3'services:localMailServer:image: schickling/mailcatcher:latest
    ports:- 1080:1080
    - 25:1025
    - 465:1025
    - 587:1025

port=1080はブラウザで表示する画面用のポート、
port=25は一般的なメールサーバのポート、
port=465,587はgmailのポート、
となります。

application.yml

mail:host:"localhost"protocol:"smtp"port:25default-encoding: UTF-8
    test-connection:trueproperties:mail:smtp:timeout:10000connectiontimeout:10000writetimeout:10000

実際にMailCatcherにメールを送信するGIFアニメ

Spring bootでAPIサーバを起動し、http://localhost:8080/send-multipart-mail/をGETリクエストするとマルチパートメール(テキストメールとhtmlメールが合体したメール)をMailCatcherに送信し、テキスト・htmlの両方が確認でき、メールのソースも確認できる事をGIFアニメにしてみました。

f:id:treeapps:20190126222601g:plain

勿論TO・CC・BCCに実際にはメールは送信されていません。ここで受信したメールの履歴は、dockerを停止すると全削除されます。(Volume Mountすれば永続化もできると思います)

ここでは日本語は使っていませんが、ちゃんと日本語も化けずに表示できます。これをローカル環境で用意しておくと、メール送信に関する実装で精神の安定を向上させる事ができます。

雑感

今回書いたやり方で困るのは、ローカル環境ではSMTPでDockerのMailCatcherに向けて送信したいが、ローカル以外の環境ではSESに送信したい場合、コードの共通化ができるのか?という点です。

docs.aws.amazon.com

↑このやり方はSMTPプロトコルではなく、SESのAPIで送信します。SMTPで送信する場合はJavaMailSender等を使いますよね。しかしAmazonSES APIだとAmazonSimpleEmailServiceClientBuilderを使い、両者で大分コードが変わります。これを共通化するいい方法があるのかがまだ解っていません。

一応AmazonSESもSMTPプロトコルでメール送信する事ができますが、物凄く遅いです。1回のメール送信で2秒くらいかかる程遅いです。よくあるユースケースとして、メール送信時にユーザーと管理者に異なるメールを同時に送信したい場合があります。その時同期処理で送信すると2秒×2通=4秒もかかります。2通をasync/awaitで非同期送信しても、2秒より速くはなりません。なので、できればSESでSMTPプロトコルは使わず、高速なAPI(AmazonSimpleEmailServiceClientBuilder)を使いたいです。

もしSMTP(ローカル環境専用)とSES APIを共存しつつソースコードの共通化をするいい方法をご存知の方がいれば是非教えて下さい!

jOOQ v3.11にアップデートした際にgradle-jooq-pluginで出るエラーの対応

$
0
0

jOOQのパッケージ構成が少し変わりますよ〜

f:id:treeapps:20190128100247p:plain

先日gradleでjOOQの依存を以下のように更新しました。

依存名アップデート前アップデート後
jOOQv3.10.7v3.11.9
gradle-jooq-pluginv2.0.11v3.0.2

gradle-jooq-pluginがメジャーバージョンアップしてたので絶対動かないだろうなあと思ったら、案の定動きませんでした。

ちょっと修正しただけで動いたので、メモっておきます。

jOOQをv3.11系にした際に起きるエラー

tree:database tree$ ./generate-jooq.sh

> Task :generateExampleJooqSchemaSource FAILED
128, 20199:59:52午前 org.jooq.tools.JooqLogger info
情報: Initialising properties  : /Users/tree/github/kotlin-spring-boot-jooq-liquibase-thymeleaf-example/database/build/tmp/generateExampleJooqSchemaSource/config.xml
128, 20199:59:53午前 org.jooq.tools.JooqLogger warn
警告: Type not found           : Your configured org.jooq.util type was not found.
Do note that in jOOQ 3.11, jOOQ-meta and jOOQ-codegen packages have been renamed. New package names are:
- org.jooq.meta
- org.jooq.meta.extensions
- org.jooq.codegen
- org.jooq.codegen.maven
See https://github.com/jOOQ/jOOQ/issues/7419for details
128, 20199:59:53午前 org.jooq.tools.JooqLogger error
重大: Cannot read /Users/tree/github/kotlin-spring-boot-jooq-liquibase-thymeleaf-example/database/build/tmp/generateExampleJooqSchemaSource/config.xml. Error : Your configured org.jooq.util type was not found.
Do note that in jOOQ 3.11, jOOQ-meta and jOOQ-codegen packages have been renamed. New package names are:
- org.jooq.meta
- org.jooq.meta.extensions
- org.jooq.codegen
- org.jooq.codegen.maven
See https://github.com/jOOQ/jOOQ/issues/7419for details
java.lang.ClassNotFoundException: Your configured org.jooq.util type was not found.
Do note that in jOOQ 3.11, jOOQ-meta and jOOQ-codegen packages have been renamed. New package names are:
- org.jooq.meta
- org.jooq.meta.extensions
- org.jooq.codegen
- org.jooq.codegen.maven
See https://github.com/jOOQ/jOOQ/issues/7419for details
        at org.jooq.codegen.GenerationTool.loadClass(GenerationTool.java:857)
        at org.jooq.codegen.GenerationTool.run(GenerationTool.java:331)
        at org.jooq.codegen.GenerationTool.generate(GenerationTool.java:222)
        at org.jooq.codegen.GenerationTool.main(GenerationTool.java:194)
Caused by: java.lang.ClassNotFoundException: org.jooq.util.DefaultGenerator
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        at org.jooq.codegen.GenerationTool.loadClass(GenerationTool.java:821)
        ... 3 more

エラーに丁寧に何が起きているか書いてますね。これは親切です。

対応

変更前パッケージ変更後パッケージ
org.jooq.util.DefaultGeneratororg.jooq.codegen.DefaultGenerator
org.jooq.util.DefaultGeneratorStrategyorg.jooq.codegen.DefaultGeneratorStrategy
org.jooq.util.mysql.MySQLDatabaseorg.jooq.meta.mysql.MySQLDatabase

上記のようにパッケージがutilからcodegen・metaと細分化されました。org.jooq.meta.mysql.MySQLDatabase は postgres等に適宜変更しましょう。

この修正だけで基本的に動かくかと思います。


java8から11にアップデートした際にgradle-jooq-pluginで出るエラーの対応

$
0
0

依存の修正だけで動きますよ〜


f:id:treeapps:20190128100247p:plain

jOOQに限った話ではないと思いますが、javaを8から11等にアップデートするとjaxbが動かない問題がjOOQでも発生するので、エラーに対応してみます。

java v11化した際に起きるエラー

tree:database tree$ ./gradlew -b jooq.gradle --stacktrace clean gen
> Task :generateExampleJooqSchemaSource FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':generateExampleJooqSchemaSource'.
> javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath.
   - with linked exception:
  [java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory]

* Try:
Run with --info or --debug option to get more log output. Run with --scan to get full insights.

* Exception is:
org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':generateExampleJooqSchemaSource'.
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:95)
        at org.gradle.api.internal.tasks.execution.ResolveTaskOutputCachingStateExecuter.execute(ResolveTaskOutputCachingStateExecuter.java:91)
        at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:57)
        at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:119)
        at org.gradle.api.internal.tasks.execution.ResolvePreviousStateExecuter.execute(ResolvePreviousStateExecuter.java:43)
        at org.gradle.api.internal.tasks.execution.CleanupStaleOutputsExecuter.execute(CleanupStaleOutputsExecuter.java:93)
        at org.gradle.api.internal.tasks.execution.FinalizePropertiesTaskExecuter.execute(FinalizePropertiesTaskExecuter.java:45)
        at org.gradle.api.internal.tasks.execution.ResolveTaskArtifactStateTaskExecuter.execute(ResolveTaskArtifactStateTaskExecuter.java:94)
        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:56)
        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:55)
        at org.gradle.api.internal.tasks.execution.CatchExceptionTaskExecuter.execute(CatchExceptionTaskExecuter.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.executeTask(EventFiringTaskExecuter.java:67)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:52)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter$1.call(EventFiringTaskExecuter.java:49)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:315)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$CallableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:305)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:175)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.call(DefaultBuildOperationExecutor.java:101)
        at org.gradle.internal.operations.DelegatingBuildOperationExecutor.call(DelegatingBuildOperationExecutor.java:36)
        at org.gradle.api.internal.tasks.execution.EventFiringTaskExecuter.execute(EventFiringTaskExecuter.java:49)
        at org.gradle.execution.plan.LocalTaskNodeExecutor.execute(LocalTaskNodeExecutor.java:43)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:355)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$InvokeNodeExecutorsAction.execute(DefaultTaskExecutionGraph.java:343)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:336)
        at org.gradle.execution.taskgraph.DefaultTaskExecutionGraph$BuildOperationAwareExecutionAction.execute(DefaultTaskExecutionGraph.java:322)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker$1.execute(DefaultPlanExecutor.java:134)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker$1.execute(DefaultPlanExecutor.java:129)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.execute(DefaultPlanExecutor.java:202)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.executeNextNode(DefaultPlanExecutor.java:193)
        at org.gradle.execution.plan.DefaultPlanExecutor$ExecutorWorker.run(DefaultPlanExecutor.java:129)
        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:63)
        at org.gradle.internal.concurrent.ManagedExecutorImpl$1.run(ManagedExecutorImpl.java:46)
        at org.gradle.internal.concurrent.ThreadFactoryImpl$ManagedThreadRunnable.run(ThreadFactoryImpl.java:55)
Caused by: org.gradle.internal.UncheckedException: javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath.
 - with linked exception:
[java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory]
        at org.gradle.internal.UncheckedException.throwAsUncheckedException(UncheckedException.java:67)
        at org.gradle.internal.UncheckedException.throwAsUncheckedException(UncheckedException.java:41)
        at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:76)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.doExecute(StandardTaskAction.java:48)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:41)
        at org.gradle.api.internal.project.taskfactory.StandardTaskAction.execute(StandardTaskAction.java:28)
        at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:704)
        at org.gradle.api.internal.AbstractTask$TaskActionWrapper.execute(AbstractTask.java:671)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$2.run(ExecuteActionsTaskExecuter.java:284)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:301)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor$RunnableBuildOperationWorker.execute(DefaultBuildOperationExecutor.java:293)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.execute(DefaultBuildOperationExecutor.java:175)
        at org.gradle.internal.operations.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
        at org.gradle.internal.operations.DelegatingBuildOperationExecutor.run(DelegatingBuildOperationExecutor.java:31)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:273)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:258)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.access$200(ExecuteActionsTaskExecuter.java:67)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter$TaskExecution.execute(ExecuteActionsTaskExecuter.java:145)
        at org.gradle.internal.execution.impl.steps.ExecuteStep.execute(ExecuteStep.java:49)
        at org.gradle.internal.execution.impl.steps.CancelExecutionStep.execute(CancelExecutionStep.java:34)
        at org.gradle.internal.execution.impl.steps.TimeoutStep.executeWithoutTimeout(TimeoutStep.java:69)
        at org.gradle.internal.execution.impl.steps.TimeoutStep.execute(TimeoutStep.java:49)
        at org.gradle.internal.execution.impl.steps.CatchExceptionStep.execute(CatchExceptionStep.java:33)
        at org.gradle.internal.execution.impl.steps.CreateOutputsStep.execute(CreateOutputsStep.java:50)
        at org.gradle.internal.execution.impl.steps.SnapshotOutputStep.execute(SnapshotOutputStep.java:43)
        at org.gradle.internal.execution.impl.steps.SnapshotOutputStep.execute(SnapshotOutputStep.java:29)
        at org.gradle.internal.execution.impl.steps.CacheStep.executeWithoutCache(CacheStep.java:134)
        at org.gradle.internal.execution.impl.steps.CacheStep.lambda$execute$3(CacheStep.java:83)
        at org.gradle.internal.execution.impl.steps.CacheStep.execute(CacheStep.java:82)
        at org.gradle.internal.execution.impl.steps.CacheStep.execute(CacheStep.java:36)
        at org.gradle.internal.execution.impl.steps.PrepareCachingStep.execute(PrepareCachingStep.java:33)
        at org.gradle.internal.execution.impl.steps.StoreSnapshotsStep.execute(StoreSnapshotsStep.java:38)
        at org.gradle.internal.execution.impl.steps.StoreSnapshotsStep.execute(StoreSnapshotsStep.java:23)
        at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.executeBecause(SkipUpToDateStep.java:96)
        at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.lambda$execute$0(SkipUpToDateStep.java:89)
        at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:52)
        at org.gradle.internal.execution.impl.steps.SkipUpToDateStep.execute(SkipUpToDateStep.java:36)
        at org.gradle.internal.execution.impl.DefaultWorkExecutor.execute(DefaultWorkExecutor.java:34)
        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:91)
        ... 32 more
Caused by: javax.xml.bind.JAXBException: Implementation of JAXB-API has not been found on module path or classpath.
 - with linked exception:
[java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at nu.studer.gradle.jooq.JooqTask$1.writeConfiguration(JooqTask.groovy:125)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at nu.studer.gradle.jooq.JooqTask$1.execute(JooqTask.groovy:118)
        at nu.studer.gradle.jooq.JooqTask$1.execute(JooqTask.groovy)
        at org.gradle.api.internal.file.DefaultFileOperations.javaexec(DefaultFileOperations.java:226)
        at org.gradle.api.internal.project.DefaultProject.javaexec(DefaultProject.java:1103)
        at org.gradle.api.internal.ProcessOperations$javaexec.call(Unknown Source)
        at nu.studer.gradle.jooq.JooqTask.executeJooq(JooqTask.groovy:103)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at nu.studer.gradle.jooq.JooqTask.generate(JooqTask.groovy:96)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
        at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:73)
        ... 68 more
Caused by: java.lang.ClassNotFoundException: com.sun.xml.internal.bind.v2.ContextFactory
        ... 89 more


* Get more help at https://help.gradle.org

BUILD FAILED in 0s
2 actionable tasks: 2 executed


com.sun.xml.internal.bind.v2.ContextFactory が ClassNotFound とのことです。

対応

f:id:treeapps:20190128104240p:plain

見にくいので一部抜粋で修正点を確認します。

修正前

buildscript {
  dependencies {
    classpath "nu.studer:gradle-jooq-plugin:3.0.2"
  }
}

jooq {
  dependencies {
    jooqRuntime "org.jooq:jooq-codegen:3.11.9"
    jooqRuntime "org.jooq:jooq-meta:3.11.9"
    jooqRuntime "org.jooq:jooq:3.11.9"
    jooqRuntime "mysql:mysql-connector-java:8.0.14"
  }
}

修正後

buildscript {
  dependencies {
    classpath "nu.studer:gradle-jooq-plugin:3.0.2"// ↓追加
    classpath "org.glassfish.jaxb:jaxb-core:2.3.0.1"
    classpath "org.glassfish.jaxb:jaxb-runtime:2.3.2"
  }
}

jooq {
  dependencies {
    jooqRuntime "org.jooq:jooq-codegen:3.11.9"
    jooqRuntime "org.jooq:jooq-meta:3.11.9"
    jooqRuntime "org.jooq:jooq:3.11.9"
    jooqRuntime "mysql:mysql-connector-java:8.0.14"// ↓追加
    jooqRuntime "javax.xml.bind:jaxb-api:2.3.0"
    jooqRuntime "javax.activation:javax.activation-api:1.2.0"
    jooqRuntime "org.glassfish.jaxb:jaxb-core:2.3.0.1"
    jooqRuntime "org.glassfish.jaxb:jaxb-runtime:2.3.2"
  }
}

こんな感じに依存を追加すると、ターミナルから動くようになります。

Spring BootでMySQLのconnectorjのreplication schemeで参照クエリをreaderに向ける

$
0
0

リードレプリカを活用しろ警察に怒られる前に、connectorjのマルチポスト機能を学ばなくては・・・!!


f:id:treeapps:20180802010416p:plain

知らないのは私だけだと思いますが、実はconnectorjにはマルチホストのコネクションを管理する機能が内蔵されています。

マルチホストコネクションの種類

connectorjには実は以下が標準機能として存在しています。

  • Server Failover
  • Client-Side Failover when using the X Protocol
  • Load Balancing with Connector/J
  • Master/Slave Replication with Connector/J

これらの詳細については以下の公式ドキュメントをご覧下さい。
dev.mysql.com

この中で今回「Master/Slave Replication with Connector/J」をピックアップします。

Master/Slave Replication with Connector/Jってどんな機能?

名前でネタバレしてますが、マスターの場合とスレーブの場合で接続先を変更する事ができる設定方法です。

実際のユースケースとしては以下のようになります。

マスター(更新クエリ)の場合はホストAに接続
スレーブ(参照クエリ)の場合はホストBに接続

Replicationと書かれていますが、どちらかというとレプリケーション云々よりも、更新クエリと参照クエリを別々の接続先に投げてくれる、と考えるといいと思います。

何に使うの?

データベースを使う場合、特別な事情(どうしてもUDFを使いたい等)が無い限り、マネージドサービス(Amazon RDS等)を使う場合がほとんどです。

オンプレ時代とは異なり、リードレプリカ機能を使えば、職人技が必要なレプリケーション設定の必要も無く、参照専用インスタンスを画面をポチポチするだけで用意できるようになりました。更に、Amazon Auroraの登場により、writerエンドポイント・readerエンドポイントという便利なものが登場しました。

しかし、です。いくら参照専用インスタンスが有る、readerエンドポイントが有る、といってもそこに参照クエリを流さないと、完全に宝の持ち腐れになります。そこで今回誰もが使うconnectorjの標準機能を使ってそれを実現します。

MySQL RouterやMariaDB MaxScale等のミドルウェアを使わずにとりあえずreaderを活用できるので、ミドルウェアのSPOFやメンテを考えなくていいので便利ですね。機能は両者には勝てませんけどね。

Spring bootで簡単に参照クエリのみを振り分ける

今回はkotlin + Spring bootで参照クエリを参照専用ホストに流してみます。

設定自体はたった2つで完了します。

1個目の設定:application.ymlのdatasouce設定

spring:datasource:url:"jdbc:mysql:replication://127.0.0.1:3306,127.0.0.1:3307,127.0.0.1:3308/work"username: worker
    password: worker
    driverClassName: com.mysql.cj.jdbc.Driver

2個目の設定:@Transactional(readOnly = true)

@Transactional(readOnly = true)
fun findReaderHost(): HostModel = hostRepository.findOne()

以上で参照クエリが参照専用ホストに流れます。たったこれだけです。

では両者の設定についてもう少し確認してみましょう。

spring.datasource.url

dev.mysql.com

公式ドキュメントを見れば解りますが、一応説明すると超ざっくり以下のように設定します。

url:"jdbc:mysql:replication://${更新+参照ホスト}:${port},${参照専用ホスト1}:${port},${参照専用ホスト2}:${port}/${DB名}"

ホスト+portの組み合わせを半角カンマ区切りで設定します。master(更新+参照が可能なホスト)が1個目固定、2個目移行は全部slave(参照専用ホスト)です。

RDS+リードレプリカの場合は1個づつ設定、Auroraの場合はwriterエンドポイントを1個目、readerエンドポイントを2個目に設定するだけですね。

@Transactional(readOnly = true)

AOPで@Serviceを付けたクラスに無条件に@Transactionalを設定するケースもありますが、今回@Transactionalを手動で定義している場合の話になります。

この設定でポイントは「readOnly = true」の部分です。このreadOnlyがtrueなら、自動的にspring.datasource.urlのslaveの方に接続が流れるのです。

masterに流れるのかslaveに流れるのかは本当にここだけで決まります。insert・update・deleteだからmasterに接続されるわけではありません。(やっちゃダメですが)更新クエリをslaveにも流せますし、参照クエリをmasterに流す事もできます。

Spring bootのソースコードは追ってませんが、恐らく@Transactional(readOnly = false)を設定すると、内部で Connection.setReadOnly(true) といった事を自動でしてくれているのだと思われます。なので、他のフレームワークでは異なる指定が必要になるか、そもそもできない可能性があるかもしれません。

レプリケーションの遅延について

Auroraでの話になりますが、ストレージを共有しているAuroraでさえ、レプリケーション遅延、所謂レプリカラグは発生します。

Aurora レプリカは、Aurora DB クラスター内の独立したエンドポイントであり、読み取りオペレーションのスケーリングと可用性の向上に最適です。最大 15 個の Aurora レプリカを、1 つの AWS リージョンの中で DB クラスターが処理するアベイラビリティーゾーン全体に分散できます。DB クラスターボリュームは DB クラスターのデータの複数のコピーで構成されます。ただし、クラスターボリュームのデータは、DB クラスターのプライマリインスタンスおよび Aurora レプリカの 1 つの論理ボリュームとして表されます。

この結果、すべての Aurora レプリカは、最短のレプリカラグでクエリの結果として同じデータを返します。レプリカラグは、通常はプライマリインスタンスが更新を書き込んだ後、100 ミリ秒未満です。レプリカラグは、データベースの変更レートによって異なります。つまり、データベースに対して大量の書き込みオペレーションが発生している間、レプリカラグが増加することがあります。

https://docs.aws.amazon.com/ja_jp/AmazonRDS/latest/AuroraUserGuide/Aurora.Replication.html

readerエンドポイント使用時はこのレプリカラグを考慮する必要があります。ここで先程の「参照クエリをmasterに流す事もできます」の部分に注目します。

例えば、「参照クエリ ->更新クエリ ->参照クエリ」といった参照と更新が入り乱れ、すぐに更新結果が欲しい場合、readerにクエリを投げてもレプリカラグで最新データが取得できない可能性があります。ここで「敢えて参照クエリをmasterに流す」事で、レプリカラグを気にせず最新データを取得できるわけです。

auroraの100 ミリ秒未満のレプリカラグを許容できるケースは、可能な限りreaderに参照クエリを流せるといいですね。その判断がちょっと難しい可能性はありますが。

GIFアニメで動きを見てみる

実は例によって事前に専用プロジェクトを用意しています。
github.com

writerを1台、readerを2台用意しており、挙動を解りやすするため敢えてレプリケーションをせず、初期データで異なる値をinsertしておき、サーバ毎に異なる値が返るようにしています。

readerに参照クエリを投げる例

docker起動時にhostというテーブルを生成し、writerには「writer1」、reader1には「reader1」、reader2には「reader2」という値をinsertしてあります。

以下の例は、@Transactional(readOnly = true)を設定し、hostテーブルをselectした結果を連続して取得しています。writerにクエリが投げられず、readerの何れかのサーバに接続される事を確認できます。

f:id:treeapps:20190210223855g:plain

writerに参照クエリを投げる例

以下の例は、@Transactionalを設定し、hostテーブルをselectした結果を連続して取得しています。readerにクエリが投げられず、必ずwriterに接続される事を確認できます。(@TransactionalのreadOnlyは初期値がfalseなのでこの場合readOnly = falseが自動的に設定された事になります)

f:id:treeapps:20190210224424g:plain




readerのGIFアニメで気づいたと思いますが、バランシングがラウンドロビンではなくランダムです。

このバランシング設定をドキュメントから見つける事ができませんでしたが、jdbc:mysql:loadbalanceと同じであれば、デフォルト値はrandomですね。

ha.loadBalanceStrategy

If using a load-balanced connection to connect to SQL nodes in a MySQL Cluster/NDB configuration (by using the URL prefix "jdbc:mysql:loadbalance://"), which load balancing algorithm should the driver use: (1) "random" - the driver will pick a random host for each request. This tends to work better than round-robin, as the randomness will somewhat account for spreading loads where requests vary in response time, while round-robin can sometimes lead to overloaded nodes if there are variations in response times across the workload. (2) "bestResponseTime" - the driver will route the request to the host that had the best response time for the previous transaction. (3) "serverAffinity" - the driver initially attempts to enforce server affinity while still respecting and benefiting from the fault tolerance aspects of the load-balancing implementation. The server affinity ordered list is provided using the property 'serverAffinityOrder'. If none of the servers listed in the affinity list is responsive, the driver then refers to the "random" strategy to proceed with choosing the next server.

Default: random

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-reference-configuration-properties.html

google翻訳すると以下となります。

(1) "random" - ドライバはリクエストごとにランダムなホストを選びます。

これはラウンドロビンよりもうまくいく傾向があります。ランダムさはリクエストの応答時間が異なる負荷を分散させるためですが、ラウンドロビンはワークロード全体の応答時間にばらつきがあるとノードが過負荷になることがあります。

という事で、ラウンドロビンよりランダムの方がいい結果になるぞ〜、との事で初期値がランダムなようですね。

雑感

最近会社でAuroraはごくごく普通に使用されるようになりましたが、readerを活用できているプロジェクトが少ないなあと思って調べてみると、connectorjにこんな機能が有る事を知りました。MySQL RouterとMax Scaleは知っているのにこっちを知らないという・・・autoReconnect等のオプション設定は気にするのに、スキーム節は気にした事が無かったです。

大規模プロジェクトの場合、このクエリの場合はwriterに、あのクエリの場合はreaderに、という判断ができる人とできない人が入り乱れますが、その場合どうしよう?といったルール決めが必要になりそうですね。

闇雲に使用すると、記事投稿後に何故か記事件数が増えないバグが有る〜、画面操作が速すぎると何故か最新データが取得できないんです〜、みたいな事になるので、それらの対応策を事前に検討した方が良さそうです。

MySQLでlimit offset専用一時テーブルを簡単に生成してページネーション処理を高速化する

$
0
0

変な挙動が有るので、今回はそこをピックアップします〜

f:id:treeapps:20180418131549p:plain

MySQLのlimit offset問題と言えば、誰もが知る有名な性能劣化問題です。

今回はcreate table select構文を利用し、primary keyとauto_incrementの強奪現象を利用して、簡単にこの問題を解消してみようと思います。

環境

検証する前に環境を確認しておきます。

MySQLv5.7(docker上のMySQLです)
CPUCore i9 9900k
MEM64G
ストレージSSD 1TB

MySQLのlimit offset問題

ざっくり解説しておくと、

select * from big_table limit 1000000, 1000;

こうすると一見1000件しかデータを参照しないように見えますが、実は100万件取得してから999000件を捨てているため、offset値が大きいほど遅くなるというものです。

よくある解決法

解決方法は大体皆答えが出ていて、その多くは

select * from big_table where id between0and1000;
select * from big_table where id between1001and2000;
select * from big_table where id between2001and3000;

という、betweenでインデックスを効かせる形ですね

ここでありがちな問題があります。betweenするには綺麗に歯抜けのないid列的なものが必要だが、勿論そんなものは無いし既存テーブルを変更したくない場合、どうするかです。

mysqlのcreate tableは実はselectもできてしまうので、今回はこれを利用してさっくりやってみます。

create table select構文

dev.mysql.com

使い方は公式サイト↑参照ですが、最小構成で言うと以下のような事が可能というだけです。

createtable fuga select * from hoge;

こうすると、hogeの構造データをコピーしたfugaというテーブルを作成する事ができます。(完全コピーではなくbtree index等はコピーされません)

部分合成

実はこの構文、意外と柔軟な記述ができて、以下のようにhogeの一部だけを持ってきた部分合成も可能です。

合成元となるテーブルは以下の定義とします。

droptableifexists hoge;
createtable hoge(
  hoge_id bigint unsigned notnull auto_increment comment'ID',
  hoge_name varchar(100) notnullcomment'名称',
  primary key(hoge_id)
) engine=innodb charset=utf8mb4;

これからfugaテーブルを作るのですが、hoge_idだけが欲しいので、以下のようにします。

droptableifexists fuga;
create temporary table fuga(
  fuga_id bigint unsigned notnull auto_increment comment'fuga ID',
  primary key(fuga_id)
) engine=innodb charset=utf8mb4 comment'合成!!'select
    hoge_id
  from
    hoge
;

こうすると、

mysql> show createtable fuga \G
*************************** 1. row ***************************
       Table: fuga
CreateTable: CREATE TEMPORARY TABLE `fuga` (
  `fuga_id` bigint(20) unsigned NOTNULL AUTO_INCREMENT COMMENT'fuga ID',
  `hoge_id` bigint(20) unsigned NOTNULLDEFAULT'0'COMMENT'ID',
  PRIMARY KEY (`fuga_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合成!!'

という列を持ったfugaテーブルが作成されます。


待て待て待て!なんか腑に落ちない事があるんだが!?

はい。では順番にその疑問と回答を見ていきましょう。

このfugaテーブルは一体?

これは、limit offset問題をbetweenで解決したいのですが、元のテーブル定義には手を入れたくありません。誰だってそうです。

なので別途採番テーブルを作成する事で、元の定義を壊さず解決しようとしています。

え?なんでfugaテーブルを作る必要が?

必要無い場合もあります。しかし100%そうとは言い切れません。

通常auto_incrementの列は連番が採番されます。しかし、歯抜けの値が入っている場合があります。

可能性としては以下が想定されます。

  • アプリケーション側でdelete insertしてidが飛び飛びになる。
  • auto_increment_increment 設定を変更し、採番値が奇数になるようにしている。(昔レプリケーションで複数台でIDが重複しないようにサーバ1では偶数、サーバ2では奇数、なんて事をしてる事もありました)
  • データ移行で仕方なくid値を100000から始めている。

こんな事が100%無いと言い切れないですし、考えるのも面倒です。なので信頼できる採番値を自分で作ってしまおう、というのがfugaテーブルです。

ん?temporary table?

しれっと「create temporary table fuga」と記述しました。

temporaryを付けると、そのセッションのみ閲覧・操作可能な一時テーブルを作成できます。

テーブルの作成時に TEMPORARY キーワードを使用できます。TEMPORARY テーブルは現在のセッションにのみ表示され、そのセッションが閉じられると自動的に削除されます。つまり、2 つの異なるセッションが同じ一時テーブル名を使用することができ、互いに、または同じ名前の既存の TEMPORARY 以外のテーブルと競合することはありません。(既存のテーブルは、一時テーブルが削除されるまで非表示になります。)一時テーブルを作成するには、CREATE TEMPORARY TABLES 権限が必要です。

https://dev.mysql.com/doc/refman/5.6/ja/create-table.html

create temporary tables権限が必要にはなりますが、他の人に見られずに、更に一時テーブルの削除漏れを無くす事ができるという機能です。

特に一時テーブルの削除漏れは結構致命的で、後になって「何この変なテーブル?削除していいの?」となり「解らん。怖くて消せないよねそれ・・」みたいな負債が貯まる要因にもなるので、極力付けた方がいいと思われます。

パフォーマンスも通常のテーブルと比較して特別大きな劣化は見られません。(そもそもID列が2列あるだけのテーブルですし)

待って、hogeとfugaの両方にPKとauto_incrementあるよね?

そうです。これこそが今回の記事の主題だったのです

もう一度おさらいしてみましょう。

droptableifexists hoge;
createtable hoge(
  hoge_id bigint unsigned notnull auto_increment comment'ID',
  hoge_name varchar(100) notnullcomment'名称',
  primary key(hoge_id)
) engine=innodb charset=utf8mb4;

droptableifexists fuga;
create temporary table fuga(
  fuga_id bigint unsigned notnull auto_increment comment'fuga ID',
  primary key(fuga_id)
) engine=innodb charset=utf8mb4 comment'合成!!'select
    hoge_id
  from
    hoge
;

こうすると、以下ができます。

mysql> show createtable fuga\G
*************************** 1. row ***************************
       Table: fuga
CreateTable: CREATE TEMPORARY TABLE `fuga` (
  `fuga_id` bigint(20) unsigned NOTNULL AUTO_INCREMENT COMMENT'fuga ID',
  `hoge_id` bigint(20) unsigned NOTNULLDEFAULT'0'COMMENT'ID',
  PRIMARY KEY (`fuga_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合成!!'

結果だけ見ると、後発のfuga_idがhoge_idからauto_incrementとprimary keyを強奪している事になりますね。

一体何故こんな事が起きるのかは解っていませんが、この強奪現象がlimit offset問題に利用できるのです

PKとauto_incrementの強奪現象を利用したlimit offsetの解決

繰り返し同じ定義を記述しますがご容赦下さい。

limit offsetしたいテーブル定義とデータの入り方

以下に対して高速にlimit offsetしたいのですが、残念な事にhoge_idが奇数になってしまっています。しかも一見に綺麗に奇数になっているようで、delete insertによってガタガタの歯抜けになっています。

-- テーブルの作成
mysql> createtable hoge(
    ->   hoge_id bigint unsigned notnull auto_increment comment'ID',
    ->   hoge_name varchar(100) notnullcomment'名称',
    ->   primary key(hoge_id)
    -> ) engine=innodb charset=utf8mb4;
Query OK, 0rows affected (0.01 sec)

-- 歯抜けのテストデータを1000万件投入
mysql> load data local
    ->   infile '/tmp/test.tsv'
    ->   intotable hoge
    ->   characterset utf8
    ->   fields
    ->     terminated by'\t'
    ->     enclosed by''
    ->   lines
    ->     terminated by'\n'
    -> ;
Query OK, 10000000rows affected (25.99 sec)
Records: 10000000  Deleted: 0  Skipped: 0  Warnings: 0-- テストデータの確認
mysql> select * from hoge limit 10;
+---------+---------------------+
| hoge_id | hoge_name           |
+---------+---------------------+
|       1 | 2019-04-2100:15:23 |
|       3 | 2019-04-2100:15:23 |
|       5 | 2019-04-2100:15:23 |
|       7 | 2019-04-2100:15:23 |
|       9 | 2019-04-2100:15:23 |
|      11 | 2019-04-2100:15:23 |
|      13 | 2019-04-2100:15:23 |
|      15 | 2019-04-2100:15:23 |
|      17 | 2019-04-2100:15:23 |
|      19 | 2019-04-2100:15:23 |
+---------+---------------------+10rowsinset (0.00 sec)

別途採番テーブルを作成する

mysql> create temporary table fuga(
    ->   fuga_id bigint unsigned notnull auto_increment comment'fuga ID',
    ->   primary key(fuga_id)
    -> ) engine=innodb charset=utf8mb4 comment'合成!!'
    -> select
    ->     hoge_id
    ->   from
    ->     hoge
    -> ;
Query OK, 10000000rows affected (16.75 sec)
Records: 10000000  Duplicates: 0  Warnings: 0-- 登録されたデータを確認
mysql> select * from fuga limit 10;
+---------+---------+
| fuga_id | hoge_id |
+---------+---------+
|       1 |       1 |
|       2 |       3 |
|       3 |       5 |
|       4 |       7 |
|       5 |       9 |
|       6 |      11 |
|       7 |      13 |
|       8 |      15 |
|       9 |      17 |
|      10 |      19 |
+---------+---------+10rowsinset (0.00 sec)

fuga_idはauto_incrementなので、自動的にfuga_idにnullがinsertされて採番値が入ったようですね。ちょっと空気を読み過ぎた挙動で不安な感じはします・・・

temporaryテーブルの場合は自セッション以外からは閲覧・操作不能なので、このfuga_idが他セッションによって追加・更新・削除される可能性は0になり、絶対に信頼できる歯抜けのない採番値になります。

hogeとfugaをinner joinしてbetweenする

mysql> select
    ->     h.*
    ->   from
    ->     fuga f
    ->     inner join hoge h
    ->       on f.hoge_id = h.hoge_id
    ->   where
    ->     f.fuga_id between0and10
    -> ;
+---------+---------------------+
| hoge_id | hoge_name           |
+---------+---------------------+
|       1 | 2019-04-2100:15:23 |
|       3 | 2019-04-2100:15:23 |
|       5 | 2019-04-2100:15:23 |
|       7 | 2019-04-2100:15:23 |
|       9 | 2019-04-2100:15:23 |
|      11 | 2019-04-2100:15:23 |
|      13 | 2019-04-2100:15:23 |
|      15 | 2019-04-2100:15:23 |
|      17 | 2019-04-2100:15:23 |
|      19 | 2019-04-2100:15:23 |
+---------+---------------------+10rowsinset (0.00 sec)

こんな感じです。後はbetweenの値を足したり引いたりするだけですね!

explainしてみる

mysql> explain
    -> select
    ->     h.*
    ->   from
    ->     fuga f
    ->     inner join hoge h
    ->       on f.hoge_id = h.hoge_id
    ->   where
    ->     f.fuga_id between0and10
    -> ;
+----+-------------+-------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+
| id | select_type | table | partitions | type   | possible_keys | key     | key_len | ref                   | rows | filtered | Extra       |
+----+-------------+-------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+
|  1 | SIMPLE      | f     | NULL       | range  | PRIMARY       | PRIMARY | 8       | NULL                  |   10 |   100.00 | Usingwhere |
|  1 | SIMPLE      | h     | NULL       | eq_ref | PRIMARY       | PRIMARY | 8       | kyoritsu_db.f.hoge_id |    1 |   100.00 | NULL        |
+----+-------------+-------+------------+--------+---------------+---------+---------+-----------------------+------+----------+-------------+

keyがPRIMARYのみの最高の形になりましたね。

現実ではここからjoinして別テーブルのカラムを取得したりフラグ値の考慮等で複雑化しますが、limit offset問題は解決しています。

速度比較をしてみる

通常のlimit offset版
mysql> select * from hoge limit 9999000, 1000;
+----------+---------------------+
| hoge_id  | hoge_name           |
+----------+---------------------+
| 19998001 | 2019-04-2100:15:29 |
| 19998003 | 2019-04-2100:15:29 |
・・・略・・・
| 19999997 | 2019-04-2100:15:29 |
| 19999999 | 2019-04-2100:15:29 |
+----------+---------------------+1000rowsinset (1.65 sec)

CPUで無理やりぶん回しているので速く見えますが、それでもたった1000件の取得に 1.65secもかかっています。

between版
mysql> select
    ->     h.*
    ->   from
    ->     fuga f
    ->     inner join hoge h
    ->       on f.hoge_id = h.hoge_id
    ->   where
    ->     f.fuga_id between9999001and10000000
    -> ;
+----------+---------------------+
| hoge_id  | hoge_name           |
+----------+---------------------+
| 19998001 | 2019-04-2100:15:29 |
| 19998003 | 2019-04-2100:15:29 |
・・・略・・・
| 19999997 | 2019-04-2100:15:29 |
| 19999999 | 2019-04-2100:15:29 |
+----------+---------------------+1000rowsinset (0.00 sec)

速過ぎて 0.00secとか出てしまいました。


fugaをtemporaryテーブルにしていますが、この場合トランザクションを張りっぱなしにする必要がある(トランザクション終了時にtemporaryは消失する)ので、短いトランザクションにしたい場合は通常のcreate tableにし、必ずテーブル削除に「drop table if exists fuge;」をし、安全に削除するようにします。

雑感

記述していて思いましたが、「hoge」「fuga」だとどっちがどっちなのか解らなくなりますね・・・・失敗です。

今回の謎現象であるPKとauto_incrementの強奪現象について公式リファレンスを流し見したのですが、書いてないっぽい???ので、実に謎な挙動です。

もしかしたら将来的にこの強奪仕様がしれっと無くなる可能性があるので、そこは注意していきたいですね。

macのAutomatorでhomebrewのコマンドをFinderのサービスから実行できるようにする

$
0
0

homebrewでインストールしたコマンドをFinder上で画面ポチポチで実行可能にしてみましょう〜

f:id:treeapps:20180418114029p:plain

モチベーション

私「このファイルRARか。解凍できん・・・」
敵「App StoreでThe Unarchiverインストールすれば?」
私「RARアーカイバなんてインストールしたくない。アンインストール時にゴミが残るの嫌だし挙動をコントロールしたい。っていうか今時どんな理由があってRAR形式なんて使うんだよ・・・」
敵「うざ」
私「homebrewでインストールしたp7zipならRAR解凍できる。」
敵「何それキモ」
私「Automatorなら確かシェルスクリプト呼べるから、画面からポチポチできそう」


こんな動機です。他にも、App Storeには無いけど、homebrewには有るコマンドをFinder上でトラックパッドでポチポチ実行したいしたい場合もです。

何故ターミナルからコマンドを実行しないかというと、「いちいちターミナルでコマンド実行するの大変じゃない?ファイル名に日本語やスペースが混じってると面倒だし」という物凄くつまらない理由になります。

このつまらない願望をAutomatorなら実現できるので、やってみましょう。

RARの解凍をサービス化してFinderからポチポチできるようにする

homebrewでp7zipをインストールする

brew install p7zip

Automatorからサービスを作成する

アプリケーションからAutomatorを起動します。
f:id:treeapps:20190430031709p:plain

Automatorを起動するといきなり以下のようなダイアログが表示されます。色々表示されていますが、ここは新規作成ボタンをクリックします。

f:id:treeapps:20190430055525p:plain

新規作成ボタンをクリックすると以下のようなダイアログが表示されます。書類の種類はクイックアクションを選択し、選択ボタンをクリックします。少し解りにくいですが、サービスに登録するにはクイックアクションを選択します。

f:id:treeapps:20190430033408p:plain

選択ボタンをクリックすると以下のような画面が表示されます。いきなり大量の機能が表れて面くらいますが、虫眼鏡アイコンのテキストボックスにシェルスクリプトと入力すると、以下のように大量のメニューの中からシェルスクリプトを実行が絞り込まれます。

f:id:treeapps:20190430033809p:plain

シェルスクリプトを実行をドラッグし、ワークフローを作成するには、ここにアクションまたはファイルをドラッグしてください。というエリアにドロップします。

f:id:treeapps:20190430034118p:plain

ワークフローが受け取る現在の項目をファイルまたはフォルダに、シェルを/bin/bashに変更し、テキストエリアに以下をコピー&ペーストします。

while read file; do# ファイルではない場合はスキップif [!-f"$file"]; thencontinuefi# パスが通っていないのでフルパスで解凍する
  /usr/local/bin/unrar x "$file""`dirname $file`"done# 完了を知らせる音を鳴らす
afplay /System/Library/Sounds/Glass.aiff

※ Automatorはログインシェルを実行してくれないのでunrarをフルパスで記述しています

f:id:treeapps:20190430042744p:plain

テキストエリアに上記スクリプトを入力後、cmd + sで保存します。ここで保存した名前がそのままサービス名になります。今回は「RARを解凍」という名前にしてみました。

f:id:treeapps:20190430034925p:plain

尚、保存したファイルの実態は/Users/ユーザ名/Library/Services/RARを解凍.workflowに保存されています。

これで完成です!

では早速試してみましょう。

単数・複数のrarファイルをサービスで解凍するGIFアニメ

f:id:treeapps:20190430043827g:plain

おまけ:フォルダ毎にzip圧縮

例えば以下のように沢山のディレクトリが有るとします。

.
├── sample1
├── sample2
・・・略・・・
├── sample49
└── sample50

このフォルダを1ファイルのzipに圧縮するのではなく、フォルダ毎にzip圧縮をしたい、更にフォルダは削除してzipファイルのみ残したい場合、以下のスクリプトをAutomatorでサービス化するだけです。サービス化手順は前述と全く同じです。

while read f;doif [!-d"$f"]; thencontinueficd"${f%/*}"
  zip -0mr-b /tmp "${f##*/}".zip "${f##*/}"-x"*/.DS_Store""*/Thumbs.db"done
afplay /System/Library/Sounds/Glass.aiff

こちらは、以下の記事を参考にカスタマイズした形になります。
mattintosh.hatenablog.com

GIFアニメで挙動を確認する

f:id:treeapps:20190430053019g:plain

おまけ:画像サイズの50%化

なんでこんな機能が必要になるかというと、retinaディスプレイで画面キャプチャを取得すると、解像度の問題か、巨大な画像(2倍のサイズ)になってしまうためです。それを等倍に戻すためにこれが便利だったりします。

この機能に関しては標準機能のみで行った方が楽なので、通常のワークフローのみで作成します

ワークフローが受け取る現在の項目をイメージファイルに、ワークフローの1件目をFinder項目を複製に、ワークフローの2件目をイメージをサイズ調整にし、比率(パーセント)指定を選択して値に50を指定し、保存すれば完了です。

f:id:treeapps:20190430052137p:plain

元のファイルを削除せずコピーしてからリサイズするので、操作を誤っても元ファイルへの影響は無いので安心です。

GIFアニメで挙動を確認する

f:id:treeapps:20190430053258g:plain

トラブルシューティング

作成したサービスがメニューに出てこないんだけど?

ワークフローが受け取る現在の項目の設定値が誤っていると、メニューに表示されない事があります。

例えば「ワークフローが受け取る現在の項目」を「自動(テキスト)」が選択されている場合、フォルダを右クリックしてもサービスに表れません。選択値の通り、テキストファイル上で右クリックした場合のみ選択できるようになります。従って、対象が画像なのか、フォルダなのか、等を考慮して適切に設定する必要があります。

homebrewのコマンドが実行されてないっぽいんだけど?

Automatorはログインシェルを実行してくれないため、.bashrcや.zshrcを参照しないため、環境変数やパスが未設定の状態で実行されます。

これに対応するにはフルパスで記述する等が必要になります。

コマンド実行後通知できない?

# コマンドのインストール
brew install terminal-notifier
# 通知テスト
/usr/local/bin/terminal-notifier -title"title"-subtitle"subtitle"-message"テスト"

これをスクリプトの最後等に挟むと通知もできます。

while read file; do って何だよ

「while read file; do」は標準入力(stdin)から選択された複数のファイル名を受け取っています。

何故標準入力(stdin)なのかというと、Automatorのワークフローでそう設定したためです。

f:id:treeapps:20190430062220p:plain

もし「入力の引き渡し方法」を「引数として」を選択した場合、スクリプトの1行目を以下のように修正する事で動くようになります。

while read file; dofor file in"$@"; do

これは単にシェルスクリプトの処理の問題なだけなので、使い慣れている方で記述すればいいかなと思います。

使わないサービスを非表示にしたいんだけど?

システム環境設定 ->キーボード ->ショートカットタブを選択 ->リストボックスからサービスを選択 ->表示したいもののみチェックする

f:id:treeapps:20190430061517p:plain

尚、サービスの数が5個以上の場合はサービスというサブメニューが表示され、5個より少ない場合は以下のようにサブメニューの代わりにコマンド名が直接表示されるようです

f:id:treeapps:20190430055140p:plain

自作したサービスを削除したいんだけど?

自作サービスは /Users/ユーザ名/Library/Services/に保存されているので、ここにあるファイルを削除するだけで、リアルタイムに削除が反映されるようです。

恐らく右クリックしてサービスメニューを展開したタイミングで都度このファイル群を参照しているため、リアルタイムに反映されるのだと思われます。

雑感

Windowsでは使えないので汎用性には大変疑問が残りますが、個人用途ではAutomatorは非常に有用だったりします。

特にシェルスクリプトが実行可能という事は、プログラマの皆様なら何ができるか想像できますね?

スクリプトを書かなくてもUIだけで構築できるので、実は色々楽だったりします。例えば画像サイズ50%化ですが、macにはsipsという画像操作コマンドがありますが、これは比率を指定できないため、ImageMagick等を別途インストールする必要があります。しかしそれをするためだけに巨大で脆弱性満載なものをインストールしたくありません。そんな時標準ワークフローが利用すると簡単に実現できるので楽です。

今回のように、App Storeからインストールしたくない系コマンドは沢山あると思うので、サービス化等をしてオペレーションを簡単にしておきたいですね。

Java+MyBatisでMySQLのLOAD DATA LOCAL INFILEを実行できるようにする

$
0
0

実は簡単にできるのでした〜

f:id:treeapps:20180418131549p:plain

MyBatisというか、ORM経由でLOAD DATA等の特殊なDMLを実行したかったり、複数行のSQLを1定義で実行したい時って結構あったりしますよね。今回はそれに対応してみます。

情報のおさらい

解説の前に情報が混乱しないよういくつか整理しておきます。

MySQLのLOAD DATAとLOAD DATA LOCALの違い

種別挙動
LOAD DATA INFILE「リモートに有る」CSV・TSV等をMySQLに直接読み込む
LOAD DATA LOCALLOAD DATA LOCAL INFILE「ローカルに有る」CSV・TSV等をMySQLに転送して読み込む

Amazon RDSやCloud SQL等のマネージドサービスの場合はそのDBサーバのファイルシステムに触れないため、基本的に「LOAD DATA」は使用できないので、ほとんどの場合「LOAD DATA LOCAL」使用する事になります。

LOAD DATAはオプション未設定では実行できなくなった

dev.mysql.com

いろいろ書かれていますが、要は以下です。

  • 今まで認可処理が無かったから、参照権限さえ持っていればリモートのファイルを読み取れて危なかったぞ。
  • クライアントとサーバの両方でLOAD DATA LOCALを許可するオプションを付けないと実行できないようにしたぞ。
  • クライアントかサーバのどちらかのオプションが未設定の場合は「ERROR 1148: The used command is not allowed with this MySQL version」エラーを返すぞ。

という事です。サーバ側の設定としては、サーバ側のmy.cnfに「local-infile=1」を設定します。

クライアント側は「mysql --local-infile=1 -hホスト名 -uユーザ名 -p DB名」というようにオプション設定して接続します。

しかしここで一つ問題が生じます。

mysqlクライアントを介さないアプリケーションからLOAD DATAする場合に「--local-infile=1」を指定するにはどうするか?という問題が起きます。

実際のユースケースとしては、単純にJDBCドライバからLOAD DATAするには?という事ですね。

未設定状態のアプリケーションでLOAD DATAすると発生するエラー

以下は、Java, Spring boot, MyBatisのアプリケーションからLOAD DATA LOCALを実行した場合のStackTraceの一部です。

; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: The used command is not allowed with this MySQL version
	at org.springframework.jdbc.support.SQLExceptionSubclassTranslator.doTranslate(SQLExceptionSubclassTranslator.java:93)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:81)
	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
	at org.mybatis.spring.SqlSessionTemplate.update(SqlSessionTemplate.java:294)
	at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:67)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:58)
Caused by: java.sql.SQLSyntaxErrorException: The used command is not allowed with this MySQL version
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:955)
	at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:372)

エラーのポイントは「ClientPreparedStatement.execute」でしょうか。「クライアント側でPreparedStatementを実行しようとして失敗した」と言っているわけです。これが正に「--local-infile=1」オプションが無いから発生したエラーなのです。

解決策

では解決策を調べていきます。

結論から言うと、「MyBatisで対応するのではなくJDBCドライバで対応する」形になります。

アプリケーションからはJDBCドライバを使用してMySQLサーバに接続しており、mysqlクライアントを介していません。従って、JDBCドライバで何とかするしかありません。

setLocalInfileInputStreamを使う

github.com

ピンポイントでいきなりメソッドが出てきましたが、要はこれが使われれば、「--local-infile=1」を指定したのと同じ状態でMySQLサーバに接続した事になります。

しかしこのご時世、直接JDBCドライバのメソッドを呼びたくありません。

対応するJDBCのオプションが有る

JDBCドライバにはズバリそれに該当するオプションがあるのです。

dev.mysql.com

allowLoadLocalInfile

Should the driver allow use of 'LOAD DATA LOCAL INFILE...'?

Default: false

Since version: 3.0.3

allowUrlInLocalInfile

Should the driver allow URLs in 'LOAD DATA LOCAL INFILE' statements?

Default: false

Since version: 3.1.4

LOAD DATAをファイルパス指定する場合は「allowLoadLocalInfile」を、URL指定する場合は「allowUrlInLocalInfile」を有効にするだけで対応できます。

このオプションは以下のようにURL属性で指定します。

jdbc:mysql://${host}:${port}/${schema}?allowLoadLocalInfile=true

これで無事、mysqlクライアントを介さずにアプリケーションから直接LOAD DATA LOCAL INFILEを実行できるようになります。

allowLoadLocalInfileは何故デフォルト値がfalseで無効なのか?と一瞬思いますが、冒頭のMySQLの公式リファレンスのセキュリティ問題に合わせ、初期値は無効にしていると思われます。

おまけ

MyBatisのmapperのxml内で複数のステートメントを記述したい

例えば以下のようにセミコロンが複数ある、つまり複数ステートメントをMyBatisのmapperのxmlで、1度のSQLで実行してみます。

<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC"-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="hoge"><select id="hoge">
    select 1;
    select 2;
  </select></mapper>

すると以下のエラーが発生します。

### Cause: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select 2' at line 2
; bad SQL grammar []; nested exception is java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select 2' at line 2
	at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:234)
	at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:72)
	at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:73)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:446)
Caused by: java.sql.SQLSyntaxErrorException: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'select 2' at line 2
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:120)
	at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
	at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
	at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:955)
	at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:372)

これもJDBCドライバのオプションが問題で、「allowMultiQueries」の初期値がfalseなので、シンタックスエラー扱いになっています。

allowMultiQueries

Allow the use of ';' to delimit multiple queries during one statement (true/false). Default is 'false', and it does not affect the addBatch() and executeBatch() methods, which rely on rewriteBatchStatements instead.

Default: false

Since version: 3.1.1

allowMultiQueries=trueをJDBCドライバのURLに指定すれば複数ステートメントの実行は可能になります。

しかしこれを有効にすると、SQLインジェクションを行う際に複数ステートメントが実行できてしまい、最悪沢山の攻撃を1度に受けてしまう可能性があります。もしallowMultiQueries=falseであれば、仮に1度に複数ステートメントを実行しようとしても、syntax error扱いにする事ができ、多少安全になります。

こういったリスクと隣合わせになるので、利便性と相談しつつ慎重に扱う形になりそうです。

雑感

この問題は他のORMを使っても起き得る問題なので、覚えておくと便利そうです。

なお、LOAD DATA LOCAL INFILEは暗黙的なコミットを発生させずトランザクションが有効になるので、バッチ処理等で積極的に使っていきたいですね!
dev.mysql.com

material-ui v4+TypeScriptでwithStylesからmakeStylesに移行してシンプルにする

$
0
0

makeStylesを使う事でシンプルになりますよ〜

f:id:treeapps:20190616112954p:plain

ついにリリースされたmaterial-ui v4ですが、色々な機能が加わったり、構造がより最適化されたり、是非ともバージョンアップしたいですね。

今回はスタイルの「makeStyles関数」に焦点を当ててみます。

v3からv4への移行

v3のスタイル設定

import React from "react"import{ createStyles, Theme, withStyles, WithStyles } from "@material-ui/core/styles"const styles = (theme: Theme) =>
  createStyles({
    root: {
      padding: theme.spacing.unit * 2,
    },
  })

interface IProps extends WithStyles<typeof styles> {
  children: React.ReactNode
}const HelloComponent = (props: IProps) => {const{ classes, children } = props 
  return (<div className={classes.root}>{children}</div>)
}exportconst Hello = withStyles(styles)(HelloComponent)

FC(functional component)内からstyles定数のrootに触れるようにするため、IPropsのインターフェースにstylesを継承させる事で、classes経由でrootに触れるようにしています。

IPropsにWithStylesを継承するとclasses.rootと書けるようになるのは、WithStylesクラスのプロパティにclassesがあるためです。
github.com

そしてwithStyles(styles) の部分がHOC(higher order component)になっていて、hooksが登場した今となっては、関数でラップしているのが冗長です。

v4のスタイル設定

import React from "react"import{ Box } from "@material-ui/core"import{ createStyles, Theme, makeStyles } from "@material-ui/core/styles"const useStyles = makeStyles((theme: Theme) =>
 createStyles({
   root: {
     padding: theme.spacing(2),
   },
 }))

interface IProps {
 children: React.ReactNode
}exportconst Hello = function(props: IProps) {const{ children } = props 
 const classes = useStyles(props)
 return (<Box className={classes.root}>{children}</Box>)
}

IPropsの呪文のようなextendsが消え、withStylesのHOCが消えた事で単なるfunctionにする事ができ、かなりシンプルになりました。

注意点

従来のReact.Componentをextendsしたコンポーネントでは使えない

例えば以下のようなライフサイクルメソッドを持つコンポーネントです。

class Hello extends React.Component<IProps, IState> {
  constructor(props: IProps) {super(props)
    this.state = {
      open: true,
    }}
  render(
    return (<div>hello</div>)
  )
}

これに対してmakeStylesを適用しようとすると、以下のようになります。
f:id:treeapps:20190616040147p:plain

Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app
See https://fb.me/react-invalid-hook-call for tips about how to debug and fix this problem.

要は「function componentで使えよな!」と怒られています。

という事で従来のReact.Componentを継承した形では使えず、FC化する必要があります。

この従来のstateをもつcomponentをFC化しようとすると、React Hooksを使う事になります。今回はスタイルの話なのでそこは割愛します。

更に言うと、makeStylesを使い、React HooksでFC化し、先日リリースされたhooks対応されたreact-redux v7でconnect関数(HOC)を排除すると、connect関数を排除する事もでき、より単純なfunction化に近づきます。
react-redux.js.org

typescript + next.js + material-uiのサンプル

以前より以下のgitリポジトリを最新化していっています。
github.com

2019/06時点では以下に対応しているサンプルとなります。

  • TypeScript v3.5
  • Next.js v8.1
  • React v16.8
  • material-ui v4
  • redux v4
  • react-redux v7

まだTSLintを使っていたりreact-redux v7のhooksも使っていないですが、一部可能な部分はmakeStylesを使う形に更新しています。

雑感

material-uiは3〜7日おきくらいにバージョンアップを繰り返しており、物凄い勢いで進化している凄いプロダクトです。

進化が速いので、twitterでmaterial-uiの更新情報をこまめにキャッチアップし、更新に追従していった方が無難かもしれませんね。

react-redux v7.1+TypeScriptでconnect,mapStateToProps,mapDispatchToPropsを撲滅する

$
0
0

ついに憎き3兄弟を除去する事ができるようになりました〜


f:id:treeapps:20190616112954p:plain

github.com

先日react-reduxがv7.1にアップデートされ、そこでhooks対応の関数がいくつか追加されました。

今回紹介するものは「useSelector」「useDispatch」の2つです。

react-redux v7.1の新機能

useSelector

ざっくり説明すると、mapStateToPropsをhooks対応したものです。

useSelectorを使う事で、mapStateToPropsを撲滅する事ができるようになります。

useDispatch

ざっくり説明すると、mapDispatchToPropsをhooks対応したものです。

useDispatchを使う事で、mapDispatchToPropsとbindActionCreatorsを撲滅する事ができるようになります。

connect関数は不要になる

useSelectorもuseDispatchもhooks apiで実装されているため、HOCベースなconnect関数は不要になります。これは非常に大きい事で、関数のexport周りのコードが凄くすっきりできます。

v7.1とそれ以前のコードの比較

v7.1以前のTypeScript + react-reduxのコード

まずは今までの見慣れたTypeScript + reduxの伝統的なコードです。これでもextends Rect.Componentなコンポーネントでないのでまだマシです。

mapStateToPropsより下辺りのコードがもう定型文と化していますね。

import{ Button } from "@material-ui/core"import{ createStyles, withStyles, WithStyles, Theme } from "@material-ui/core/styles"import React from "react"import{ connect } from "react-redux"import{ CounterActions } from "../store/actions"const styles = (theme: Theme) => createStyles({
  root: {},
})

interface IProps extends WithStyles<typeof styles> {
  count: number
  increment: () => number
  decrement: () => number
}exportconst Redux = (props: IProps) => {const{ classes, count } = props
  const handleIncrement = () => props.increment()
  const handleDecrement = () => props.decrement()
  return (
    <div className={classes.root}>
      <p>{count}</p>
      <Button color="primary" onClick={handleIncrement}>+ 1</Button>
      <Button color="primary" onClick={handleDecrement}>- 1</Button>
    </div>
  )
}const mapStateToProps = (state: IInitialState) => ({
  count: state.counter.count,
})

const mapDispatchToProps = (dispatch: Dispatch<Action<any>>) =>
  bindActionCreators(CounterActions, dispatch)

exportdefault withStyles(styles)(
  connect(
    mapStateToProps,
    mapDispatchToProps
  )(Redux as any)
)

※ 汚さを際立たせるため、敢えてwithStylesを使っています。

このコードを見ると私は以下を考えてしまいます。

  • propsにstateやactionをコピーするせいで、IPropsに本来のプロパティ値以外のreduxでしか使わないものが混入している。
  • withStylesとconnectのHOCを多重ラップしている部分の汚さ。
  • mapDispatchToPropsの型定義部分の苦しさ。

v7.1以降のTypeScript + react-reduxのコード

import{ Button } from "@material-ui/core"import{ createStyles, makeStyles, Theme } from "@material-ui/core/styles"import React from "react"import{ useDispatch, useSelector } from "react-redux"import{ CounterActions } from "../store/actions"import{ IInitialState } from "../store/states"const useStyles = makeStyles((theme: Theme) => createStyles({
    root: {},
}))

const countSelector = (state: IInitialState) => state.counter.count

exportdefaultfunction() {const classes = useStyles({})
  const dispatch = useDispatch()
  const count = useSelector(countSelector)
  const handleIncrement = () => dispatch(CounterActions.increment())
  const handleDecrement = () => dispatch(CounterActions.decrement())

  return (
    <div className={classes.root}>
      <p>{count}</p>
      <Button color="primary" onClick={handleIncrement}>+ 1</Button>
      <Button color="primary" onClick={handleDecrement}>- 1</Button>
    </div>
  )
}

滅茶苦茶コードがスッキリしましたね!

今までのconnect + mapStateToProps + mapDispatchToPropsの定型文が悪夢のように感じる程です。

全部入りのサンプルコード

github.com

TypeScript v3.5 + Next.js v8.1 + material-ui v4 + react-redux v7.1に対応したサンプルプロジェクトになります。

Next.jsなので、勿論SSR対応しており、このサンプルではSSRはpagesディレクトリ配下で使用しています。

コンポーネント自体は単純なfunctionで記述しておき、functionにgetInitialPropsをstaticメソッドとして定義し、最後にexport defaultする形になります。

function Redux() {// snip}// for SSR
Redux.getInitialProps = async ctx => {const pagePayload: IPagePayload = {
    selectedPage: Page.REDUX,
  }
  ctx.store.dispatch({
    type: PageActions.changePage.toString(),
    payload: pagePayload,
  })
}exportdefault Redux

雑感

hooksにより、相当シンプルにコードが記述できるようになりました。

もはや定型文と化していたconnect + mapStateToProps + mapDispatchToPropsの3兄弟がいなくなったのは大きいですね。

今回の機能はまだリリースされたばかり解っていない部分があったりドキュメントが読み込めてないので、随時キャッチアップして以下のリポジトリに反映していきたいと思います!
github.com


REALFORCE R2 TKL・HHKB Pro2 TypeS・NIZ 2019 Waterproof Seriesを使ってみた

$
0
0

今回はキーボードの使用レポートになります

f:id:treeapps:20180418125439p:plain

最近Realforce for macテンキーレス、HHKB HYBRIDと、相次いでフラッグシップなキーボードが発表されたので、それに触発されて旧版の使用レポートをしてみようと思います。

※ 個人の主観によるレビューになります。
※ ぶっちゃけレビューなので、変に期待させたり、不自然に持ち上げるレビューではないです。

レビューするにあたって

私は静音性を最重要視しているので、それ前提のレビューになります。

比較対象のスペック

PFU Happy Hacking Keyboard Professional2 Type-S 英語配列/白 PD-KB400WS

PFU Happy Hacking Keyboard Professional2 Type-S 英語配列/白 PD-KB400WS

  • 発売日: 2011/06/15
  • メディア: Personal Computers
名称 スイッチ方式キースイッチ押下圧APC防水静音モデル
REALFORCE R2 TKL 静電容量無接点専用ALL 30g無し
HHKB Professional2 Type-S 静電容量無接点専用ALL 45g無し
NIZ 2019 87 IP68 waterproof series静電容量無接点Cherry MX軸ALL 35g無し○ IP68

防水レベルIP68については以下をご覧下さい。
www.gizmodo.jp
https://www.ip68.jp/technicalguide/pdf/PP%20IPtoukyu.pdf

スクリーンショット

f:id:treeapps:20191214202723p:plain

NIZの白さが際立ってますね!

文字の刻印位置がRealforce・HHKBとは少し異なる点は不思議です。

f:id:treeapps:20191214202740p:plain

静音

左(打鍵音小)             右(打鍵音大)
REALFORCE > HHKB Type-S >>> NIZ

  • Realforceは低〜中の打鍵音
  • HHKBは中〜高の打鍵音
  • NIZは中の打鍵音。静音モデルですが、Realforce・HHKBより打鍵音が明確に大きいです。

静音を意識して打鍵すればRealforceが最も静音です。

HHKBは後述するようによく打鍵する左Shiftが煩いです。

NIZは全体的に煩いです。

※ 煩いといっても静電容量無接点方式+静音モデルの中では、という意味です。

打鍵音

REALFORCE R2 TKL

全体的に静かで打鍵音は低めです。

打鍵感は全体的に ストストです。

カチャカチャ音は少しします。

HHKB Professional2 Type-S

英数記号キーの打鍵感は コトコトです。

それ以外のキーの打鍵感は コンコン・カンカンです。

カチャカチャ音は少しします。

NIZ 2019 87 IP68 waterproof series

打鍵感は スコスコ・シャコシャコです。

カチャカチャ音はしません。全体的に均一にスコスコ・シャコシャコです。

特定のキー押下の気になる点

REALFORCE R2 TKL

Backspaceキー・Returnキー

キーの中で最も音がします。中音でコンコン!みたいな感じです。

バネ音

数字キーを強めに打鍵した際にバネの音が少しビヨンビヨン聞こえます。

HHKB Professional2 Type-S

左Shiftキー

左Shiftキーが明らかに打鍵音が大きいです。カンカン!と音がしてしまいます。個体差ではなく構造上の問題な気がします。

左Shiftキーだけ、打鍵時に机に衝撃が一番強く伝わり、甲高い音が響きます。以下のHHKB吸振マットを付けていますが、付ける前と変化はほぼ無く煩いです。

Returnキー

左Shiftキーより、Returnキーの方が明らかに静かです。ただ、キーの中で2番目に煩いです。

打鍵時の机への衝撃もそこそこあります。

スペースキー

スペースキーは甲高い音ではなく、低い音です。

NIZ 2019 87 IP68 waterproof series

バネ音

矢印キーや左Ctrlキー打鍵時にバネの音が少しビヨンビヨン聞こえます。

打鍵の重さ

Realforce(ALL30g)、HHKB(ALL45g)、NIZ(ALL35g)なのですが、何故かRealforceの30gよりNIZの35gの打鍵の方が軽く感じます。キーストローク差等が要因なのかもしれませんね。

個人的に押下圧は軽い方が好みなので、HHKBは重く感じますね。

打鍵感

REALFORCE R2 TKL

良いと言えば良いのですが、なんでしょう。この微妙に制御しきれていない感は、みたいな印象です。

少しグラつき感があるような(キーが底面に到達するまでに少し斜めにグラつくみたいなイメージです)、微妙な精度の悪さを感じます。

HHKB Professional2 Type-S

凄く良いです。制御しきれている感が強いです。グラつきも不安定感も全く感じません。

Realforceと比較すると、とにかく打ち心地が良いです。

NIZ 2019 87 IP68 waterproof series

Realforceと同等くらいで、HHKBより悪いです。


完全に私の感覚になりますが、打鍵ミスが起きるのは最多はRealforce、次点でNIZ、最小はHHKBです。

押下圧と精度の両方が関係しているのかもしれませんね。

筐体の作り

※ 筐体は本体の箱の部分を指しています

左(作りが良い)      右(作りが良くない)
HHKB Type-S > NIZ > REALFORCE

REALFORCE R2 TKL

筐体が薄い?というか、他の2つよりは作りが微妙な気がします。トータル品質が平均点くらいな感じです。

打鍵時にドッシリとした安定感は無く、HHKB・NIZより安定感を感じません。

HHKB Professional2 Type-S

HHKBが最も作りが良く感じます。カッチリしているというか、「これは明らかに長期間使える」感が強くします。

打鍵時の安定感も高く、Realforce・NIZと異なりバネ音もしません。

NIZ 2019 87 IP68 waterproof series

価格の割に作りが良く感じます。カッチリしてます。

打鍵時の安定感は高いです。筐体が最も重く筐体に厚みがある?ので安定感が高く感じる気がしています。

本体の重さ

名称 重量
REALFORCE R2 TKL 1.1Kg
HHKB Professional2 Type-S 530g
NIZ 2019 87 IP68 waterproof series1.4 Kg

圧倒的にNIZは重いです。凄くズッシリ重量感があります。持ち運びは絶望的ですが、重さがあるので打鍵時の本体の安定感が高いです。

デザイン・カラーバリエーション

REALFORCE R2 TKL

カラーバリエーションは、アイボリー・黒の2種類です。

全体的に昔ながらの業務用アイボリー臭が全開です。薄いアイボリーと薄めなアイボリーのツートンカラーで、濃淡にメリハリが無いため、強い業務臭を発してしまっている気がします。

昔のRealforceより筐体が角張った事により、多少見栄えは良くなってはいます。

HHKB Professional2 Type-S

カラーバリエーションは、(主に)アイボリー・黒(墨)の2種類です。

全体的に昔ながらの業務用アイボリー臭はしますが、薄いアイボリーと薄い青みがかったアイボリーなので、Realforceより良く見えます。

無刻印モデルにすれば余計な文字が消え、デザイン性が高くなります。墨色モデルもホコリや汚れが無い状態を維持できれば、とても綺麗で良い感じです。

NIZ 2019 87 IP68 waterproof series

カラーバリエーションは、アイボリーの1種類です。

アイボリーではありますが、白と濃い色のアイボリーで、濃淡にメリハリがあり、白基調な見た目が結構いい感じに見えます。

多分白さによって綺麗さが演出できているのだろうと思いました。

インターフェース

名称USBバージョンUSB HUB
REALFORCE R2 TKL2.0無し
HHKB Professional2 Type-S2.0有り
NIZ 2019 87 IP68 waterproof series2.0無し

REALFORCE R2 TKL

Realforceは基本的にインターフェースに期待してはいけない系です。。。それは最新のRealforce for macでも同じです。

HHKB Professional2 Type-S

HHKBはPro2まではUSB2.0ですが、最新のHHKB HYBRIDはUSB Type-Cに対応しています

NIZ 2019 87 IP68 waterproof series

防水仕様という事で、余計な穴や隙間は一切無いので、USBハブ等は無いです。

価格

おいくら万円?

名称amazon価格
REALFORCE R2 TKL約 23,500円
HHKB Professional2 Type-S約 24,500円
NIZ 2019 87 IP68 waterproof series約 15,000円

最も高価なのはHHKB、最も安価なのはNIZ、でした。

ちなみにテンキー有りよりテンキー無しの方が価格が安いです。

HHKBに矢印キーが無い点

HHKBのUS配列は昔から矢印キーが存在せず、右下のFnキーとReturnキー隣の上下左右の同時押しで矢印を再現するアレです。

暫く使ってみましたが、やっぱり辛い!という結論に至りました。

例えばブラウザやVSCodeのタブの前後移動は、私は option + cmmand + 左右の矢印キー でやっていますが、ボタンを4個押さないといけないのが大変で、「あの時はFn押す、この時はFn押さない」みたいな頭の切り替えが頻繁に起き、これを無意識で行うには修行が足りませんでした・・・

HHKBのこの配列は「ホームポジションが崩れにくい」とよく言われていますが、私はHHKBで矢印を打鍵しようとすると絶対ホームポジションが大きく崩れてしまうので、結局「ホームポジションとは」みたいになってしまいました。どうせ崩れるなら独立矢印キーが有った方が楽でいいですね。


また、通以上配列の矢印キーは以下のような山型なのに対し、

f:id:treeapps:20191214200534p:plain

HHKBは以下のようにひし形で、しかも↑と↓の縦軸がズレている点と、↓が下部に配置されているせいで指が楽に自然に曲がる形状にフィットしていない点も辛い要因でした。

f:id:treeapps:20191214200207p:plain

結局どれ使ってるの?

自宅ではローテションさせてます。使用頻度が高い順だと、1位:Realforce、2位:NIZ、3位:HHKB、になります。

HHKBは矢印の辛さ、左Shiftキーのカンカン音、押下圧の重い点が敬遠理由で、NIZは打鍵音が煩いのが敬遠理由です。

消去法でRealforceになっているのが現状ですが、HHKB・NIZとか比較して作りの甘さを徐々に感じ始めてきています。


NIZは防水仕様で筐体を洗える事は大きなメリットだと感じています。防水でないと色系の汚れが付着した際に掃除が難しく、ホコリ取りもエアダスター等がいちいち必要ですが、NIZのwaterproof seriesなら洗面所や風呂場で少量の石鹸や洗剤で水洗いできちゃいます。これは本当に楽です。

雑感

キーボード、自分の中での決定版が見つかりませんね。

個人的に最もバランスがいいのはRealforceですが、掃除が面倒なのと品質が微妙な気がするのが残念ですね。押下圧ALL30gが影響しているのかな🤔

HHKBはUS配列で矢印キーがあって左Shiftが静かになれば最高ですね。(あとできれば独自のキー配列もやめて欲しい)

NIZは個人的に実はかなりオススメで、安いのに筐体もしっかりしていて、何より汎用キートップなので、パステルカラーにしたり、キャラクターが立体的に浮き出ているキートップ等、自在に装飾する事ができます。

変わり種キートップについてはAliExpressに沢山あるので見てみると面白いです。

ja.aliexpress.com

複数のキーボードを使って思うのは、キーによって打鍵音が結構異なる点ですね。HHKBは顕著で、左Shiftはコンコン煩いのに右Shiftはそうでもなかったり。NIZは全体的に一定の打鍵音だったり。こういう細かい点も以外と見逃せない点だったりするので、極力実際に店舗で試すのをオススメします(NIZは絶望的だと思いますが)。

PostmanでGlobalsの環境変数をリクエスト結果で更新する

$
0
0

毎回手作業で更新しなくていいのです

f:id:treeapps:20170828014922p:plain

皆大好きPostman。

Postmanには環境変数があり、各Request設定で環境変数を埋め込む事で、設定を一元管理する事ができます。

今回はこの環境変数についてのお話です。

Postmanって何?

www.getpostman.com

(主に)HTTPリクエストを発行するためのツールです。

ブラウザのアドレスバーだとGETリクエストしかできないし、ターミナルでは都度APIのコマンドを用意する必要があります。

Postmanはこれらを解決してくれ、更に環境変数でAPIサーバのポート番号やトークン値等を一元管理できたり、javascriptで動的に色々制御できたりするツールです。

設定のimport/exportも可能なので、gitで管理して全員共通のAPI実行環境を用意する〜、なんて事もできます。

環境変数を使ってトークンを一元管理するが・・・

例えば「データ取得API」が20個有り、それら全てのAPIはトークン取得APIによって取得したトークンをAuthenticationヘッダに設定する事で、APIが使用できるとします。トークンには有効期限が有り、一定期間で無効になってしまいます。

最初は、トークン失効後にトークンを再発行し、そのトークンを毎回20種類のAPIに対して1件づつ設定していました。しかしこれは流石に無理があるので、環境変数で一言管理してみました。

f:id:treeapps:20191221141421p:plain
画面右上の歯車ボタンをクリックし、Globalsボタンをクリックします。

f:id:treeapps:20191221141437p:plain
テーブルに変数名と値を入力します。今回はトークン値だけでなくAPIサーバのportも登録してみました。

f:id:treeapps:20191221141510p:plain
中括弧2個のヒゲ(mustache)形式で、アドレスバーにもBearerトークン値にも、Globalsに登録した変数が参照可能になりました。以下のような形で埋め込みます。

{{bearerToken}}

さて、これで以降は環境変数を変更するだけで全APIにBearerトークン値を設定する事ができました!!

・・・・トークンの有効期限が短いので、想像していたよりも頻繁に設定の修正が発生してしまいました。一元管理できてはいますが、1日に何回も変更するのは大変です・・・

そうだ、環境変数を自動更新しよう!

ここで今回の本題です。

現状より更に楽をするためには、トークン取得APIを実行したら自動的にレスポンスjson内のトークン値をGlobalsの環境変数値に上書きする事ができれば、手動での変更は不要になります。

Testsを使ってGlobalsを上書きする

もっといいやり方がある可能性がありますが、「レスポンス値を使って」を実現できるのがTestsを使った方法しか解らなかったので、Testsで説明します。

f:id:treeapps:20191221143804p:plain
リクエストのTestsタブを開き、以下のようにjavascriptを記述します。

const json = JSON.parse(responseBody);
postman.setGlobalVariable("bearerToken", json.accessToken);

↑のように記述すると、↓のトークン取得APIのレスポンスjsonからbearerTokenの値を取得し、setGlobalVariableでGlobalsを上書きする事がきるのです。

{"accessToken": "ZDQ1YTkxNTgtZTJkMi00OTU4LTk1ZWItMTM3YzM3NWFhMDg1"
}

トークン取得APIのTestsに上記javascriptを記述したら、早速トークン取得APIを実行してみて下さい。取得結果のトークン値でGlobalsの値が上書きされている筈です。

f:id:treeapps:20191221144606p:plain

雑感

PostmanでChrome DevToolsを表示する事ができる(という事はpostmanはElectron?)ので、console.log(response) のようなデバッグコードを記述して確認する事もできます。

公式ドキュメントには、各種実行順序やscriptsについてのドキュメントもありますので、他にも自動化したい等があれば、一度ドキュメントに目を通しておくとよさそうですね!
learning.getpostman.com

Ionic React+CapacitorでElectron利用時にstaticがずれる件に対応する

$
0
0

初見殺しなのでメモです

f:id:treeapps:20190616112954p:plain

Ionic Reactがリリースされたので、早速試してみました。

今回は Ionic React + Capacitorで成果物をElectronで出力してみたのですが、いきなり洗礼を浴びたので、そのメモを残しておきます。

環境

種別バージョン
macOSCatalina
node.jsv12.16.0
ionic-cliv6.1.0
capacitor-cliv1.5.0

staticが見つからない

ビルドやらcopyやらをし、最終的に npx cap open electronすると、以下のようになりました。

f:id:treeapps:20200228090450p:plain

GET file:///static/css/10.4c99b71.chunk.css index.html:1
net::ERR_FILE_NOT_FOUND

githubに以下のissueが挙がっていました。

github.com

対応策

public/index.html

以下をコメントアウトします。

<basehref="/" />

<!-- <base href="/" /> -->

package.json

以下を追加します。追加位置はどこでもいいので、今回は末尾に追加しています。

"description": "An Ionic project"
}

"description": "An Ionic project",
  "homepage": "."
}

ビルドする

ionic build
npx cap copy
npx cap open electron

さて、結果は・・・

f:id:treeapps:20200228091607p:plain

OKです。

修正前は

<linkhref="/static/xxxx"rel="stylesheet">

だったのが以下のように「.」始まりに変わり、解決したようです。

<linkhref="./static/xxxx"rel="stylesheet">

👍

おまけ

環境構築から動かすまで

# ionic-cliをグローバルにインストール
npm install -g @ionic/cli

# プロジェクトの生成(ここでcapacitorの依存が追加されるので別途install不要)
ionic start ionic-react-example tabs --type=react--capacitor--no-git# ionic start時に以下のエラーが起きた
gyp: No Xcode or CLT version detected!
gyp ERR! configure error
gyp ERR! stack Error: `gyp` failed with exit code: 1
gyp ERR! stack     at ChildProcess.onCpExit (/Users/tree/.nodebrew/node/v12.16.0/lib/node_modules/npm/node_modules/node-gyp/lib/configure.js:351:16)
gyp ERR! stack     at ChildProcess.emit (events.js:321:20)
gyp ERR! stack     at Process.ChildProcess._handle.onexit (internal/child_process.js:275:12)
gyp ERR! System Darwin 19.3.0
gyp ERR! command"/Users/tree/.nodebrew/node/v12.16.0/bin/node""/Users/tree/.nodebrew/node/v12.16.0/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js""rebuild"
gyp ERR! cwd /Users/tree/work/ionic-react-example/node_modules/jest-haste-map/node_modules/fsevents
gyp ERR! node -v v12.16.0
gyp ERR! node-gyp -v v5.0.5
gyp ERR! not ok

# Xcodeコマンドラインツールを本体のものに切り替え
sudo xcode-select --switch /Applications/Xcode.app

# リトライ -> 成功
ionic start ionic-react-example tabs --type=react--capacitor--no-git# webをビルド
ionic build

# electronの依存等を準備(事前にionic buildが必要)
npx cap add electron

# ionicでbuildした成果物を各プラットフォームassetにコピー
npx cap copy

# Electronで起動
npx cap open electron

TSLintからESLintに雑に移行する

$
0
0

雑の極みです

f:id:treeapps:20180418115102p:plain

ESLint v7.0.0がついに正式リリースされました👏
github.com

そしてgitリポジトリ見ると解りますが、TSLintは既にDeprecatedなのです😱

そんな状況のTSLintで騙し騙し継続して使っていましたが(まだTSLintが使われるscaffoldingも結構あります)、v7リリースという事で、これは絶好の移行機会だと思い、早速移行しました。

また、最近Nest.jsを試しているのですが、Nest.jsは既にTypeScript + ESLintでscaffoldingされたので、それを参考にしています。

環境

React + TypeScript + Prettier + ESLintの組み合わせの設定になります。

TSLintの削除

package.jsonからtslintを削除する

package.jsonを開き、tslintと名の付くものを全てuninstallします。

npm uninstall tslint tslint-config-prettier tslint-config-standard tslint-plugin-prettier

package.jsonからtslint関連のnpm scriptを削除する

例えばscriptsセクションに以下のようなtslintコマンドが有る場合は全て削除します。

"scripts": {"lint": "tslint -c ./tslint.json --exclude **/*.d.ts --exclude ./node_modules --project . --fix **/*.tsx --fix **/*.ts",
    "tslint-check": "tslint-config-prettier-check ./tslint.json"
  }

tslint.jsonを削除する

rm-rfv tslint.json

ESLintの追加

package.jsonにtslintを追加する

npm i -D eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint-config-prettier eslint-plugin-prettier eslint-plugin-react eslint-plugin-import

package.jsonにeslint関連のnpm scriptを追加する

components,constans...の部分は適宜対象フォルダ名を羅列して下さい。

"scripts": {"lint": "eslint \"{components,constants,hooks,model,pages,store,types}/**/*.{ts,tsx}\" --fix"
  },

.eslint.jsを追加する

cat <<EOF> .eslintrc.jsmodule.exports = {  parser: "@typescript-eslint/parser",  parserOptions: {    project: "tsconfig.json",    sourceType: "module",    ecmaFeatures: {      jsx: true,    },    useJSXTextNode: true,  },  plugins: ["@typescript-eslint/eslint-plugin", "react"],  extends: ["plugin:@typescript-eslint/eslint-recommended","plugin:@typescript-eslint/recommended","prettier/@typescript-eslint","plugin:react/recommended","prettier/react",  ],  root: true,  env: {    node: true,    jest: true,  },  rules: {"@typescript-eslint/interface-name-prefix": "off","@typescript-eslint/explicit-function-return-type": "off","@typescript-eslint/no-explicit-any": "off","@typescript-eslint/no-unused-vars": ["error",      {        argsIgnorePattern: "^_",      },    ],  },}EOF

これで完了です。

おまけ

以下のサンプルアプリケーションを公開中です。最近Vercel(旧Zeit)の個人プランが無料化したので、両者ともLiveデモで実際に触る事ができます。

TypeScript + Next.js + Material-UI + Redux + Redux Saga

git repository

github.com

TypeScript + Next.js + Material-UI + Redux + Redux Toolkit

こちらは Redux Toolkitに最近追加された createAsyncThunk , createEntityAdapter といった新機能も使っています。

git repository

github.com

2020年の振り返り

$
0
0

2020年を振り返りますよ

f:id:treeapps:20170818174241p:plain


いやー、今年は全然ブログ記事書きませんでした。

今年の簡単な振り返りと、ブログについて少しだけ書いてみようと思います。

ブログ・SEO

検索エンジンというか、SEOが理想論を追い求め過ぎ?ていると感じています。

間違いの無い完璧な記事の発信。正しさの追求。オフィシャル性の追求。画面の表示速度の追求。等、様々な正しさ・清廉潔白さの追求をしていて、もはや企業コンテンツか超人気コンテンツ以外はもう付いていけない・・・というのが今のブログ界隈、というかネット界隈の状態だと思っています。

一応軽くSEO系ニュースはウォッチしているのですが、新たな仕様が追加される度に「もう無理かな・・」という気持ちが強まるのが正直なところです。

今後もこのブログはバグの解決系やメモ書き等の軽い感じになる予定です。

仕事

守秘義務な部分は話せないですが、ざっくり言うと丸一年間教育(IT業界の開発)に関して取り組んでいました。

開発には全くアサインせず、完全に教育一本です。

今年はコロナの一年だったので採用がアレで、去年のような教育密度ではなかったので、ドキュメントやツールの整備や管理面を主にやっていました。

来年コロナがどうなるか解りませんが、今後も開発案件にアサインする事は無いかな?と考えています。どちらかというと管理面を強めていきそうです。

個人開発

www.tree-maps.com

以前↑のtree-mapsという地図系サービスをリニューアルしようと考えていましたが、今は完全に凍結中です。理由は簡単で、Google Geocoding APIの料金が高過ぎるためです。もはや個人では払えない額です。サイトの作りの悪さもありますが、月5万円請求が来た事もありました・・・

流石にそんな払えないので、APIの利用に上限を付けたので1日に実行できるジオコーディング件数は劇的に少なくなり、恐らくもう実用に耐えないと思います。

map.yahoo.co.jp

Yahoo陣営も地図はサービス提供終了してしまいました。ジオコーディングは継続して使用できますが、Googleジオコーディングと比較すると1段階精度が浅い住所して取得できず、これは使えるのだろうか、という微妙な感じです(だからこそサービスが継続できるのかもしれませんね)。

Googleジオコーディングの実質の個人利用の限界、Yahooジオコーディングの精度の悪さ、という事があり、正直tree-mapsの開発に対するモチベーションを完全に失っているのが現状です。

本当はGoogle・Yahoo・Bing等、複数サービスを横断して使える地図サービスサイトを目指していたのですが、ちょっと無理そうです。。。

GitHub

github.com

プロダクトの開発は全くしていません。特定の技術要素を使ったサンプルプロジェクトをいくつか作りました。

個人的に今年はTypeScriptの年だったので、TypeScript関連リポジトリばかり作りました。何故サンプルばかり作るのかというと、仕事の教育で初学者に教えなくてはならないので、実際にコードを書いてみて理解しないと教える事ができないから、です。

余談ですが、たまにIssueが起票されますが、所謂「クソIssue」と言えそうなものも有ります、というか大半がクソIssueです。タイトル無しでスクリーンショットだけを貼り付けてきたり、別のリポジトリのエラーを聞いてきたり、それこのリポジトリと関係無い話だよね?、等々。

体感95%がクソIssueですね。。。

前述したようにブログが完全にお通夜状態なため、今後はGitHubでサンプルを作り続ける形になりそうです。

リングフィットアドベンチャーを買った

リングフィット アドベンチャー -Switch

リングフィット アドベンチャー -Switch

  • 発売日: 2019/10/18
  • メディア: Video Game

あまりの運動不足でやば過ぎると感じ、リングフィットアドベンチャーをやり始めました。

最初は運動負荷 5 から開始(負荷の最大値は30)したのですが、5なのに初日に筋肉痛になり、うっそだろこれ・・・と悶絶していました。

筋肉痛 ->頑張る ->筋肉痛 ->頑張る、を繰り返し、今は↓こんな感じです。

f:id:treeapps:20201231005457j:plain

現在の運動負荷は24まで到達しました。私は運動ガチ勢ではなくエンジョイ勢ですらなく、無運動勢なので、まあまあ成長したのではないかと思います。ただ、ここから運動負荷を上げるのはもう本当に地獄で、なっかなか上げる気にはならないでしょう。。

まだクリアはしていませんが、個人的に辛いと感じたスキルのランキングを書いてみます。

辛いスキルランキング

1位 マウンテンクライマー

www.youtube.com

無っっっっっ理。無理。むり。ムリ。

一瞬で物凄い心拍が上がり、腕の負担、大きな足の移動、無理過ぎてやばい。さり気なく回数が物凄く多いのも無理に拍車をかける・・

高威力の範囲スキルなので選びたいけど、これは無理・・

2位 プランク

youtu.be

これも無理。見た目以上に腕と大腿四頭筋に負担がかかります。想像以上の負荷です。やる前は「ふーん。できそう。」とか軽く思ってましたが、いざやってみると2〜3回で潰れてぜぇぜぇはぁはぁしました。

これも高威力の範囲スキルで選びたいけど、無理・・

3位 スクワット

youtu.be

ただのスクワットだと思いますよね?動画見ると解りますが、腰を落とした状態で数秒キープする動作を何十回もやるんです。回数が物凄く多い系スキルなので、途中休憩を挟まないと完走できないです。

よくネットで見るスクワットって、キープ無しでせいぜい1セット30回を×3回とかですよね。これはキープする動きを1日に何回もするので、負荷が全然違います。

4位 バンザイスクワット

youtu.be

またしてもスクワット系です。これのキツイ点は腕を上げる点です。腕を上げる=肩の筋肉をずっと張り続ける事になります。大掃除で高いところを掃除しているとすぐ疲れますよね?アレをずーーーっとやる感じです。この動画のインストラクターは物凄く綺麗なフォームで腕を上げていますが、実際は斜め45度くらいしか上がらなくなります・・

5位 バンザイプッシュ

youtu.be

「日常生活でこんな動き絶対しねーよ」系のスキルです。肩の筋肉メインで、腕の筋肉はほぼ使わないので、普段こういう動きに慣れていない人には物凄く辛いスキルです。普段こんな動きする人なんているのだろうか・・・

思ったより辛いスキル

ベントオーバー

youtu.be

これ、以外と横っ腹の筋肉の負担が強く、完走はできるけどかなり辛いです。ただ、お腹の横の筋肉にダイレクトに効いてくるので、積極的に選びたいですね。

トライセプス

youtu.be

普段こんな動きしねーよ系です。20回を超えた辺りから、あれ腕が上がらない・・・!とジワジワ辛くなるやつです。

バタバタレッグ

youtu.be

他の足系スキルより足の付根への負担が強いです。ぽっこりお腹改善と書いてますが、お腹より足の付根への負担の方が圧倒的に強く感じます。

これに頼っちゃうスキル

スワイショウ

youtu.be

あまりに負担が少なくてついつい選びがちなスワイショウ。全体攻撃で且つ1回の動きが非常に短く、短時間で低負荷で全体攻撃できるというスキル。これの誘惑に負けてしまう人は多いでしょう。

私もこれに逃げがちですが、腕の動に対して逆方向に腰をグリンっ!と回す事で横っ腹の筋肉に負荷をかけてます。

リングアゲサゲ

youtu.be

範囲1のスキルですが、あまりにも負荷が弱く、疲れた時はこれに逃げがち。

アゲサゲコンボ

youtu.be

これも範囲1ですが、思ったより威力も高く、筋トレ系ではなく有酸素系の運動で、動き的には大分緩いです。これも疲れた時に選びがちですね。

その他

バンザイコシフリも簡単な部類ですが、判定が思ったより厳しい?っぽく、思ったより腰を大きく左右しないとbest判定にならないように感じました。

ヨガマットとか

Amazonでヨガマット探すと、180〜190cmのものが多く、こんな長いの敷けねーよ!なので、以下の170cmで厚さ10mmの分厚いものを使っています。

10mmでも騒音は少し気になるので、これを2枚重ねてもいいかもしれませんね。


こんな感じでリングフィットアドベンチャーを続けてます。筋肉痛が続いた日はきつかったですが、今のところほぼ毎日継続できています!プロテインを飲むようにしているので、今ではゲーム時間で1時間(現実時間で90分前後)やっても筋肉痛にはならなくなるくらいになりました。

エアロバイクを漕いでいた事もありましたが、Amazon Primeで映画やアニメを見ながら漕いでいましたが、とにかく単調な動きで継続できなかったのですが、リングフィットアドベンチャーはゲーム性があるおかげで、今まで経験した事が無いレベルで継続できています。

リング君がいい感じに褒めてくれたり励ましてくれたり、オーバーワークになりそうなタイミングで逐一確認してくれるし、継続させる仕組みが絶妙で、これは仕事の教育面に活かしたいな、と強く感じています。

フィットボクシング2を買った

リングフィットアドベンチャーは自重筋トレで、有酸素運動スキルもありますが、どちらかというと筋トレゲームです。

筋トレだけだとダイエットとしては足りないので、有酸素運動系のフィットボクシング2を購入しました。

購入初日は「リングフィットを20分やった後、フィットボクシングを40分やったろ。リングフィットの負荷24だし余裕やろ」とか、クソ舐めたムーブをかまそうと思ったのですが、10分で「ぜぇぜぇ・・・はぁはぁ・・・全然駄目だこれ・・・」となり、初日で背中側の脇の下が筋肉痛になりました。

筋肉痛は1週間くらい続いてしまい、その回復が終わるまでリングフィットしかできませんでした。。

筋肉痛の回復後、デイリーを30分で再開し、何とか今も継続しています。ただ、リングフィットより頻度は低く、1〜3日に1回しかできてません。できるだけパンチを強く打つようにこころがけているので、割とすぐ息切れして腕が上がりにくくなります。

リングフィットで足に負担をかけた後にダッキング・ウィービングをすると、負担は更に上がり、これ毎日やってる人すげぇ・・・と思う程の疲労です。

汗問題

フィットボクシングをプレーするにあたり、汗問題は結構注意する必要があります。

リングフィットと違い腕をブンブン、ダッキング・ウィービングで体をブンブンするので、汗が飛びます。無対策だとカーテンやモニターに汗が飛び散ります。。。

握っているコントローラーにも当然汗が付着するので、壊れないかヒヤヒヤしていました。流石に対策が必要だと思い、今は頭はタオルを巻き、他は以下を装着してプレーしています。

この自転車・車用グローブは薄手でメッシュ状で少しだけ通気性があります。デイリー30分やってもコントローラーには全然ダメージは無い感じです。

コンプレッションウェアは通気性が非常に高く、腕を動かすと通気し過ぎて「え、こんなに!?」というくらい冷えます。長袖なので汗が飛び散る心配もありません。

足に関しては柔らか素材のマリンシューズがいいそうですが、私は面倒なので普通に靴下履いてるだけです。

CV

このゲーム、トレーナーが有名声優しかいないので誰にするか迷いますが、カレン/CV.鬼頭明里 と ソフィ/CV.小清水亜美 と ベルナルド/CV.大塚明夫 を気分で変える形で落ち着いてますね。

特定のトレーナーをランダム選択する機能が欲しいところです。

TVの故障と液晶ディスプレイの購入

リングフィットとフィットボクシングが少しずつ安定して日課になり、さあ頑張るぞ!、という矢先に12/29の時点でTVが壊れました・・・

www.bunkei-programmer.net

2015/06/22 に東芝の4K・43インチ・アップスケーリング内蔵の液晶TVを購入し、ついに壊れました。年末のこのタイミングでです。

またTV買うかな〜?と考えて調べてましたが、今年一度もTV番組を視聴していない事に気づいたのと、PS5・XBOXの次世代機がHDMI2.1規格に対応し、4K + 120Hzでゲームができるようになっている事を考慮しておいた方がいい事が解りました。

次世代ゲーム機と対応TV

調査してみると、HDMI2.1・4K・120Hzに対応したTVで、一番小さいサイズがLGの48インチのOLED48CXPJAでした。

ちょっとでか過ぎるのと、昨今のTV番組がやらせ・偏向報道・ニュース番組で芸人が適当な事を言うなどが気になり、一切TV番組を見なくなったので、TV購入はもういいかな?と思い、TVは見送りました。

液晶ディスプレイ

任天堂SwitchをPC用の液晶ディスプレイにHDMIで接続すると、普通にゲームがプレーできる事は以前から知っていました。

しかし、TVと同様HDMI2.1に対応した液晶ディスプレイは先日ようやくAsusが型番すら未定の機種を発表したばかりで、実質対応機種無し!という、完全に時期が悪い状況です。

pc.watch.impress.co.jp

以下の27インチ・4K・144Hzで実売20万円である事を考慮すると、43インチだと価格がいくらになるか考えたくもないですね・・

もう完全に時期が悪いとしかいいようが無いので、HDMI2.1対応機種が出揃って価格が安定するまでの繋ぎとして、以下を購入しました。

www.dospara.co.jp

27インチ・IPS・WQHD(2560×1440)・144Hz・フレームレス・簡易スピーカー付き・HDMI2.0(144Hz) × 1・DisplayPort(144Hz) × 1 で税込み37,400円でした。

ドット抜け無し、発色良し、画面綺麗で、ゲーム用途なら中々良いのでは?と思います。ACアダプタもゴツい塊タイプではなく普通の細いコンセントタイプなのもGoodですね。

WQHDという特殊な解像度の懸念

WQHDは特殊なサイズで、PS5・XBOX Seriesは勿論WQHDに非対応で1080p(FHD)にダウンスケールされます。また、一般的なAVアンプもWQHDに非対応です。

最近はゲームをやるよりプログラムの勉強をしている時間の方が圧倒的に長いのと、ゲーム専用モニタとして48インチのTVを置きたくないし、次世代機はそもそも購入できないし、実質任天堂Switch専用ディスプレイならこれでいいかな?と考えて選びました。

次世代機が普通に購入できるようになって、対応ディスプレイが安定してきたら、改めてその時買い替えを検討しています。

SwitchでフルHD以上だと画面がボヤける問題

任天堂Switchは携帯モード720p(1280×720)、TV出力時は1080p(1920×1080)です。1080pも実はネイティブ対応しておらず、NVIDIA Tegraの力で720pを1080pにアップスケールしているだけです。ここから更にWQHD(2560x1440)にアップスケール無しで縦横を引き伸ばして画面が出力される事になるので、当然引き伸ばし過ぎてボヤけた画面になります。

ドック側にあるHDMI出力端子はフルHDの1920×1080ドット,60Hz(60fps)にまで対応。1600×900ピクセルの30fps描画となるゼルダのようなタイトルだと,フルHDへのアップスケールを経て出力することになる。

https://www.4gamer.net/games/990/G999026/20170114010/

実際SwitchをWQHDでリングフィットをプレーしてみたのですが、Switch起動後のゲーム選択画面は明らかにボヤけています。しかし、実際のゲーム画面では以外とボヤけておらず、あれ、以外といけるじゃん、と感じました。勿論ジャギーは普通にあります。

どうしても気になる方は、以下のようなアップスケール装置を噛ませると良さそうです。

ただ、2021年の春頃にSwitchの後継機?次世代機?であるSwitch Proが発表されるかも!という噂があり、それが4Kや120Hzにネイティブ対応する可能性があり、アップスケール装置の購入より後継機への買い替えの方がいいのでは?という懸念があります。

液晶ディスプレイと同様、完全に時期が悪いですね・・

色々考えるのが面倒な人は

LGの↑これで大体解決すると思います。価格.com調べでは実売16万円前後です。HDMI2.1、4K、120Hz、eARC対応、アップスケーリング対応で、恐らく諸々の機能全部入りのやつですね。ただ、48インチで横幅が107cmもあるので、そこは要注意です。

総評

今年はリングフィットアドベンチャーばっかりしていた1年でした。

フィットボクシング2も並行しているので、来年はジワジワ体が締まっていけばいいな、と考えています。

GitHubの方も引き続き何らかのサンプルを継続してpushしていく予定です。

ブログに関しては正直はてなブログをやめてJamstack化しようかな?と考えてますが、はてな記法の記事をmarkdown化するプログラムを書くのが嫌過ぎて、中々手が動いていません。誰かコンバーター公開してたりしないかな。。


こんな感じで来年も緩く色々継続はしていく予定なので、よろしくお願いします 🙇‍♂️

Viewing all 140 articles
Browse latest View live