TES Blog

株式会社テクニカルエンジニアリングサポートに所属する社員が、自身が携わるテクノロジーやイベントに関する情報を発信しています。

Ruby on Rails の Active Record で結合先のテーブルを COUNT する場合は joins/left_joins が良い

はじめに

こんにちは、Web エンジニアの Hayato Yamashita です。
最近は暑い日が続き、35度を超える日もあるそうなので、みなさん熱中症に気をつけてくださいね。

さて、今回は Ruby on Rails の Active Record に関する記事をあげます。
Qiita でも投稿している 内容ですが、会社のブログにもあげておきます。

Active Record で COUNT する方法

Ruby on Rails の Active Record って便利ですよね。
SQL から解放されたかのようにデータアクセスできるので、心穏やかに DB と向き合えます。

でも、以下のような SQL をイメージしてデータを取得したい場合はどうでしょうか。

SELECT
  users.*,
  COUNT(thanks.id) AS thanks_count
FROM
  users
  LEFT OUTER JOIN
    thanks
  ON  users.id = thanks.user_id
GROUP BY
  users.id
;

usersthanks は親子関係のテーブルで、 1:n の関係性を持っているケースです。

users に紐づく thanks の件数を取得したいと考え、例えば以下のようなコードを書いてしまうと、よく聞く n+1 問題が発生してしまいます。

User.all.thanks.each { |thank| thank.count }

この対処法としてよく聞くのが eager_loadjoins などです。

eager_load とか joins とか includes とか、いろいろあって何がどうだったかよく分からなくなっちゃいますよね。
それぞれの特徴に関して、私はよく以下の記事を参考にしております。お世話になっております。

qiita.com

結論としては、結合先のテーブルで COUNT をしたい場合は joins が有効です。
eager_load では、結合先テーブルの全カラムを select に入れてしまい、集計関数が使えなくなってしまうためです。

なので、上記の SQL 的なデータアクセスをする場合は、以下のように書きます。

User.all.left_joins(:thanks).group(:id).select('users.*, COUNT(`thanks`.`id`) AS thanks_count')

もし、これを複数箇所で使う場合、さらに他のテーブルなどにも汎用性をもたせたい場合、以下のように Model にメソッド化しておきたいですね。

class User < ActiveRecord::Base
  scope :add_count_column, -> (join_table_name) do
    scope = current_scope || relation
    scope = scope.select("`#{table_name}`.#{Arel.star}") if scope.select_values.blank?
    scope.left_joins(join_table_name).group(:id).select('COUNT(`#{join_table_name}`.`id`) AS #{join_table_name}_count')
  end
end

上記の書き方は、以下の記事を参考にさせていただきました。ありがとうございます。

www.techscore.com

おわりに

Ruby on Rails の Active Record は良いツールではあるのですが、それだけで完結できないこともあります。
どこかで SQL を考え、パフォーマンスを意識しておかないといけないのかもしれません。

今回紹介した joins や、eager_loadincludes を使いこなせると、こうした問題に立ち向かえる力が増すと思いますよ。

Docker 18.03でMySQL5.7コンテナ起動時に[File ./ib_logfile101: 'aio write' returned OS error 122.]メッセージが表示されたときの対処法

クイックスタート・ガイド:Docker Compose と Rails — Docker-docs-ja 17.06.Beta ドキュメント

上記のドキュメントを参考にしつつ、 DockerにてRails + MySQLの環境を構築する際に、 docker-compose run 実行時に下記のエラーメッセージが表示されました。

% docker-compose up
Recreating hoge_db_1 ... done
Recreating hoge_web_1 ... done
Attaching to hoge_db_1, hoge_web_1
db_1   | Initializing database
db_1   | 2018-07-06T02:46:35.367468Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
db_1   | 2018-07-06T02:46:35.379436Z 0 [Warning] Setting lower_case_table_names=2 because file system for /var/lib/mysql/ is case insensitive
db_1   | 2018-07-06T02:46:37.409832Z 0 [ERROR] InnoDB: Operating system error number 22 in a file operation.
db_1   | 2018-07-06T02:46:37.410358Z 0 [ERROR] InnoDB: Error number 22 means 'Invalid argument'
db_1   | 2018-07-06T02:46:37.410704Z 0 [ERROR] InnoDB: File ./ib_logfile101: 'aio write' returned OS error 122. Cannot continue operation
db_1   | 2018-07-06T02:46:37.411234Z 0 [ERROR] InnoDB: Cannot continue operation.
hoge_db_1 exited with code 3

無駄だと思いつつ再度 docker-compose run を実行すると…

% docker-compose up
Recreating hoge_db_1 ... done
Recreating hoge_web_1 ... done
Attaching to hoge_db_1, hoge_web_1
db_1   | Initializing database
db_1   | 2018-07-06T02:47:39.513408Z 0 [Warning] TIMESTAMP with implicit DEFAULT value is deprecated. Please use --explicit_defaults_for_timestamp server option (see documentation for more details).
db_1   | 2018-07-06T02:47:39.517116Z 0 [ERROR] --initialize specified but the data directory has files in it. Aborting.
db_1   | 2018-07-06T02:47:39.517646Z 0 [ERROR] Aborting
db_1   |
hoge_db_1 exited with code 1

やはりダメですね😥

エラーが発生するdocker-compose.yml

上記のエラーが発生する時の docker-compose.yml の状態は、下記の通りです。

version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - .mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: hogehoge
    user: '1000:50'
  web: &web_base
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/hoge
    ports:
      - '3000:3000'
    depends_on:
      - db

調査

似たような事例を探してみると「パーミッションの問題」とか「ファイル配置がイケてない」的な意見が多かったのですが、今回は特に当てはまらず...。

MySQL docker 5.7.6 and later fails to initialize database · Issue #69 · docker-library/mysql · GitHub

調査を進めていくと、MySQLの設定に innodb_use_native_aio=0 を追加したほうが良いという書き込みを発見。

Issues with Docker db images on Windows 10 Home · Issue #55 · farmOS/farmOS · GitHub

docker-compose.ymlの修正

下記を参考にして、MySQL起動時のコマンドに上記のオプションを追加してみました。
Bountysource (ここを見る限りDockerのバージョンも怪しいですね…)

version: '3'
services:
  db:
    image: mysql:5.7
    volumes:
      - .mysql_data:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: hogehoge
    user: '1000:50'
    command: --innodb-use-native-aio=0 # <- 追加!
  web: &web_base
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    volumes:
      - .:/hoge
    ports:
      - '3000:3000'
    depends_on:
      - db

再度 docker-compose run を実行すると…無事起動することができました🐧

MySQL :: MySQL 5.7 Reference Manual :: 14.6.8 Using Asynchronous I/O on Linux
最初に出力されたログでは、Linuxの非同期I/Oを利用しようと試みた結果、volumes で指定したディレクトリ配下のファイルへの読み書きが正常に行えないことがログに出力されています。
今回はローカル環境ということでオプション指定して対処してみましたが、実運用ではまた異なる設定となるので注意したいと思います。

Unicodeの文字プロパティを指定した正規表現をみてみる(ECMAScript2018)

はじめに

Unicodeは、世界で使われる文字を利用できるようにすることを目的としています。 そのため、ラテン文字はもちろん、漢字、ハングル、キリル文字、タイ文字、(なんと!)絵文字までもがコード化されています。

一方、JavaScript(ECMAScript2018)ではUnicodeの文字プロパティにマッチングする正規表現の書き方が取り入れられます。
 関連:https://github.com/tc39/proposal-regexp-unicode-property-escapes
Unicodeの文字プロパティとは、Unicodeの規格で定められた各コードポイントの属性のことです。

まず簡単に見せると、以下のようなことができるようになります。

// 絵文字が含まれているかチェックする
/\p{Emoji}/u.test("😃");    // → true
/\p{Emoji}/u.test("絵文字"); // → false

// 平仮名が含まれているかチェックする
/\p{sc=Hiragana}/u.test("ひらがな");         // → true
/\p{sc=Hiragana}/u.test("漢字カタカナLatin"); // → false

夢が広がりますね!
本記事では、解説をしたのち、いくつかのサンプルを書いていきます。

※Unicode 10.0を前提にしています。
※本記事にあるサンプルコードは参考程度に留め、自己責任で使用してください。
※Chrome、Safari(記事投稿時点)で使えます。対応状況は、こちらでも確認できます。
http://kangax.github.io/compat-table/es2016plus/

続きを読む

社員旅行でベトナムに行ってきました!

こんにちは!普段はインフラ業務を携わらせて頂いている者です。

最近は雨が多くなってきていよいよ梅雨…と思いきや、いきなり晴れて熱くなったり忙しい天気が多いですね。 温度調整など体調には気を付けてお過ごしください!

弊社はというと…

6月にベトナムのダナンへ社員旅行に行ってまいりました!

f:id:manabkr:20180628101029j:plain
ベトナムに行きました♨

今回はその様子をお伝えしたいと思います!

続きを読む

Ansible のバージョンを下げる(Homebrew)

Ansible のダウングレード手順です(メジャーバージョンのみ)。 Homebrew で管理しているのが前提となります。

コマンドの実行結果などはないのでさくっとやりたい人向けです。

📌 環境

  • Homebrew 1.6.8
  • MacOS Sierra 10.12.6

📌 手順

1. 現在使用している Ansible とのリンクを解除する

リンク解除

brew unlink ansible

2. 過去バージョンをインストール

インストール可能な過去バージョンの確認

brew search ansible

変更したいバージョンを指定してインストール

brew install ansible@1.9

※例として1.9を指定

インストールされたか確認

brew list

インストール時に指定したバージョンの Ansible がリストにあればOK。

3. インストールした Ansible にリンクする

リンクする

brew link ansible@1.9

※例として1.9を指定

バージョン確認

ansible --version

ここまででバージョンを下げる対応は終わりです。

📌 備考

Ansible のバージョン変更を元に戻したい時

現在使ってる Ansible のリンクを解除

brew unlink ansible@1.9

※例として1.9を指定

元の Ansible にリンクする

brew link ansible

バージョン確認

ansible --version

📌 参考

大変参考にさせていただきましたありがとうございます🙏🏻

Homebrewで過去versionをインストール | technote

📌 最後に

恐らく、メジャーバージョンのみのダウングレードであれば、 Ansible 以外のパッケージでもいけるはずです。

マイナーバージョンも指定したい場合は、他の方がたくさん記事を上げているのでそちらを参考にしてください。

2018年ロシアワールドカップのスケジュールをOutlookに連携する

はじめに

間もなく2018年ロシアワールドカップが開幕し、寝不足が続く日々が始まろうとしています。
(個人的にイタリア代表のファンなのですが、残念ながら本大会には出場できず…)

注目カードをテレビで視聴するつもりでいますが、 グループリーグの観戦スケジュールをどのように管理しようかと悩み、「スケジューラーに手入力するかー」と考えていましたが、 お手軽にOutlookの予定表で確認できる方法を見つけましたので、今回ご紹介します。

環境

今回は弊社で配布されているOffice365アカウントを使用して説明しますが、
個人利用が可能なMicrosoftアカウントでも設定可能です。

設定手順

  1. 利用可能なアカウントを用いてOffice365にログインします。
  2. ホーム画面の「Outlook」をクリックします。
  3. 画面左下の予定表アイコンをクリックします。 f:id:t-miyahara11:20180604230737p:plain
  4. 画面上部の「趣味の予定表」をクリックします。 f:id:t-miyahara11:20180604231356p:plain
  5. 画面右側からサイドメニューが表示されるので、「ブラジルW杯」をクリックします。 f:id:t-miyahara11:20180604230819p:plain
  6. ここで出場国の名称一覧が表示されます。今回は「ブラジルW杯のすべての対戦」をクリックします。 f:id:t-miyahara11:20180604230832p:plain
  7. 自分の予定表に選択した予定が組み込まれて、対戦予定が表示されます。 f:id:t-miyahara11:20180604230844p:plain

※全対戦カードは不要で、お気に入りのチームのみ確認したい場合は、個別にチームを選択してください。

おわりに

今回はワールドカップを対象に説明しましたが、よくよく見てみると色々なスポーツが連携可能ですね。
スポーツの視聴環境が整っている方は、是非活用してみてはいかがでしょうか?

良い観戦生活の一助になれば幸いです。

C# .NET Framework によるRS232C通信

はじめに

普段案件としてWebアプリケーション開発に携わることが多いですが、最近関わった案件にてRS232C通信を用いた開発を行いましたので、備忘の意味合いも込めて記事にしました。

開発環境

  • Windows 10 Pro
  • Visual Studio 2017
  • C# .NET Framework 4.5

開発準備

コントロールの要素として SerialPort を用意して、マシンに接続されているCOMを参照することになりますが、開発時に通信状況を確認するために仮想シリアルポートを用意すると、効率よく開発ができると思います。
今回の作業時には、com0comを利用しました。
こちらを用いることで、マシン内に仮想シリアルポートを用意して、WindowsFormのアプリケーションにてCOMを認識することができます。

データの受信

機器から送信されるコマンドを受信する場合については、下記のコードで実装可能です。

ポートのオープン

通信を行う前に、対象のシリアルポートのオープンを行います。

try
{
    this.serialPort.PortName = "COM4";
    this.serialPort.Open();
}
catch (Exception ex)
{
     Console.WriteLine(ex.ToString());
}

受信処理

RS232Cのポートに対するデータの受信処理は SerialPortDataReceived イベントハンドラを使用します。

private void serialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    // シリアルポートをオープンしていない場合、処理を行わない.
    if (!this.serialPort.IsOpen) return;

    try
    {
        // 受信データを読み込む.
        string receivedData = this.serialPort.ReadExisting();
        Console.WriteLine(receivedData);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

フォームの要素(LabelTextBox 等)に受信した文字列を表示する場合ですが、フォームを管理するスレッドとDataReceivedによるデータ受信を管理するスレッドは異なるため、delegate を使用して実装します。

private delegate void ShowDataDelegate();

private void SetTextBox(string data)
{
    this.textBox.Text = data;
}

private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    // シリアルポートをオープンしていない場合、処理を行わない.
    if (!this.serialPort.IsOpen) return;

    try
    {
        // 受信データを読み込む.
        string receivedData = this.serialPort.ReadExisting();
        Console.WriteLine(receivedData);
        
        ShowDataDelegate showData = new ShowDataDelegate(SetTextBox);
        this.textBox.Invoke(showData, receivedData);
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

データの送信

機器に対するコマンドを送信する場合については、下記のコードで実装可能です。

ポートのオープン

送信時も受信時と同様にポートのオープンを行います。

送信処理

送信を行う場合、SerialPortWriteメソッドを使用します。
ポイントとなるのは、送信対象の機器において改行コードの扱いが異なりますので、適宜調整が必要です。

string command = "sample code";
this.serialPort.Write(command + "\r\n");
// this.serialPort.Write(command + "\r");

単体のコマンド発行であれば上記で問題ありませんが、複数同時にコマンドを発行する場合には、書き込みに失敗するケースを考えてリトライ処理を実装すると、精度向上が見込めます。

private async void WriteSerialPort(string command)
{
    int retry = 0;

    try
    {
        this.serialPort.Write(command + "\r\n");
    }
    catch (IOException ex)
    {
        await Task.Delay(300);
        retry += 1;

        if (retry > 5) throw ex;
    }
    catch (Exception ex)
    {
        Console.WriteLine(ex.ToString());
    }
}

選択可能なポートの取得

接続されているポート一覧を取得するには、SerialPortGetPortNamesメソッドを使用します。
コンボボックスから選択できるように要素に一覧を設定する場合は、以下のコードで実装可能です。

private void InitializeSerialPort()
{
    DataTable ports = new DataTable();
    ports.Columns.Add("key", typeof(string));
    ports.Columns.Add("value", typeof(string));

    foreach (string name in SerialPort.GetPortNames())
    {
        DataRow row = ports.NewRow();
        row["key"] = name;
        row["value"] = name;
        ports.Rows.Add(row);
    }

    ports.AcceptChanges();

    this.cmbSerialPort.ValueMember = "key";
    this.cmbSerialPort.DisplayMember = "value";
    this.cmbSerialPort.DataSource = ports;
}