新人エンジニアのつぶやき

日々の学びをアウトプットするためのブログです

【npm パッケージ】 処理速度を指標とした CSV を解析するライブラリの選定

この記事について

いいコードはいい心から

フロントエンドの CSV ファイル解析パッケージについて、2つの npm パッケージを比較しました。 今回比較したのはpapaparsecsv-parseです。比較の観点としては、3つです。高速に処理できること登録されている場合でも正常に文字列型に変換できることQiita の記事などから広く使われており、知見が多くあることです。比較した結果、条件を満たす papaparse を使用することになりました。

背景

そもそもなぜ CSV ファイルを解析するツールの選定を行うことになったか、それは私が現在携わっている農業分野で特有の「合筆」が関係しています。合筆とは複数の土地を法的に 1 つの土地に合体することです。例えば、東京都千代田区丸の内 1-2 と東京都千代田区丸の内 1-3 の2つの土地がある場合、登記簿には2つをまとめて東京都千代田区丸の内 1-2,1-3 として登録するのです。既存の実装ではこの合筆を含む CSV ファイルは文字列型に変換できず、処理が止まってしまうという問題がありました。これを解決するために、オープンソースのパッケージの中で合筆を含む CSV ファイルを解析できるものを探しました。

選定方法

npm のパッケージから CSV ファイルを解析するためのパッケージを選定しました。条件としては以下の通りです。

  • データの中身としては地番(住所のようなもの)、タイプ、管理者名、作物名、土地の面積が格納されている(例:'東京都千代田区丸の内 1-2, 1-3', '1', '丸の内太郎', 'とまと', '10')。地番は合筆で登録されている場合でも正常に文字列型に変換できること
  • 1回の処理で最低 1000 件のデータを含む CSV ファイルを処理する必要がある。その場合でも高速に処理できること。
  • Qiita の記事などから広く使われており、知見が多くあること。

3 つの条件を満たせそうなパッケージの中でpapaparsecsv-parseを選択しました。

データの確認と処理時間の計測のためのサンプルコード

以下のようなサンプルコードを作成し、データの確認と処理時間の計測を実施しました。 * papaparseの場合

import fs from "fs";
import Papa from 'papaparse';
import { performance } from "perf_hooks";

const file = fs.readFileSync("./csv/sample.csv", "utf8");

const startTime = performance.now(); // 開始時間

const result = papa.parse(file); // papaparseを用いた処理

const endTime = performance.now(); // 終了時間

console.log("計測時間[ms]:", endTime - startTime); // 何ミリ秒かかったかを表示する

console.log(result);
  • csv-parseの場合
import fs from "fs";
import { parse } from "csv-parse/sync";
import { performance } from "perf_hooks";

const file = fs.readFileSync("./csv/sample.csv", "utf8");

const startTime = performance.now(); // 開始時間

const result = parse(file, { skip_empty_lines: true }); // csv-parseを用いた処理

const endTime = performance.now(); // 終了時間

console.log("計測時間[ms]:", endTime - startTime); // 何ミリ秒かかったかを表示する

console.log(result);

csv-parse ではskip_empty_lines: trueを指定することで空行のレコードをスキップしてくれます。

計測結果

  • csv-parse の場合
計測時間[ms]:6.990803003311157
// resultの内容
  data: [
    [ '地番', 'タイプ', '管理者名', '作物', '面積(㎡)' ],
    [ '東京都千代田区丸の内1-2, 1-3', '1', '不明', '不明', '1' ],
    [ '東京都千代田区丸の内1-4, 1-5', '3', '不明', '不明', '1' ],
    [ '東京都千代田区丸の内2-1', '3', '不明', '不明', '1' ],
    [ '東京都千代田区丸の内2-2', '3', '不明', '不明', '1' ],
    ... 995 more items
  ],
  • papaparse の場合
計測時間[ms]:34.444244146347046
// resultの内容
  data: [
    [ '地番', 'タイプ', '管理者名', '作物', '面積(㎡)' ],
    [ '東京都千代田区丸の内1-2, 1-3', '1', '不明', '不明', '1' ],
    [ '東京都千代田区丸の内1-4, 1-5', '3', '不明', '不明', '1' ],
    [ '東京都千代田区丸の内2-1', '3', '不明', '不明', '1' ],
    [ '東京都千代田区丸の内2-2', '3', '不明', '不明', '1' ],
    ... 995 more items
  ],

2つとも合筆を含む CSV ファイルを解析することができた。

また計測時間の平均値を下記に示す。(n = 3) | csv-parse を用いた場合の計測時間の平均値[ms] | papaparse を用いた場合の計測時間の平均値[ms] | | -------------------------------------------- | -------------------------------------------- | | 2.9 | 37.93 |

以上の結果より、Papa Parse を使用し、実装を進めることにしました。

最後に

今回は合筆した地番が存在する CSV ファイルを解析するパッケージについての比較、処理時間の計測を実施しました。npm のパッケージでCSV Parseと調べると、267 パッケージも出てきます。react-parse-csv や vue-papa-parse などフレームワークに特化したようなものもあるようです。パッケージの選定はより難しくなっていきそうなので、個人やチームの中で基準を持っておくことが大切なのかなと感じました。

おまけ

  • データの件数ごとの計測時間の比較結果を以下に示します。データを増やすほど、papaparseの恩恵を受けやすいようです。
データの件数 papaparseを用いた場合の計測時間[ms] csv-parseを用いた場合の計測時間[ms]
10 2.356204032897949 2.863926887512207
100 2.318110942840576 7.995607137680054
1000 3.1772091388702393 30.95689105987549
3000 4.6432719230651855 49.76654601097107
5000 8.524864912033081 57.25490593910217
10000 18.785115003585815 65.55030989646912

【Flutter】 Andoroidでimage_picker を使う際はAndroidManifestを修正しよう

この記事について

いいコードはいい心から

最近、ネイティブアプリの勉強してみたいなと思い、flutterを触っています。Webアプリとは別物だなと感じ、色々な壁にぶち当たっては怪我ばかりしているのですが、今回はその中の1つです。image_pickerというライブラリについてです。こちら、OSのバージョンによって使い方が少し異なるので注意です。今回はAndroidで問題が起こったので、まとめました!Flutter、、、仲良くしたい、、、

image_pickerとは

動作確認動画

実装について

実装手順は以下の通りです。

1:プラグインをpubspec.yamlに追加する (実装当初は0.8.6を使用しているが現在は1.0.4なので、バージョン値は変更する必要がある)

dependencies:
  flutter:
    sdk: flutter

  # 画像ライブラリから画像を選択、もしくはカメラで新しい写真を撮るため
  image_picker: ^0.8.6

2:実装する

import 'dart:io';
import 'package:image_picker/image_picker.dart';
import 'package:flutter/material.dart';

class ImageAddPage extends StatefulWidget {
  const ImageAddPage({Key? key}) : super(key: key);
  @override
  State<ImageAddPage> createState() => _ImageAddPageState();
}

class _ImageAddPageState extends State<ImageAddPage> {
  XFile? _image;
  final imagePicker = ImagePicker();

  // カメラで撮影した画像を取得するメソッド
  Future getImageFromCamera() async {
    final pickedFile = await imagePicker.pickImage(source: ImageSource.camera);
    setState(() {
      if (pickedFile != null) {
        _image = XFile(pickedFile.path);
      }
    });
  }
  
  // ギャラリーから画像を取得するメソッド
  Future getImageFromGalley() async {
    final pickedFile = await imagePicker.pickImage(source: ImageSource.gallery);
    setState(() {
      if (pickedFile != null) {
        _image = XFile(pickedFile.path);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body : Padding(
        padding: const EdgeInsets.only(right: 16.0, left: 16.0),
        child: Column(
        children: [
          Center(
            child: _image == null 
            ? Container(
              width: 350,
              height: 250,
              decoration: BoxDecoration(
                color: Colors.blueGrey[100],
                borderRadius: BorderRadius.circular(10),
              ),
              child: const Icon(Icons.camera_alt_outlined,  size: 200,),
            )
            : Image.file(
                File(_image!.path),
                width: 350,
                height: 250
              )
          ),
        ],
      ),  
      ),
      floatingActionButton: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly, 
        children: [
          // カメラから取得するボタン
          FloatingActionButton(
            heroTag: 'camera',
            onPressed: getImageFromCamera,
            child: const Icon(Icons.photo_camera)

          ),
          // ギャラリーから取得するボタン
          FloatingActionButton(
            heroTag: 'gallery',
            onPressed: getImageFromGalley,
            child: const Icon(Icons.photo_album)
          )
        ]
      )
    );
  }
}

※先ほどGifで写っている成果物と一部実装が異なります。

これでビルドすることで使用することができます。

なんか動かない。。。

今回、Android 13(API レベル 33)で動作確認を行い、問題なく使えることを確認しました。そのため、安心していたのですが、端末によっては動かないものがあることが後々わかりました。調べてみると、Android 12(API レベル 31、32)以下で、image_pickerが実装されている機能を動かした場合に画面が固まってしまうという事象が発生するのです。なぜだ。。。

調査

  • ログ見るとライブラリの中でのエラーぽい→ NullPointerExceptionで落ちている

ログの詳細はこちら

05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android: Failed to handle method call
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android: java.lang.NullPointerException
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.util.Objects.requireNonNull(Objects.java:220)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.util.Arrays$ArrayList.<init>(Arrays.java:4248)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.util.Arrays.asList(Arrays.java:4235)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.f.a(Unknown Source:16)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.f.b(Unknown Source:15)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.e$a.b(Unknown Source:2)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.e.E(Unknown Source:6)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.e.J(Unknown Source:10)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at io.flutter.plugins.imagepicker.ImagePickerPlugin.f(Unknown Source:229)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at c0.j$a.a(Unknown Source:17)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.c.k(Unknown Source:18)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.c.l(Unknown Source:40)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.c.h(Unknown Source:0)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at p.b.run(Unknown Source:12)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Handler.handleCallback(Handler.java:938)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Handler.dispatchMessage(Handler.java:99)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Looper.loopOnce(Looper.java:226)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.os.Looper.loop(Looper.java:313)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at android.app.ActivityThread.main(ActivityThread.java:8582)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at java.lang.reflect.Method.invoke(Native Method)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:563)
05-02 18:21:13.616 22705 22705 E MethodChannel#plugins.flutter.io/image_picker_android:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1133)
05-02 18:21:13.619 22705 22730 I flutter : PlatformException(error, null, null, java.lang.NullPointerException
05-02 18:21:13.619 22705 22730 I flutter :  at java.util.Objects.requireNonNull(Objects.java:220)
05-02 18:21:13.619 22705 22730 I flutter :  at java.util.Arrays$ArrayList.<init>(Arrays.java:4248)
05-02 18:21:13.619 22705 22730 I flutter :  at java.util.Arrays.asList(Arrays.java:4235)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.f.a(Unknown Source:16)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.f.b(Unknown Source:15)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.e$a.b(Unknown Source:2)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.e.E(Unknown Source:6)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.e.J(Unknown Source:10)
05-02 18:21:13.619 22705 22730 I flutter :  at io.flutter.plugins.imagepicker.ImagePickerPlugin.f(Unknown Source:229)
05-02 18:21:13.619 22705 22730 I flutter :  at c0.j$a.a(Unknown Source:17)
05-02 18:21:13.619 22705 22730 I flutter :  at p.c.k(Unknown Source:18)
05-02 18:21:13.619 22705 22730 I flutter :  at p.c.l(Unknown Source:40)
05-02 18:21:13.619 22705 22730 I flutter :  at p.c.h(Unknown Source:0)
05-02 18:21:13.619 22705 22730 I flutter :  at p.b.run(Unknown Source:12)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Handler.handleCallback(Handler.java:938)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Handler.dispatchMessage(Handler.java:99)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Looper.loopOnce(Looper.java:226)
05-02 18:21:13.619 22705 22730 I flutter :  at android.os.Looper.loop(Looper.java:313)
05-02 18:21:13.619 22705 22730 I flutter :  at android.app.ActivityThread.main(ActivityThread.java:8582)
05-02 18:21:13.619 22705 22730 I flutter :  at java.lang.refl

  • Android 12のカメラが起動しないことに関して調査を進めたところ
    • キャプチャ取得にIntentクラスを使用している
    • カメラが起動しない状態でライブラリを起動しようとしても起動しないのは状態が切り替わらないから
  • 権限を付与するかの確認画面が表示されなかった..

ネイティブアプリの実装者ならば、気付いている方もいらっしゃると思いますが、AndroidManifest.xmlを修正していないためにこの事象は発生していました。

AndroidManifest.xmlというのはAndroidアプリに関する情報をAndroidのOS(システム)に伝える役目を担っており、コード実行の際に必要になる重要な情報を記述する必要があります。

ここで追記が必要なのはpermission(認可、許諾)に関する設定です。今回カメラやギャラリーを使います。この時に、ユーザーに許可をもらわないといけません。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="io.example">
    <uses-permission android:name="android.permission.CAMERA" /> 
   <application
        android:label="HOGE"
        android:name="${applicationName}"
        android:icon="@mipmap/launcher_icon">
        <activity
            android:name="io.example"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>

今回、<uses-permission android:name="android.permission.CAMERA" />を足すことで、ユーザーに対して、許可をもらうためのダイアログが表示されるようになり、許可した場合にはカメラやギャラリーにアクセスすることができるようになりました。

ちなみに、iOSの場合はInfo.plistというファイルに色々追記する必要があります。以下は例です。

<key>NSPhotoLibraryUsageDescription</key>
<string>フォトライブラリを利用する理由を記載</string>
<key>NSCameraUsageDescription</key>
<string>カメラを利用する理由を記載</string>
<key>NSMicrophoneUsageDescription</key>
<string>マイクを利用する理由を記載</string>

皆様もきちんと記載していきましょうね。。。私もちゃんと書きます。

  • AndroidManifest.xmlの修正により事象の解決には至ったのですが、ドキュメントには以下のような記載がありました。
// 原本
Android
Starting with version 0.8.1 the Android implementation support to pick (multiple) images on Android 4.3 or higher.

No configuration required - the plugin should work out of the box. It is however highly recommended to prepare for Android killing the application when low on memory. How to prepare for this is discussed in the [Handling MainActivity destruction on Android](https://github.com/flutter/packages/tree/main/packages/image_picker/image_picker#handling-mainactivity-destruction-on-android) section.

It is no longer required to add android:requestLegacyExternalStorage="true" as an attribute to the <application> tag in AndroidManifest.xml, as image_picker has been updated to make use of scoped storage.


// 日本語
バージョン0.8.1より、Android 4.3以降のAndroid実装で、画像のピック(複数枚)をサポートしました。

設定は不要で、プラグインはすぐに動作するはずです。ただし、メモリ不足の際にAndroidがアプリケーションを終了させることを想定しておくことを強くお勧めします。その方法については、「AndroidにおけるMainActivityの破壊の処理」のセクションで説明しています。

image_pickerがscoped storageを使用するように更新されたため、AndroidManifest.xmlの<application>タグの属性としてandroid:requestLegacyExternalStorage="true "を追加する必要がなくなりました。
  • ドキュメントにはandroid:requestLegacyExternalStorage="true "を追加する必要がなくなりました。と記載もあり、特に設定する必要はないように感じました。しかし、このようなことがあるので、ドキュメントを見つつ動かしていく必要があることを改めて認識しました。

最後に

ネイティブアプリの実装とWebアプリの実装は日本語習得と英語習得くらい違うと思っているので、バイリンガルになれるようにインプット、アウトプットを続けていこうと思います。

【npm パッケージ】 Apache ECharts を使ってグラフを書こう

この記事について

いいコードはいい心から

フロントエンドの データビジュアル化ツールについて紹介します。今回は Apache ECharts について書きたいと思います。chart.js の方が馴染み深いと思いますが、chart.js よりも描画できる図形の数が多く、メンテナンスも多くされているようです。今後使用していく可能性が高いので、Apache ECharts を使ってみたいと思います。

導入方法

  • echarts についてはこちらを参照してください。今回は echart を使用し、レーダーチャートを書いていきます。

  • パッケージをインストールする。今回、Vue で使用したかったので、vue-echartsをインストールしました。echartsvue-echartsをインストールしないと動かなかったので、そこは注意が必要です。

$ yarn add echarts
$ yarn add vue-echarts
  1. 実装する

  2. 実装する前にデモページで実装イメージを膨らませておくと、実装が楽になります。

chartの例

  • 今回はいちごのランクの識別を例にグラフを作成します。当然以下のような基準は実在しません。私の妄想です。
<script setup lang="ts">
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { RadarChart } from "echarts/charts";
import {
  TooltipComponent,
  LegendComponent,
  GridComponent,
} from "echarts/components";
import VChart from "vue-echarts";

use([
  CanvasRenderer,
  RadarChart,
  TooltipComponent,
  LegendComponent,
  GridComponent,
]);

const option = {
  grid: {
    containLabel: true,
  },
  legend: {
    data: ["選択したイチゴ", "ハイクラス", "スタンダード", "ベーシック"],
    align: "left",
    itemWidth: 8,
  },
  radar: {
    indicator: [
      { name: "形", max: 5 },
      { name: "糖度", max: 5 },
      { name: "色味", max: 5 },
      { name: "重さ", max: 5 },
    ],
    radius: ["0%", "55%"],
  },
  series: [
    {
      type: "radar",
      data: [
        {
          value: [5, 5, 5, 5],
          name: "ハイクラス",
        },
        {
          value: [3, 3, 3, 3],
          name: "スタンダード",
        },
        {
          value: [1, 1, 1, 1],
          name: "ベーシック",
        },
        {
          value: [3, 2, 3, 5],
          name: "選択したイチゴ",
        },
      ],
    },
  ],
};
</script>

<template>
  <div>
    <VChart class="chart" :option="option" />
  </div>
</template>

<style lang="scss" scoped>
.chart {
  width: 20vw;
  height: 40vh;
}
</style>

解説

templete タグについて

  • templete タグは以下のみです。script タグで作成した option を props で渡してあげることで図が描画されます。もし図の表示領域を変更したい場合は css で調節してください
<template>
  <div>
    <VChart class="chart" :option="option" />
  </div>
</template>

<style lang="scss" scoped>
.chart {
  width: 20vw;
  height: 40vh;
}
</style>

script タグについて

以下の部分で使用する部品をインポートします。

  • CanvasRenderer をインポートすることで図の描画を実現しています。
  • RadarChart をインポートすることで図の中でもレーダーチャートを描画することができます。円グラフならばPieChartをインポートしてあげると良いです。
  • LegendComponent をインポートすることで各種データが何を指しているかを示すことができます。
  • GridComponent をインポートすることで、図にグリッドを表示することができます。今回は指定した枠内に図が収まるようにするために使用しました。
  • TooltipComponent をインポートすることで、プロットするデータを設定することができます。
import { use } from "echarts/core";
import { CanvasRenderer } from "echarts/renderers";
import { RadarChart } from "echarts/charts";
import {
  TooltipComponent,
  LegendComponent,
  GridComponent,
} from "echarts/components";

use([
  CanvasRenderer,
  RadarChart,
  LegendComponent,
  GridComponent,
  TooltipComponent,
]);
  • 以下が具体的に表示する値を決めている部分です。
    • grid:指定した枠内に図が収まるようにするためにcontainLabelを true で設定しています。
    • legend:データの凡例を指定します。
      • dataに表示したい凡例を設定します。data の要素の数だけ、レーダーチャートの凡例が増えます。今回は凡例を 4 つとしています。
      • alignで凡例を表示する場所を指定しています。今回は左寄せで表示したかったので、leftを指定しました。
      • itemWidthで凡例の色を表す部分の表示幅を指定しています。単位は px です。初期値は 25 となっています。
    • radar:レーダーチャートのデータの最大値を設定します。
      • indicatorでデータ名とデータの最大値を表示します。今回は「形、糖度、色味、重さ」を指定しています。
      • radiuscanvas の表示領域を指定します。最初の項目は内側の領域を指定し、2 番目の項目は外側の領域を指定します。
    • series:具体的にプロットする値を指定します。
      • typeでチャートの種類を指定します。これを指定しないと、データがプロットされません。
      • dataで具体的にプロットする値を指定します。valueに各項目の値、nameにそのデータが表すサンプル名を指定します。
const option = {
  grid: {
    containLabel: true,
  },
  legend: {
    data: ["選択したイチゴ", "ハイクラス", "スタンダード", "ベーシック"],
    align: "left",
    itemWidth: 8,
  },
  radar: {
    indicator: [
      { name: "形", max: 5 },
      { name: "糖度", max: 5 },
      { name: "色味", max: 5 },
      { name: "重さ", max: 5 },
    ],
    radius: ["0%", "55%"],
  },
  series: [
    {
      type: "radar",
      data: [
        {
          value: [5, 5, 5, 5],
          name: "ハイクラス",
        },
        {
          value: [3, 3, 3, 3],
          name: "スタンダード",
        },
        {
          value: [1, 1, 1, 1],
          name: "ベーシック",
        },
        {
          value: [3, 2, 3, 5],
          name: "選択したイチゴ",
        },
      ],
    },
  ],
};

上記のコードにより、図を描画することができます。完成品はこちら。

検証動画

最後に

  • 今回はechartsを使って、レーダーチャートを作成しました。echartは豊富な種類の図を作成することができるので、他にもいろいろ作成してみようと思います。

おまけ:chart.js vs echarts

2年間のダウンロード数

chat.js vs echart

github の各項目の比較

githubのデータによる比較

こう見ると、chart.js の人気さは一目瞭然。。。。この差は果たして縮まるのか。。。

【MySQL】 ON DUPLICATE KEY UPDATE にはご用心

この記事について

いいコードはいい心から

今回はmysqlの罠についてです。便利なものほど疑えという格言が似合う体験をしたのでまとめました。 触れるのはON DUPLICATE KEY UPDATE 句についてです。INSERTとUPDATEを自動で振り分けてくれるとても便利な句ですが、実は罠が潜んでいます。 どんな罠なのか、、、、 なんと、ON DUPLICATE KEY UPDATEを含んだクエリを実行するとデッドロックがさらに発生しやすくなるというものです。 そのため、実装は避ける必要がありそうです。

今回はこのようなことがわかった経緯についてまとめました。

ON DUPLICATE KEY UPDATEとは

です。

例えば、以下のようなテーブルがあったとする

// userテーブル、ulidはPRIMARY KEYとする

ulid first_name last_name
01FVSHW3S63T5D5Q8KTT132RK3 Japan Taro
01FVSHW3S7HW03J702MAE82MQS Japan Jiro
01FVSHW3SER8977QCJBYZD9HAW Japan Saburo

そこに以下のようなクエリが実行されたとする

INSERT INTO 
  user (
    ulid
    , first_name
    , last_name
  ) VALUES
  (
    '01FVSHW3S99VWCKTQVG1EQB6CM',
    'US',
    'John'
  ),
  (
    '01FVSHW3SER8977QCJBYZD9HAW',
    'US',
    'SAM'
  );

この場合、ulidが01FVSHW3SER8977QCJBYZD9HAWのレコードはすでにテーブルに存在するため、エラーとなってしまう。 そのため、SQL側を実行する前にインサートするデータを選抜しなければならない。少し面倒ですよね そんな時にON DUPLICATE KEY UPDATE句の出番。以下のように書き換えてみる

INSERT INTO 
  user (
    ulid
    , first_name
    , last_name
  ) VALUES
  (
    '01FVSHW3S99VWCKTQVG1EQB6CM',
    'US',
    'John'
  ),
  (
    '01FVSHW3SER8977QCJBYZD9HAW',
    'US',
    'SAM'
  )
  ON DUPLICATE KEY UPDATE
  ulid = VALUES(ulid)
  , first_name = VALUES(first_name)
  , last_name = VALUES(last_name);

その結果、新規のレコードはインサートされ、既存のレコードは更新されます

ulid first_name last_name
01FVSHW3S63T5D5Q8KTT132RK3 Japan Taro
01FVSHW3S7HW03J702MAE82MQS Japan Jiro
01FVSHW3SER8977QCJBYZD9HAW US SAM
01FVSHW3S99VWCKTQVG1EQB6CM US John

とっても便利なので、よく使われてました。足下に火がついているとも知らずに。。。。

とある日にバグが見つかりました。データが登録できていないという状況でした。サーバーのログを見てみるとDeadLockが発生しており、サーバー側で500エラーが発生していました。 さらに詳しく調べていくことにしました。

調査方法

DeadLockが発生している状況を作り出しつつ、その際にどんなクエリが叩かれているかを確認することにしました。そのためにgeneral.log, slow_query_logをonにしました。またlong_query_timeを0にし、全ての処理のログを出すようにしました。 (long_query_timeで設定された 秒数 より処理時間が長いとスロークエリログの対象になる。)

mysql> set global general_log = on;
mysql> set global slow_query_log=1;
mysql> set global long_query_time=0;

その結果以下のようなログが出てきました

2023-04-04T02:52:08.644934Z 1847 Query INSERT INTO テーブル名 (
          省略
        ) VALUES (  
          省略
         ),(
          省略
         )
        ON DUPLICATE KEY UPDATE
        省略

ON DUPLICATE KEY UPDATE句を含むクエリがログに出てきた。ON DUPLICATE KEY UPDATEに関して調べてみると興味深い記事が出てきました。

Bug #98324 Deadlocks more frequent since version 5.7.26という報告によると、5.7.26 以降にアップグレードすると、ON DUPLICATE KEY UPDATEを含んだクエリでデッドロックがさらに発生するそうです。

マイナー バージョンを 5.7.25 から 5.7.26 以降にアップグレードすると、さらに多くのデッドロックが発生します。 REPLACE ステートメントを使用した単純なテスト ケースでは、アップグレード後に約 20 倍のデッドロック メッセージが発生します。 5.7.26 での変更がおそらく関連しているのではないかと思います。

13.2.6.2 INSERT ... ON DUPLICATE KEY UPDATE ステートメントにも以下のように記載れています。

複数の一意キーまたは主キーを持つテーブルに対する INSERT ... ON DUPLICATE KEY UPDATE ステートメントも安全でないとマークされます。 (Bug #11765650、Bug #58637)

今回事象が発生した際のmysqlのバージョンは5.7.38であったため、デッドロックが発生しやすい環境でした。。。 実際に動作確認し、DeadLockが起こることを確認しました。

  1. a.sqlとb.sqlを用意する

// a.sql, b.sql

begin;

INSERT INTO hoge_table (
          省略
        ) VALUES (  
          省略
         ),(
          省略
         )
        ON DUPLICATE KEY UPDATE
        省略;

commit;
  1. ターミナルを2つ準備する。
  2. ターミナルAではwhile true;do mysql -u root database_1 < a.sql; doneSQLを無限ループさせる
  3. ターミナルBではwhile true;do mysql -u root database_1 < b.sql; doneSQLを無限ループさせる

その結果、以下のようなログが出力されました

$ while true;do mysql -u root database_1 < a.sql; done
ERROR 1213 (40001) at line 3: Deadlock found when trying to get lock; try restarting transaction

大量に insert into on duplicate updateが走った際にDeadlockが引き起こされることが確認できました。

対応方針

ON DUPLICATE KEY UPDATE句が使えないため、アプリケーション側で制御する必要があります。 今回は2つの対応策を挙げました。

1. try catchで囲み、Exceptionを拾って、エラーをキャッチした場合はupdateの処理を実行するようにする

try {
  save();
} catch (e) {
  update();
}

2. update用の圃場データと、insert用の圃場データをselectで取得し、それぞれ処理を実行する

1の場合、圃場リストのデータを1つ1つsave or updateのクエリを叩く必要がある。これは処理速度の低下につながるため、避けたい。 2の場合は一括登録、一括更新することができる。

よって、2を採用することにしました。

最後に

今回の事象でmysqlにすこーーーーしだけ詳しくなった気がします。綺麗な花には棘があるというように、疑うことも必要なのだなと改めて感じました。

【フロントエンド】スマホでwebアプリを動かす場合、画面の拡大縮小を無効化する方法

この記事について

いいコードはいい心から

今回はスマホでwebアプリを動かす際の制御についてです。 人為的に画面が拡大縮小してしまう要因として主に2つ存在すると考えています。

  1. 2本指でピンチアウト、ピンチインで画面の拡大縮小する方法
  2. 1本指で画面をダブルタップする方法

上記の2つを無効化することができれば、画面が拡大縮小しないようにすることができるのではないかと考えていくつか試行錯誤しました。今回はその方法を紹介します。

2本指でピンチアウト、ピンチインで画面の拡大縮小する方法を無効化する方法

無効化する方法として2つ挙げられます。

  1. javascript側でDOMの要素にイベント発火時の処理を追加する
  2. cssで制御する

1. javascript側でDOMの要素にイベント発火時の処理を追加する場合

画面を2本指操作している場合にイベントを無効にすることで拡大縮小を無効化します

<script setup lang="ts">
import { ref, onMounted } from 'vue';

const wrapDiv = ref<HTMLElement>();

onMounted(() => {
  if (wrapDiv.value) {
    wrapDiv.addEventListener('touchmove', disableScreenScaling, false);
  }
});

/**
 * 2本指で操作する際に画面の拡大縮小を無効にする処理
 * @param event
 */
const disableScreenScaling = (event: TouchEvent) => {
  if (event.touches.length > 1) {
    event.preventDefault();
  }
};
</script>

<template>
  <div ref="wrapDiv" />
</template>

TouchEventをチェックします

面における現在のすべての接触点を表すすべての Touch オブジェクトが入ります。

とのことなので、こちらで2点以上タッチされている場合はデフォルトの動作をキャンセルすることができます。

2. cssで制御する場合

cssで制御する場合はtouch-actionを使用します 色々指定できるのですが、今回は3つ紹介します。

1: none: ブラウザーがすべてのパンやズームのジェスチャーを扱うことを無効にします。 これと使うと手っ取り早く画面の拡大縮小を無効化できますが、スクロール機能も無効化されてしまうので注意が必要です

2: pan-x: 指 1 本で水平にパンするジェスチャーを有効にします

3: pan-y: 指 1 本で垂直にパンするジェスチャーを有効にします

<template>
  <div class="wrapper-content" />
</template>

<style lang="scss" scoped>
.content-wrapper {
  touch-action: pan-y;
}
</style>

ちなみに私が対応した際はcssで一本指の場合の操作を制御しつつ、javascriptで2本指の操作を無効化するようにしました。部分的に効かないところがあったからです。

1本指で画面をダブルタップして画面の拡大縮小する方法を無効化する方法

無効化する方法として2つ挙げられます。

  1. javascript側でDOMの要素にイベント発火時の処理を追加する
  2. cssで制御する

1: javascript側でDOMの要素にイベント発火時の処理を追加する

1本指で画面をダブルタップした場合にイベントを無効にすることで拡大縮小を無効化します

<script setup lang="ts">
import { ref, onMounted } from 'vue';

const wrapDiv = ref<HTMLElement>();

onMounted(() => {
  if (wrapDiv.value) {
    wrapDiv.addEventListener('touchend', disableDoubleClick, false);
  }
});

/**
 * ダブルタップによる画面の拡大縮小を無効にする処理
 * @param event
 */
const disableDoubleClick = (event: Event) => {
  let isClicked = false;
  if (isClicked) {
    if (event.cancelable) {
      event.preventDefault();
    }
    isClicked = false;
  } else {
    isClicked = true;
    setTimeout(function () {
      isClicked = false;
    }, 500);
  }
};
</script>

<template>
  <div ref="wrapDiv" />
</template>

こちらは500ms以内に2回タップされた場合はダブルタップと認識し、デフォルトの動作をキャンセルすることができます。

2: cssで制御する方法

cssで制御する場合はtouch-actionmanipulationを使用する方法があります。

パンおよびズームのジェスチャーは有効にしますが、ダブルタップでのズームなど、標準外の追加的なジェスチャーを無効します。

と記載されています。

<template>
  <div class="wrapper-content" />
</template>

<style lang="scss" scoped>
.content-wrapper {
  touch-action: manipulation;
}
</style>

しかし、こちらを試したところ、良い効果は得られませんでした。 なので今回はjavascript側で制御する方法を採用しました。

最後に

画面を制御するだけでも色々な方法があることを知りましたし、コンポーネントによっても効いている効いていないがあるのでプログラミングというのは奥が深いなと感じました。