読者です 読者をやめる 読者になる 読者になる

has_manyのhas_manyのcountで効率よくパフォーマンスを出す

rails

counter_cacheはできるだけ使いたくないんですよ。

無駄なカラムを持ちたくないということで、頑張りました。

対策としては、最初で全てのcountを手にするのが一番いいですね。

countは重たいので、先に何とかして値を入手したい・・・。

今回のサンプルでは、Blogがあって、Commentをたくさん持っていて、さらにCommentに対して複数のReactionをつけれるようなものです。 それのgroup化したReactionの数が欲しいという状況でした。

サンプル

class Blog < ActiveRecord::Base
  has_many :comments, dependent: :destroy
  has_many :comment_reactions, through: :comments

  def comment_reactions_count_map
    comment_reactions.group(:comment_id, :reaction_id).count←これが今回の肝
=> {[1, 1]=>7,
 [1, 315]=>1,
 [1, 332]=>1,
 [1, 708]=>1,
 [1, 825]=>1,
 [1, 864]=>1,
 [1, 1038]=>1,
 [1, 1044]=>1,
 [1, 1182]=>1,
 [1, 1229]=>1,
 [1, 1288]=>1,
 [2, 57]=>1,
 [2, 82]=>1,
 [2, 220]=>1,
  end
end
class Comment < ActiveRecord::Base
  belongs_to :blog
  has_many :comment_reactions
  has_many :reactions, through: :comment_reactions
end
class CommentReaction < ActiveRecord::Base
  belongs_to :comment
  belongs_to :reaction
end
class Reaction < ActiveRecord::Base
  has_many :comment_reactions
  has_many :comments, through: :comment_reactions
end

controller側

def show
    @blog = Blog.find(params[:id])
    @count_map = @blog.comment_reactions_count_map←これでIDをセットする。
end

view側はこんな感じになりますね。

サンプルなんで、適当です。

show.html.slim

  = render partial: "blog/comments", collection: @blog.comments.includes(:reactions), as: :comment

_blog/comments.html.slim

  - if comment.reactions.present?
    - comment.reactions.group(:id).each do |reaction|
      = image_tag reaction.image.url
      = @count_map[[comment.id, reaction.id]]←これでcountの値をもらう

こんな感じでどうでしょうか。

パフォーマンスはexplainでコツコツ攻略するのも大事ですが、可能ならば、sqlのクエリを減らすのがいいですね。

ES2015のIteratorとGeneratorについて

javascript

最近はVue.jsを個人で使用しているので、javascriptについても学んでいます。

ES2015の機能を少しづつ勉強しています。

基礎能力がないと結局開発の仕方がわからないなってのがあるので、勉強がてらに記事を書きます。

Iteratorって何?

そもそもIteratorってなんぞや?って感じですよね。

イテレータ - Wikipedia

プログラミング言語的には、反復子と呼ばれているようです。

反復するためのものの意味で反復子(はんぷくし)と訳される。繰返子(くりかえし)という一般的ではない訳語もある。

繰り返されるものに対して、呼ばれるようですね。

反復子(反復可能なオブジェクト)に対して、javascriptfor..ofを使って処理ができるようです。

ということは・・・

配列

配列は反復可能なオブジェクトになります。

const programming_languages = ["javascript", "ruby", "php"]

for (let language of programming_languages) {
  console.log(language)
}
=>javascript
=>ruby
=>php

なるほど。

ただし、Iteratorオブジェクトではありません。

Iteratorはメソッドのnextを使用することができます。

Iteratorプロトコル

Iteratorであるためには、「Iteratorプロトコル」を実装する必要があります。

Symbol.iterator()

const programming_languages = ["javascript", "ruby", "php"]

const it = programming_languages[Symbol.iterator]()
console.log(it.next())
=>Object {value: "javascript", done: false}
console.log(it.next())
=>Object {value: "ruby", done: false}
console.log(it.next())
=>Object {value: "php", done: false}
console.log(it.next())
=>Object {value: undefined, done: true}

doneの真偽値で次があるかどうかがわかるんですね。

独自のIteratorを作成する

Logクラスを作成して、試してみます。

class Log {
  constructor() {
    this.messages = []
  }

  add(message) {
    const now = Date.now();
    console.log(`ログ追加:${message}(${now})`)
    return this.messages.push({ message, time: now })
  }

  [Symbol.iterator]() {
    let i = 0
    const messages = this.messages
    return {
      next: () => i >= messages.length ?
        { value: undefined, done: true } : { value: messages[i++], done: false }
    }
  }
}

const log = new Log()
log.add("foo")
log.add("bar")
log.add("baz")
console.log(log)
for (let l of log) {
  console.log(l)
=>Object {message: "foo", time: 1487771847145}
=>Object {message: "bar", time: 1487771847146}
=>Object {message: "baz", time: 1487771847147}
}

Iteratorを返すだけなら、既存のメソッドを使用した方が楽なので、既存のメソッドで書き換えます。

  [Symbol.iterator]() {
    return this.messages[Symbol.iterator]()
  }

結果は同じになります。

Generatorについて

ここでGeneratorが出てきます。

Generatorってなんぞや?って感じですよね。

関数の一種と捉えることができますが、通常の関数とは違います。

  • 関数は制御(と値)を任意の場所から呼び出し側に戻すことができる
  • Generatorを呼び出すときにはすぐには実行されず、まずはIteratorが戻される。そのあとで、Iteratorのメソッドnextを呼び出す度に実行が進む。

意味がわからないって感じなので、実際のコードと動きを見ていきます。

定義方法

Generatorを使用するにはfunctionの後に*を付けます。

呼び出し側はyieldを使用します。

rubyでもあるのに、ここでもあるのか!って思いました。

rubyの場合はブロックを返してくれるやつでしたね。

function* frame_work() {
  yield 'ruby on rails'
  yield 'vue'
  yield 'react'
}

const it = frame_work()
console.log(it.next())
=>Object {value: "ruby on rails", done: false}
console.log(it.next())
=>Object {value: "vue", done: false}
console.log(it.next())
=>Object {value: "react", done: false}
console.log(it.next())
=>Object {value: undefined, done: true}

確かにIteratorオブジェクトになっとる。

双方向コミュニケーション

yieldは式なので、評価の結果、何らかの値になります。

では、どんな値なのでしょうか。

next呼び出し時の引数の値になります。

どういうことだ?ってことで例をみます。

function* introduction() {
  const name = yield '名前は何ですか?'
  const hobby = yield '趣味は何ですか?'
  return `名前は${name}です。趣味は${hobby}です。`
}

const it = introduction()
console.log(it.next())
=>Object {value: "名前は何ですか?", done: false}
console.log(it.next('javascript'))
=>Object {value: "趣味は何ですか?", done: false}
console.log(it.next('プログラミング'))
Object {value: "名前はjavascriptです。趣味はプログラミングです。", done: true}

一個前の値を保持していますね。

この辺の遅延評価を利用して、非同期処理を行えるようですね。

どうやるんじゃろうか。

以上です。

フックメソッドについて

ruby

特定のタイミングで実行されるものです。

意味不明だと思うので、解説します。

included

includeではありません、includedです。

なんとなく、見覚えがあったのですが、違いを全く理解していませんでした。

includedメソッドは、includeメソッドによってモジュールが他のモジュールやクラスにインクルードされたあとに呼び出されます。引数にはモジュールをインクルードするクラスやモジュールが入ります。

ref.xaio.jp

どういうことか、コードで説明します。

module Foo
  def self.included(klass)
    puts "#{klass}にincludeされました"
  end
end

class Bar
  include Foo
end
=>Barにincludeされました

includeしたタイミングで上のputsが反応しています。

これを利用して、特異メソッド化する手法がよく使われます。

module Foo
  def self.included(klass)
    puts "#{klass}にincludeされました"
    klass.extend Methods
  end

  module Methods
    def foo
      'foo'
    end
  end
end

class Bar
  include Foo
end
Bar.foo
=> "foo"

extendでMethodsをBarのみに使用できるようにしています。 これでincludeした場合に特異メソッド化ができます。

includedは単なるフックメソッドとして、使用されており、inlucdeとは全く違うものでした。

ここの理解を全くしていなかったな。

特異メソッドでincludeする方法

ruby

何個か方法あるので、少しづつ紹介します。

クラス内でincludeする

module Foo
  def hello
    'hello'
  end
end

class Bar
  class << self
    include Foo
  end
end

Bar.hello
=> "hello"

特定のオブジェクトだけincludeする

x = Bar.new
class << x
  include Foo
end

x.hello
=> "hello"

Object#extend

これが一番一般的ですね。

y = Bar.new
y.extend Foo
y.hello
=> "hello"

方法としては、この3つあります。

意外にextendの用法を忘れてしまう・・・orz

クラス変数の注意点

ruby rails

クラス変数なんて、めったに使うことなんてないし、railsで使うならclass_attributeがあるんで、普通は必要ないです。

しかし、よくクラス変数やめろ!なんて言われることが多いと思うので、ここでおさらいしておきます。

クラス変数とは

そもそもクラス変数ってなんぞや?

クラス間で共有出来る変数ですね。

例えば、下記のようなものです。

class Foo
  @@x = 1

  def x
    @@x
  end

  def x=value
    @@x = value
  end
end

pry(main)> a = Foo.new
=> #<Foo:0x007f89982500e8>
pry(main)> a.x = 3
=> 3
pry(main)> b = Foo.new
=> #<Foo:0x007f8998343978>
pry(main)> b.x = 5
=> 5
pry(main)> a.x
=> 5

確かに共有されますね。

ただし、厄介なことがあります。

  • サブクラスにも継承される
  • クラス階層に属している

前者はわかりやすいと思うので、後者について説明します。

先程の続きです。

pry(main)> @@x = 6
(pry):17: warning: class variable access from toplevel
=> 6
pry(main)> b.x
=> 6

上記のように、mainクラスの時に変更したものも値が変更されてしまいます。

こういう罠が多いので、クラス変数は使うなってことになります。

けど、どうしてもクラス間で共有したいって時があります。

そういう場合はどうすればいいのかってことです。

あくまでもrails側の方法です。

class_attribute

railsではclass_attributeがあります。

これは、先程のサブクラスの継承を無しにして、なおかつトップレベルのものは参照されなくなります。

なので、railsでクラス間の値を共有したい場合は、class_attributeを使いましょう。

railsguides.jp

以上です。

mysqlの予約語に気をつけた方がいい

rails

やられました...orz

直接sqlをいじることがなかったのですが、いじった時に気付きました。

mysql> show tables;
+-----------------------------------+
| Tables_in_foo |
+-----------------------------------+
| categories                        |
| comments                          |
| rankings                          |
| reads                             |
| schema_migrations                 |
| supplementals                     |
| taggings                          |
| tags                              |
| users                             |
+-----------------------------------+

こんな感じで表示されています。

ここでreadsテーブルのデータを取得しようとします。

mysql> select * from reads;
ERROR 1064 (42000): 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 'reads' at line 1

なぜ、syntaxエラーになる???

おかしいだろ!ってなりました。

テーブルはきちんと作成されているのか?

reads.frm
reads.ibd

二つともきちんとある。

rails側では値が取得できている。

[1] pry(main)> Read.count
   (0.3ms)  SELECT COUNT(*) FROM `reads`
=> 0

わからん・・・。

試しにデータを投入しても、無事に投入できる。

ここで予約語っていう概念か?と思い調べました。

そうしたらREAD/READSが見つかりました。

ああ、そういうことかーってなりました。

予約語を使用する場合はバッククォートでエスケープすればいいです。

今思うと、上のrailssqlはきちんとバッククォートでエスケープしています。

今まで気にしなかったですが、予約語対策をしていたんですね。

MySQL :: MySQL 5.6 リファレンスマニュアル :: 9.3 予約語

cpuの情報を見る方法(mac/linux)

linux

lscpuでいけるんじゃない?って思っていたのでしたが、macは違いました。

mac

system_profiler SPHardwareDataType

    Hardware Overview:

      Model Name: MacBook Pro
      Model Identifier: MacBookPro11,1
      Processor Name: Intel Core i5
      Processor Speed: 2.4 GHz
      Number of Processors: 1
      Total Number of Cores: 2
      L2 Cache (per Core): 256 KB
      L3 Cache: 3 MB
      Memory: 8 GB
      ....

linux

lscpu

mikami@ik1-324-22232:~$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                2
On-line CPU(s) list:   0,1
Thread(s) per core:    1
Core(s) per socket:    1
Socket(s):             2
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 45
Model name:            Intel(R) Xeon(R) CPU E5-2640 0 @ 2.50GHz
Stepping:              7
CPU MHz:               2499.998
BogoMIPS:              4999.99
Hypervisor vendor:     KVM
Virtualization type:   full
L1d cache:             32K
L1i cache:             32K
L2 cache:              4096K
NUMA node0 CPU(s):     0,1
mikami@ik1-324-22232:~$ lscpu
Architecture:          x86_64
CPU op-mode(s):        32-bit, 64-bit
Byte Order:            Little Endian
CPU(s):                2
On-line CPU(s) list:   0,1
Thread(s) per core:    1
Core(s) per socket:    1
Socket(s):             2
NUMA node(s):          1
Vendor ID:             GenuineIntel
CPU family:            6
Model:                 45
Model name:            Intel(R) Xeon(R) CPU E5-2640 0 @ 2.50GHz
Stepping:              7
CPU MHz:               2499.998
BogoMIPS:              4999.99
Hypervisor vendor:     KVM
Virtualization type:   full
L1d cache:             32K
L1i cache:             32K
L2 cache:              4096K
NUMA node0 CPU(s):     0,1

linuxの方がいっぱい情報あるんで、こっちの方がいいな・・・って思いました。

ちなみにsystem_profilerは他の引数もあるので、それを渡すことで色々見れそうな感じがしました。

macのmysqlをupdateしたら、Library not loaded: /usr/local/opt/mysql56/lib/libmysqlclient.18.dylibって出るようになった

rails

なんじゃこりゃーってなりました。

なんだ昔のmysqlを見に行ってるの?

そんな古い情報見に行かないでくれよ!って感じだと思います。

原因は、gemの情報で古いmysqlの場所を見に行っているのが原因でした。

gem uninstall mysql2

ここで全てのversionを削除しました。

gem install mysql2

これでbundle exec rake db:createで無事に作成出来ました。

参考

stackoverflow.com

railsのmysqlでemojiを使う方法

rails mysql

emojiを使おうと思ったら、色々と問題があるようなので、まとめます。

文字コード問題

まず、emojiはmysqlのutf8では使用できません。 mysqlでemojiを使用するにはutf8mb4にする必要があります。

これは何をしようとしているのかいうと、emojiの文字コードは1文字に4バイト使用します。 通常のutf8では、1文字に対して3バイトまでしか使用できません。 なので、mysqlで保存するためには、文字コードを変更する必要があります。

collation問題

こちらは初期設定のutf8mb4の設定でいくと、寿司ビール(🍣🍺)問題になります。 🍣と🍺が同じものとして扱われます。 こちらを別々に検索できるようにするための設定が必要になります。

これに関連してハハパパ問題というのがあります。

utf8でもきちんと設定しておかないと、はまったりします。

ハハパパ問題(参考)

ハハパパ問題は、「ハハ」「パパ」が同一の値として扱われます。 そんなことがあるのか!って思いましたが、railsmysqlではdefaultでこの設定になっています。 何も知らないと検索結果おかしい・・・みたいな罠にはまります。 では、どのようなcollationがあるかを見ていきます。

  • utf8_bin
  • utf8_general_ci
  • utf8_unicode_ci

utf8_bin 文字コードが完全に一致するもののみ返す 一番厳しいですね

utf8_general_ci アルファベットの大文字・小文字は区別しない 他は全て区別する

utf8_unicode_ci 大文字小文字/全角半角を区別しない。

「あああ」は「アアア」「ァァァ」「ぁぁぁ」にマッチする。

railsはdefaultでutf8_unicode_ciになっています。 なので、この設定は変更しましょう。

show full columns from topics;でcollationの確認をすることができます。

mysql> show full columns from topics;
+------------------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
| Field                  | Type         | Collation       | Null | Key | Default | Extra          | Privileges                      | Comment |
+------------------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
| id                     | int(11)      | NULL            | NO   | PRI | NULL    | auto_increment | select,insert,update,references |         |
| title                  | varchar(255) | utf8_unicode_ci | NO   |     | NULL    |                | select,insert,update,references |         |
| body                   | text         | utf8_unicode_ci | NO   |     | NULL    |                | select,insert,update,references |         |
| name                   | varchar(255) | utf8_unicode_ci | NO   |     | NULL    |                | select,insert,update,references |         |
| category_id            | int(11)      | NULL            | NO   | MUL | NULL    |                | select,insert,update,references |         |
| thumbnail_updated_at   | datetime     | NULL            | YES  |     | NULL    |                | select,insert,update,references |         |
| thumbnail_file_size    | int(11)      | NULL            | YES  |     | NULL    |                | select,insert,update,references |         |
| thumbnail_content_type | varchar(255) | utf8_unicode_ci | YES  |     | NULL    |                | select,insert,update,references |         |
| thumbnail_file_name    | varchar(255) | utf8_unicode_ci | YES  |     | NULL    |                | select,insert,update,references |         |
| created_at             | datetime     | NULL            | NO   |     | NULL    |                | select,insert,update,references |         |
| updated_at             | datetime     | NULL            | NO   |     | NULL    |                | select,insert,update,references |         |
+------------------------+--------------+-----------------+------+-----+---------+----------------+---------------------------------+---------+
11 rows in set (0.00 sec)

何もしていないと、無事にutf8_unicode_ciになって、ウワって結果になると思います。

default: &default
  adapter: mysql2
  collation: utf8_general_ci

これは初期設定でやるべきことですね。

もし、運用フェーズで気づいたなら、mysql側で変更しましょう。

index問題

最大のインデックス長が757バイトになっています。 utf8では255文字で、utf8mb4では191文字になります。

解決方法としては、二つあります。

  • インデックス長を増やす
  • インデックス長を減らす

減らす方法はデフォルトの動きではならなくなるので、インデックス長を増やす方が良いです。

mysql側の設定

my.cnfを設定します。

[mysqld]
innodb_file_per_table
innodb_file_format=barracuda
innodb_large_prefix = 1

rails側の設定

database.ymlの設定

default: &default
  adapter: mysql2
  charset: utf8mb4
  encoding: utf8mb4
  collation: utf8mb4_bin
  pool: 5
  timeout: 5000

絵文字を区別したいのであれば、collationの設定をutf8mb4_binにしないといけません。 区別する必要がないのであれば、utf8mb4_general_ciで大丈夫です。

ただし、このままだとmigrationの設定がうまくいかないので、挙動を変更するようにします。

config/initializers/ar_innodb_row_format.rb

ActiveSupport.on_load :active_record do
  module ActiveRecord::ConnectionAdapters

    class AbstractMysqlAdapter
      def create_table_with_innodb_row_format(table_name, options = {})
        table_options = options.merge(:options => 'ENGINE=InnoDB ROW_FORMAT=DYNAMIC')
        create_table_without_innodb_row_format(table_name, table_options) do |td|
          yield td if block_given?
        end
      end
      alias_method_chain :create_table, :innodb_row_format
    end

  end
end

以上です。

参考

qiita.com

qiita.com

クラスメソッドをprivateメソッドにする方法

ruby

単純にprivateにするだけはダメです。

class Foo
  def self.bar
    puts 'bar'
  end

  private

  def self.baz
    puts 'baz'
  end
end

Foo.baz
=>baz

という風に実は普通に呼べています。

方法は二種類あります。

  • self内でprivateを呼ぶ
  • private_class_methodを定義する

self内privateを呼ぶ

class Foo
  def self.bar
    puts 'bar'
  end

  class << self
    private

    def baz
      puts 'baz'
    end
  end
end

Foo.baz
NoMethodError: private method `baz' called for Foo:Class
Did you mean?  bar

きちんとprivate化されていますね。

private_class_methodを定義する

class Foo
  def self.bar
    puts 'bar'
  end

  def self.baz
    puts 'baz'
  end

  private_class_method :baz
end

Foo.baz
NoMethodError: private method `baz' called for Foo:Class
Did you mean?  bar

同じ結果になっています。 ただし、後者はメソッドが定義された後に書かないといけなません。

そこは注意が必要ですね。

以上です。