MySQL: エラー`this is incompatible with sql_mode=only_full_group_by`回避方法

#1055 - Expression #3 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'hoge' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by

このエラー、何度も何度も手を止められて、その都度解決策は違います。

もうムリぽ。

MySQL5.6まではこれでもイけました。
でもバージョン5.7から標準のSQL仕様になったためこのエラーを出すようになりました。

つまり今までがMySQLの方言だったのですが、もう方言で結構という場合の解決策です。

SET session sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';

を頭に入れるだけ。
SET sessionにすればold_modeに一々戻す必要がなくなります。

Node.jsならまんま、

const sqlSet = `SET session sql_mode = "STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION"`;
await conn.query(sqlSet);

const sql = `SELECT id, name FROM hoge GROUP BY foo`;
const [rows] = await conn.query(sql);

と書けばイケます。

エラーの原因ですが、例えば上の例だと、fooでGROUP BYすると複数のidとnameがあるのに、何を一つだけ表示すればいいのかわかんなーい、とSQLは言ってくるのです。

そりゃそうなんだけどテキトーでいいよ、という今までの場合のほうが圧倒的に多いってことですね。

MacにRabbitMQをインストールしてNode.js(async/await)で送受信する方法

RabbitMQとは何ゾヤ? という記事はQiitaなどに転がっているので読んでください。

要はNuxt/vue.jsなどでリアタイビューしたい時に、キューを使うことによってDBへの負荷を下げることができます。

本番環境ではサーバーにRabbitMQをインスコし、アプリケーション側で送信用、受信用のプログラムを用意することになります。

ここでは開発用にMacへインストールし、送受信するまでの工程と実際のコードです。

インストールはオフィシャルの解説どおりhomebrewで普通にインスコ。
https://www.rabbitmq.com/install-homebrew.html

上から順番にターミナルに入れるだけおk。

% brew services start rabbitmq

でスタート。

Management Plugin enabled by default at http://localhost:15672

と出てきますが、ここにアクセスすると送受信の状況を把握できる管理画面を見ることができます。
IDとパスワードの初期値はguest/guest。

send.js

const amqp = require('amqplib');

const main = async () => {
  try {
    const conn = await amqp.connect('amqp://localhost');
    const channel = await conn.createChannel();
    const queue = 'message';
    const msg = '送信するメッセージ';
    await channel.assertQueue(queue, { durable: false });
    await channel.sendToQueue(queue, Buffer.from(msg));
    console.log(" [x] Sent %s", msg);

    setTimeout(() => {
      conn.close();
      process.exit(0)
    }, 500);
  } catch (err) {
    console.error(err);
  }
};

main();

receive.js

const amqp = require('amqplib');

const main = async () => {
  try {
    const conn = await amqp.connect('amqp://localhost');
    const channel = await conn.createChannel();
    const queue = 'message';
    await channel.assertQueue(queue, { durable: false });
    console.log(" [*] Waiting for messages in %s. To exit press CTRL+C", queue);
    await channel.consume(queue, msg => {
      console.log(" [x] Received %s", msg.content.toString());
    }, { noAck: true });

    // channel.close();
    // conn.close();
  } catch (err) {
    console.error(err);
  }
}

main();

コピペして、それぞれ別々のターミナルウィンドウを開いて実行すれば確認できます。
本番ではsend.jsのsetTimeoutの部分は消すことになると思います。

Node + Nuxt.js + Ant Design VueでTABLEを作るとき、それぞれの行の値を抽出する方法

NuxtのUIコンポーネントはいくつかあって、Reactからの移植版も多いのでカオスになっていますね。

以前はVuetifyをメインで使っていたのですが、機能がまだ不足気味です。

TABLEで検索機能や固定ヘッダを使いたい場合、今のところReactからの移植版Ant Design Vue一択ではないでしょうか。

例えばIDとNAMEというカラムがあるTABLEを作っていたとします。

NAMEのリンク先をIDを使って動的にしたい場合が多々あると思うのですが、どのUIコンポーネントもどうやってリンクをはればいいのかサッパリわからないです。

Vuetifyのときもかなりググってpropsというobjectの中に入っていることを突き止めました。

Ant Designの場合は結局見つからず、マニュアルを見ながら一つ一つ確かめて、keyというobjectに入ることがわかりました。

マニュアルの下の方に一応書いてあるのですが、まったく意味がわかりませんでしたよw

実際のコードの例です。
Ant Design Vueのcustomized-filter-panelを使ったとします。

<template>
  <a-table :dataSource="data" :columns="columns">
    <div
      slot="filterDropdown"
      slot-scope="{ setSelectedKeys, selectedKeys, confirm, clearFilters, column }"
      style="padding: 8px"
    >
      <a-input
        v-ant-ref="c => searchInput = c"
        :placeholder="`Search ${column.dataIndex}`"
        :value="selectedKeys[0]"
        @change="e => setSelectedKeys(e.target.value ? [e.target.value] : [])"
        @pressEnter="() => handleSearch(selectedKeys, confirm)"
        style="width: 188px; margin-bottom: 8px; display: block;"
      />
      <a-button
        type="primary"
        @click="() => handleSearch(selectedKeys, confirm)"
        icon="search"
        size="small"
        style="width: 90px; margin-right: 8px"
      >Search</a-button>
      <a-button @click="() => handleReset(clearFilters)" size="small" style="width: 90px">Reset</a-button>
    </div>
    <a-icon
      slot="filterIcon"
      slot-scope="filtered"
      type="search"
      :style="{ color: filtered ? '#108ee9' : undefined }"
    />
    <!-- ここに設定を追加 -->
    <template slot="customRender" slot-scope="text,key">
      <span v-if="searchText">
        <!-- ここにリンク設定を追加 -->
        <nuxt-link :to="`/hoge/`+key.key">
          <template
            v-for="(fragment, i) in text.toString().split(new RegExp(`(?<=${searchText})|(?=${searchText})`, 'i'))"
          >
            <mark
              v-if="fragment.toLowerCase() === searchText.toLowerCase()"
              :key="i"
              class="highlight"
            >{{fragment}}</mark>
            <template v-else>{{fragment}}</template>
          </template>
        </nuxt-link>
      </span>
      <template v-else>
        <!-- ここにリンク設定を追加 -->
        <nuxt-link :to="`/hoge/`+key.key">{{text}}</nuxt-link>
      </template>
    </template>
  </a-table>
</template>

<script>
const data = [
  {
    key: "1",
    name: "John Brown",
    age: 32,
    address: "New York No. 1 Lake Park"
  },
  {
    key: "2",
    name: "Joe Black",
    age: 42,
    address: "London No. 1 Lake Park"
  },
  {
    key: "3",
    name: "Jim Green",
    age: 32,
    address: "Sidney No. 1 Lake Park"
  },
  {
    key: "4",
    name: "Jim Red",
    age: 32,
    address: "London No. 2 Lake Park"
  }
];

export default {
  data() {
    return {
      data,
      searchText: "",
      searchInput: null,
      columns: [
        {
          title: "Name",
          dataIndex: "name",
          key: "name",
          scopedSlots: {
            filterDropdown: "filterDropdown",
            filterIcon: "filterIcon",
            customRender: "customRender"
          },
          onFilter: (value, record) =>
            record.name
              .toString()
              .toLowerCase()
              .includes(value.toLowerCase()),
          onFilterDropdownVisibleChange: visible => {
            if (visible) {
              setTimeout(() => {
                this.searchInput.focus();
              }, 0);
            }
          }
        },
        {
          title: "Age",
          dataIndex: "age",
          key: "age",
          scopedSlots: {
            filterDropdown: "filterDropdown",
            filterIcon: "filterIcon",
            customRender: "customRender"
          },
          onFilter: (value, record) =>
            record.age
              .toString()
              .toLowerCase()
              .includes(value.toLowerCase()),
          onFilterDropdownVisibleChange: visible => {
            if (visible) {
              setTimeout(() => {
                this.searchInput.focus();
              });
            }
          }
        },
        {
          title: "Address",
          dataIndex: "address",
          key: "address",
          scopedSlots: {
            filterDropdown: "filterDropdown",
            filterIcon: "filterIcon",
            customRender: "customRender"
          },
          onFilter: (value, record) =>
            record.address
              .toString()
              .toLowerCase()
              .includes(value.toLowerCase()),
          onFilterDropdownVisibleChange: visible => {
            if (visible) {
              setTimeout(() => {
                this.searchInput.focus();
              });
            }
          }
        }
      ]
    };
  },
  methods: {
    handleSearch(selectedKeys, confirm) {
      confirm();
      this.searchText = selectedKeys[0];
    },

    handleReset(clearFilters) {
      clearFilters();
      this.searchText = "";
    }
  }
};
</script>
<style scoped>
.highlight {
  background-color: rgb(255, 192, 105);
  padding: 0px;
}
</style>

<!– ここにリンク設定を追加 –>と書いてあるところにリンクを、slot-scopeにkeyを追加しています。

slot=”customRender”とセットになっているslot-scopeにtextとkeyを追加すると、該当のtemplateで値を取り出せるようになるみたいです。

ここらへんはAnt Designの仕様みたいで、イマイチまだよくわかっていません。

住所から標高を調べる

住所から標高を調べるツールをPHPで作りました。

→住所から標高チェック

先日、なんとなく標高を調べていたのですが、標高チェックサイトで自宅を調べると「標高5m」と出てきました。
しかし、自宅近くの駅に「標高1.6m」と書かれていたのを思い出し、坂道はないのに値がおかしいとフと疑問に思いました。

標高を調べるサイトを検索してみると、Google MapのAPIを使っているサイトがほとんどで、そこでは「標高5m」と表示されます。

ところが国土交通省の国土地理院のサイトで調べると、やはり自宅の標高は1.9mでした。

災害の備えをするうえで5mと1.9mでは差が大きすぎます。
Googleと国土地理院はどうやって標高を調べているのでしょうか。

Googleはお得意の衛星写真から割り出す方法、国土地理院は航空レーザー測量、もしくは写真測量、あるいは等高線のいずれかの値だそうです。

どう考えても国土地理院のほうが正確です。
というわけで、不動産購入をお考えの皆様で標高が気になる方は、データの出処に注意しましょう。

技術的な話をすると、普通はJavaScriptで作ると思うのですが、それだと他のサイトと同じになってしまうと思い、うちではPHPで作ってみました。

PHPのほうが簡単です・・・。


追記:2020年1月12日

Google Maps APIが有料となり、表示されなくなっていたことに気付きました。

Googleの地図自体は表示されるのですが、緯度経度を取得できなくなっていたので、Yahoo!ジオコーダAPIに変更しました。

標高のデータは国土地理院のままです。

ですので複雑ですが、地図そのものはGoogle、住所から緯度経度を取得しているのはYahoo!ジオコーダAPI、標高は国土地理院のAPIから取得しています。
 

Mac:CatalinaでPHPを動かす設定まとめ

MacのCatalinaをクリーンインストールしました。
前の環境に戻すためPHPとphpMyAdminを入れるための設定をまとめておきます。

PHPはCatalinaに元々入っている7.3.9を使います。

ちなみにですが、MacはCatalinaからbashではなくてzshに変わっているので要注意です。
.bash_profileもまんま.zshrcに変わっています。

■Apacheを起動
後々いろいろ面倒なので先にApacheの起動確認をしておく。

/private/etc/apache2/httpd.conf

ファイルをコピーして一応オリジナルを保管。
httpd.confの186行目辺りの

#LoadModule php7_module libexec/apache2/libphp7.so

コメントアウトを外す。
254行目、255行目辺りの2行にrootディレクトリを設定する場所があるので、

DocumentRoot "/Users/ユーザー名/Sites"
<Directory "/Users/ユーザー名/Sites">

などに変更し、rootにSitesディレクトリを作成、仮のindex.htmlファイルを作っておく。

Sitesフォルダを自動的に「サイト」に変更したい場合はSitesディレクトリに以下の非表示ファイルを作ればおk。

touch ~/Sites/.localized

拡張子phpのファイルをindexにしたい場合は、289行目辺りにindex.phpを追加。

DirectoryIndex index.html index.php

ターミナルで、

sudo apachectl start

すればApache起動。
再起動ならstartをrestart。

http://localhost/にアクセスすると先程のindex.htmlが表示されるはず。

 
■php.iniを設定

/private/etc

にphp.ini.defaultがあるのでコピーしてオリジナルは残しておく。
名前をphp.iniに変更。

1,055行目と1,195行目辺りをそれぞれ以下のように修正。

pdo_mysql.default_socket= "/tmp/mysql.sock"
mysqli.default_socket = "/tmp/mysql.sock"

ここはMacオリジナル設定かと。

 
■phpMyAdminインストール
https://www.phpmyadmin.net/
からダウンロードしてまんまSitesディレクトリに移動。
名前を「phpMyAdmin」にでも変更しておく。

/phpMyAdmin/config.sample.inc.php

をコピーしてオリジナルは残す。
ファイル名をconfig.inc.phpに修正し、18行目辺り、

/* YOU MUST FILL IN THIS FOR COOKIE AUTH! */

とあるところにハッシュ代わりにテキトーな文字列を入れておく。
こんな↓感じ

ww6jghiuaweyrw8743h7nkjt9aieryuqwb563y9wruafiy

34行目辺りのfalseをtrueに変更してパスワードなしでログインできるようにしておく。

$cfg['Servers'][$i]['AllowNoPassword'] = true;

http://localhost/phpmyadmin/
にアクセス。
で、いつものように以下のエラー。

$cfg[‘TempDir’] (/Users/hoge/Sites/phpmyadmin/tmp/) にアクセスできません。phpMyAdmin はテンプレートをキャッシュすることができないため、低速になります。

phpMyAdminにtmpフォルダがないため新規作成。
tmpフォルダをコマンド+iで「共有とアクセス権」everyoneに読み/書きOKにする。

 
以上、これが最短距離かと思われます。