しおしお

IntelliJ IDEAのことなんかを書いてます

VueRouterでroute変更時にaxiosで実行中のリクエストをすべてキャンセルする

やりたいこと

VueRouterでroute変更時に、前のページで投げられていたリクエストを一括でキャンセルしたい。 これが出来ると、遷移前のページで大量にリクエストが投げられていた場合、遷移後のページのcreatedで実行するリクエストがすぐに実行出来るようになる。

サンプルコード

リクエストをキャンするするためのトークンなどを作る

リクエストをキャンするするためには、axiosでリクエストを投げる時にキャンセルトークンを設定する必要があるので、それを生成するクラスを作ってあげる。 リクエストキャンセル時には、以降のリクエストでは新しいキャンセルトークンが使えるようにしています。*1

import axios, { CancelTokenSource } from 'axios'

class RequestCanceler {
  private source: CancelTokenSource = axios.CancelToken.source()

  cancel() {
    this.source.cancel()
    this.source = axios.CancelToken.source()
  }

  token() {
    return this.source.token
  }
}
const requestCanceler = new RequestCanceler()

export {
  requestCanceler
}

axiosでリクエストを投げる

axiosでリクエストを投げるときには、configcancelTokenを設定してあげます。 キャンセルが行われると例外が上がるので、axios.isCancelを使ってキャンセルによる失敗なのかそれ以外なのかを判定してあげる必要があります。

import { requestCanceler } from './api'
import axios from 'axios'

axios.get(`http://localhost:8080/test`, {cancelToken: requestCanceler.token()}).then(res => {
  console.log(res.data)
}).catch(reason => {
  if (axios.isCancel(reason)) {
    console.log('cancel!!!!')
  } else {
    console.log('cancelじゃないよ')        
  }
})

route変更時にリクエストを一括キャンセルする

VueRouterbeforeEachでキャンセル処理を呼び出してリクエストを一括キャンセルしてあげます。

router.beforeEach((to, from, next) => {
  console.log('beforeEach')
  requestCanceler.cancel()
  next()
})

動かしてみた結果

VueRouterbeforeEachのログが出力された後に大量のcancel!!!が出力されているので、 キャンセル処理が動いてエラー処理でキャンセル処理の分岐に入っていることがわかりますね。

f:id:sioiri:20200223065955p:plain:w300

参考情報

*1:新しいトークンに変えないと、タイミングの問題なのか直後に投げるリクエストがキャンセル対象になったりしてしまいます…

Gradle Dependencies HelperプラグインをKotlin DSL対応したよ

Gradle Dependencies Helper - Plugins | JetBrainsのKotlin DSL対応したよ。 これで、今までGroovy DSLでしか出来ていなかったArtifactやVersion番号の候補表示がKotlin DSLでも出来るようになるよ。

こんな感じのことができるよ。 f:id:sioiri:20200218064403g:plain

2020.1 (eap)の対応としているので、最新のEAPを落としてくるとプラグインがインストール出来ます。

Vue.jsのrouteパラメータを$router.paramsではなくpropsで受け取る

やりたいこと

routeパラメータを、コンポーネント側で$router.paramsを使って取り出すのではなく、propsに代入してもらいコンポーネントvue-routerに依存しないようにする。

お試しコード

ルート定義

ルート定義する際に、 props: true を追加してあげます。 これをすることで、コンポーネントのプロパティでrouteパラメータを受け取れるようになります。

const routes = [
  { path: '/hello/:message', name: 'hello', component: HelloWorld, props: true }
]

コンポーネント側のコード

routeパラメータを受け取るプロパティを定義してあげるだけですね。 検証用に、createdでプロパティの内容を出力してみます。

import { Component, Prop, Vue } from 'vue-property-decorator';

@Component
export default class HelloWorld extends Vue {
  @Prop() private message!: string;

  created() {
    console.log('message', this.message)
  }
}

動かしてみた結果

hello/hogeにアクセスすると、コンソールにhogeが出力されているので、プロパティでrouteパラメータが受け取れていることが確認できます。 f:id:sioiri:20200209070057p:plain

お試しコード(任意のオブジェクトで受け取る)

propsでrouteパラメータの値をオブジェクトで受け取ってみたいと思います。

コンポーネント側のコード

プロパティの型を、stringからMessage型に変更してみます。 Message型には、idとmessageのプロパティを定義してみます。

import { Component, Prop, Vue } from 'vue-property-decorator';

interface Message {
  id: number
  message: string
}

@Component
export default class HelloWorld extends Vue {
  @Prop() private message!: Message

  created() {
    console.log('message', this.message)
  }
}

ナビゲーションのコード

ナビゲーション時に、paramsにコンポーネントが要求するオブジェクトを設定してあげます

<router-link :to="{name: 'hello', params:{message: {id: 999, message: 'メッセージ'}}}">Hello!!</router-link>

動かしてみた結果

コンソールにrouteパラメータが出力出来ているので、オブジェクトでの受け渡しが出来ていることが確認できますね。 f:id:sioiri:20200209071703p:plain

Vue-Routerへの依存をなくした場合のメリット

テストを考えた場合、コンポーネントがVue-Routerに依存している場合($router.paramsへ依存している場合)、モックを定義してあげないとテストができなくなってしまいます。 $router.paramsを使わずにプロパティー経由で受け取った場合、下のテストコードのように、propsDataをセットアップするだけでテストが実行可能となります。

describe('HelloWorld', () => {
  it('test', () => {
    const wrapper = shallowMount(HelloWorld, {
      propsData: {
        message: {
          id: 1, message: 'メッセージ'
        }
      }
    })

    expect(wrapper.text()).toContain('メッセージ')
  })
})

$route.paramsに依存している場合は、下のコードのようにmocksでデータをセットアップする必要があります。 ちょっとめんどくさいのと、コンポーネント単体だけじゃなくルート定義などを意識したテストコードになるのが嫌ですね。

describe('HelloWorld', () => {
  it('test', () => {
    const wrapper = shallowMount(HelloWorld, {
      mocks: {
        $route: {
          params: {
            message: 'メッセージ'
          }
        }
      }
    })

    expect(wrapper.text()).toContain('メッセージ')
  })
})

Spring Boot&JPAでコネクション不足になってハマったお話

起こったこと

Spring BootでJPAを使ったデータベースアクセスを行っているアプリケーションで、 データベースアクセス後(特定の条件時のみデータベースアクセスが行われていた…)に遅いAPI呼び出しを行った結果コネクションプール不足となった。

原因にたどり着くまでめっちゃデバッグしたりログ仕込んだりして大変だったので(3時間ぐらいかかった)、ちっちゃいアプリ作って検証や対応方法などを考えてみる。

検証用アプリ

コントローラ

ざっくりとアプリケーションの内容は下のような感じ。 JPAでデータベースアクセスを行ったあとに遅い処理を行っている。検証用なのでAPI呼び出しの代わりにスリープ処理を入れてます。

@RestController
@RequestMapping("/sample")
class SampleController(private val sampleRepository: SampleRepository) {

    @GetMapping
    fun sample() {
        // DBアクセス
        sampleRepository.findAll()
        // 遅いAPIの代わりにスリープ
        TimeUnit.SECONDS.sleep(5)
    }
}

設定ファイル

再現しやすくするために、プール数とタイムアウト時間を調整しています。

spring.datasource.hikari.maximum-pool-size=2
spring.datasource.hikari.connection-timeout=1000

再現結果

同時に複数リクエストを投げると以下のようなエラーが発生します。タイムアウト時間まで待ってみたけど、使えるコネクション見つからなかったよってやつですね。

java.sql.SQLTransientConnectionException: HikariPool-1 - Connection is not available, request timed out after 1000ms.

原因

spring.jpa.open-in-viewという設定がデフォルトでtrueになっていて、Viewの構築時(Restの場合のjsonの構築時)まで、データベース接続が維持されていたため。 この設定値、デフォルトがtrueになっていて、明示的に指定しないと起動時にワーニングログが出力されていたけど気づかなかった…

データベースアクセス終わったタイミングでコネクション開放されると思ってたから、なかなかここにたどり着けなかった…(まだまだ知らないことがおおいなー)

試しに、コントローラの最後でアクティブなコネクション数を出してみると、スリープ後もコネクションがプールに戻っていないことが確認できる。

@RestController
@RequestMapping("/sample")
class SampleController(private val sampleRepository: SampleRepository, private val dataSource: HikariDataSource) {

    @GetMapping
    fun sample() {
        val hikariPoolMXBean = dataSource.hikariPoolMXBean
        println("start: ${hikariPoolMXBean.activeConnections}")

        // DBアクセス
        sampleRepository.findAll()
        // 遅いAPIの代わりにスリープ
        TimeUnit.SECONDS.sleep(5)

        println("end: ${hikariPoolMXBean.activeConnections}")
    }
}

実行ログを確認すると、こんな感じにコントローラ終了時には返却されていないコネクションが残っていることがわかる。
f:id:sioiri:20200118072047p:plain

対応

spring.jpa.open-in-viewを無効化してみる

application.propertiesを下のように変更し、open-in-viewを無効化して、コネクションが開放されるかを確認してみる。

spring.jpa.open-in-view=false

実行ログを確認すると、先程の結果とは異なりデータベース接続がコントローラ終了時まで維持されなくなったことがわかる。
f:id:sioiri:20200118072345p:plain

これで、コネクション不足の問題は解決できるが、すでに動いているアプリケーションのこの設定値を変更した場合、間違いなくlazyなプロパティにviewが依存しているため動かなくなっちゃいますね…

該当リクエストのデータベースアクセスでJPAを使うのを諦める

例えば、下のような感じにJdbcTemplateを使うようにすることで、他の処理には影響を与えずコネクションプール不足問題を解決できるようになる。

@RestController
@RequestMapping("/sample")
class SampleController(private val jdbcTemplate: JdbcTemplate, private val dataSource: HikariDataSource) {
    
    @GetMapping
    fun sample() {
        val hikariPoolMXBean = dataSource.hikariPoolMXBean
        println("start: ${hikariPoolMXBean.activeConnections}")

        // DBアクセス
        val list = jdbcTemplate.queryForList("select * from sample")
        println("list = ${list}")
        // 遅いAPIの代わりにスリープ
        TimeUnit.SECONDS.sleep(5)

        println("end: ${hikariPoolMXBean.activeConnections}")
    }
}

まとめ

  • データベースから取得したJPAのエンティティをViewまで引き回すのやめましょう!
  • データベースアクセスをおっそいAPI呼び出しを同じリクエスト内でやるのやめましょう!
  • どうしてもやらないと行けない場合は、長時間コネクションが握られないようにしましょう!
  • とりあえず、今回はJPA使わずに他の方法でデータベースアクセスして逃げておいた…

C3.jsのドーナツチャートの中に表示されるテキストの見た目を変更する

やりたいこと

ドーナツチャートの中に表示されるテキストのフォントサイズの変更や改行などして表示したい

何も考えずにdonut.titleを使った場合

単純に値を表示したい場合は、下のコードのようにdonut.titleに値を設定する。

c3.generate({
  bindto: '#chart',
  data: {
    columns: [
        ['item1', 100],
        ['item2', 200]
    ],
    type: 'donut',
  },
  donut: {
    title: 'title'
  },
})

ドーナツチャートの中にタイトルを表示することは出来るけど、フォントのカスタマイズは出来るけど、改行して表示や行毎のフォントのカスタマイズなどは出来ない…*1
f:id:sioiri:20200111071746p:plain:w300

チャート内の値の表示方法を変更する方法

チャート生成後にコールバックされるonrenderedを使って、チャート内に表示される値をカスタマイズしてあげる。実装する内容的には、こんな感じ。

  1. チャート内に表示される値は、text.c3-chart-arcs-titleに出力されるので、d3.selectを使って、対象のDOMを抽出する*2
  2. 表示する値をtspanで囲って上げて、表示位置と表示をカスタマイズするためのクラスを設定する

実装例

c3.generate({
  bindto: '#chart',
  data: {
    columns: [
        ['item1', 100],
        ['item2', 200]
    ],
    type: 'donut',
  },
  onrendered() {
    const title = d3.select('text.c3-chart-arcs-title')
    title.html('')
    title.insert('tspan').text('title').attr('x', 0)
        .attr('dy', 0).attr('class', 'donut-chart-title-header')
    title.insert('tspan').text('値!').attr('dy', 30)
        .attr('x', 0).attr('class', 'donut-chart-title-value')
  }
})

スタイル定義

donut-chart-title-headerクラスがついている部分のみフォントサイズを変更してみる。

tspan.donut-chart-title-header {
  font-size: 3em;
}

チャート出力結果

いい感じにカスタマイズして表示出来ました!
f:id:sioiri:20200111072936p:plain:w300

最後に

僕が所属しているFORCASでは、エンジニア募集中です! ↓のnoteを読んで、少しでも興味をもってもらえたら気軽に連絡してもらえればと!
仕様変更を歓迎し、「ユーザーの理想」を最速で実装していくエンジニア|FORCAS|note

*1:htmlを書いてみたりしても、エスケープされてそのまま表示される

*2:C3使ってるのに、D3も使わないといけないのがちょっとあれな感じもありますが…

jest-whenを試してみたよ

Vue.jsのコンポーネントテストをjestを使ってやっている中で、モック用のライブラリのjest-whenを知ったので軽くお試ししてみたよ。

jest-whenとは

ざっくりこんなことが出来るようです。

  • パラメータの内容に応じて、モックが返す値を簡単に定義できる
  • Promiseを返すケースをちょっとだけ簡単に書ける

インストール

npm使って、さくっとインストールですね。

npm i --save-dev jest-when

お試ししてみる

モック化したい関数を定義する

引数を2つ受け取る関数ですね。モック化して動かすので中身はなくても大丈夫ですが…

export default function test(n1, n2) {
  return n1 + n2
}

モック化してテストするコード

  • この例の場合、calledWith1, 2を指定しているので、test(1, 2)と呼び出した場合に100を返すモックが定義される
    もし、パラメータが1, 2以外の場合は、undefinedが戻される
  • モックが返す値の定義は、mockReturnValueOnceで行っているため、1回だけ100を返す
    もし、2回以上呼び出された場合は、2回目以降はundefinedが戻される
import test from './test'
import {verifyAllWhenMocksCalled, when} from 'jest-when'
jest.mock('./test.js')

describe('test', () => {

  it('jest-when', () => {
    when(test)
      .calledWith(1, 2)
      .mockReturnValueOnce(100)

    expect(test(1, 2)).toBe(100)

    verifyAllWhenMocksCalled()
  })
})

jest-whenを使わなかった場合のテストコードはこんな感じですね。 パラメータの検証を後からやらないといけないのでちょっと面倒ですね。

  it('test', () => {
    test
      .mockReturnValueOnce(100)

    expect(test(1, 2)).toBe(100)
    expect(test.mock.calls[0]).toEqual([1, 2])
  })

実行結果

テストが成功しました(๑•̀ㅂ•́)و✧

npx jest test.spec.js
 PASS test.spec.js
  test
    ✓ jest-when (3ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.378s
Ran all test suites matching /test.spec.js/i.

試しに異なるパラメータに書き換えてテストを実行してみる

パラメータの値を変更してみます。

@@ -9,7 +9,7 @@
       .calledWith(1, 2)
       .mockReturnValueOnce(100)
 
-    expect(test(1, 2)).toBe(100)
+    expect(test(1, 3)).toBe(100)
 
     verifyAllWhenMocksCalled()
   })

モック定義時と実際の呼び出し時のパラメータが違うので、undefinedが戻されてテストが失敗しますね。

npx jest test.spec.js
 FAIL  test.spec.js
  test
    ✕ jest-when (6ms)test › jest-when

    expect(received).toBe(expected) // Object.is equality

    Expected: 100
    Received: undefined

      10 |       .mockReturnValueOnce(100)
      11 | 
    > 12 |     expect(test(1, 3)).toBe(100)
         |                        ^
      13 | 
      14 |     verifyAllWhenMocksCalled()
      15 |   })

      at Object.<anonymous> (test.spec.js:12:24)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.45s
Ran all test suites matching /test.spec.js/i.

Promiseを返すようなケースを試してみる

  • Promiseを返す場合は、戻り値の定義時にmockResolvedValueOnceを使ってあげる
  • rejectを返す場合には、mockRejectedValueOnceを使ってあげる
  it('jest-when', () => {
    when(test)
      .calledWith(1, 2)
      .mockResolvedValueOnce(100)

    expect(test(1, 2)).resolves.toBe(100)

    verifyAllWhenMocksCalled()
  })

実行結果

よさげに動きますね(๑•̀ㅂ•́)و✧

npx jest test.spec.js
 PASS  test.spec.js
  test
    ✓ jest-when (4ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.322s
Ran all test suites matching /test.spec.js/i.

まとめ

jest-whenを使うと、パラメータ毎の結果の定義が出来るので、使わなかった場合と比べてちょっとだけ簡単にモックの定義と検証が出来るかも

Vue.jsでC3.jsを使ってドーナツチャートを表示してみる

Vue.jsなアプリケーションでドーナツチャートを表示する必要があったので、C3.jsを試してみたよ。

ドーナツチャートを表示するコンポーネントの作成

C3.jsを使ってドーナツチャートを表示するためのコンポーネントを作ります。 とりあえず、ファイル名をDonutChart.vue として作ってみます。

ドーナツチャートは、C3のExampleを参考にして作ります。

<template>
  <div id="chart"></div>
</template>

<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import c3 from 'c3'

@Component
export default class DonutChart extends Vue {
  @Prop()
  data!: {key: string, count: number}[]

  mounted() {
    c3.generate({
      bindto: '#chart',
      data: {
        columns: this.data.map(value => [value.key, value.count]),
        type: 'donut'

      },
    })
  }
}
</script>

<style scoped>
  @import '~c3/c3.min.css';
</style>

DonutChart.vueを使う親コンポーネント

data1からdata4までの4つのデータを渡してチャートを表示するようにしています。

<template>
  <div id="app">
    <donut-chart :data="data"/>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import DonutChart from './components/DonutChart.vue'

@Component({
  components: {
    DonutChart,
  },
})
export default class App extends Vue {
  data = [
    {key: 'data1', count: 500},
    {key: 'data2', count: 250},
    {key: 'data3', count: 150},
    {key: 'data4', count: 100},
  ]
}
</script>

表示結果

いい感じに、ドーナツチャートが表示されました。

f:id:sioiri:20191224131518p:plain