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

Yamakichi’s blog

yamakichiの技術ブログ

Rails includes joins eager_load preload merge

includes

includesはデータの先行読み込み。主に関連したmodelに対して参照を持ちたい場合に使う

N+1問題を解決する時に用いる。

Topic.includes(:tags).each { |topic| p topic.tags.map(&:name) }
#=> SELECT "topics".* FROM "topics"
#=> SELECT "topic_tags".* FROM "topic_tags"  WHERE "topic_tags"."topic_id" IN (`取得したtopicsのID`)
#=> SELECT "tags".* FROM "tags"  WHERE "tags"."id" IN (`取得したtopic_tgasのid`)

これだと効率が悪い。

SELECT "topics".* FROM "topics"
SELECT "tags".* FROM "tags" INNER JOIN "topic_tags" ON "tags"."id" = "topic_tags"."tag_id" WHERE "topic_tags"."topic_id" = `topicのidのうち一つ`

N+1

joins

joinsはシンプルに、INNER JOINしてくれる。(内部結合)

left_joinsとすると、LEFT OUTER JOINになる。

joinsはassociationをキャッシュしない。

Category.joins(:topics)
#=> SELECT  `categories`.* FROM `categories` INNER JOIN `topics` ON `topics`.`category_id` = `categories`.`id`

1レコードが1つのオブジェクトにマッピングされるため注意する必要がある。

includes + references

includesは先読み、referencesを付けることでLEFT OUTER JOINになる。 OUTER JOIN (外部結合)は片方にしか存在しないレコードも結果に含まれる。 INNER JOINよりも広い範囲でレコードを取得する。

Topic.includes(:tags).references(:tags).where('tags.topic_id < ?', 100)
#=> SELECT  DISTINCT `topics`.`id` FROM `topics` LEFT OUTER JOIN `tags` ON `tags`.`topic_id` = `topics`.`id` WHERE (tags.topic_id < 100)

eager_load

指定したassociationをLEFT OUTER JOINでキャッシュしてくれる joinsしたい場合にeager_loadを使うことでキャッシュしてくれる

preload

scope・joins・merge

Topic.joins(:tags).merge(Tag.published)

まとめ

  • includes preloadとeager_loadのハイブリット 先読みして読み込んでくれる

  • joins INNER JOIN

  • eacger_load LEFT OUTER JOIN

  • preload

  • merge

Rails ActionCacheを導入する

Rails 開発 思考の整理 Ruby

Webアプリケーションのパフォーマンス改善として、RailsのActionCacheを導入しました。

Railsのキャッシュに関する概要はRailsGuidesが一番わかりやすかったです。

Rails のキャッシュ: 概要

「キャッシュ(caching)」とは、リクエスト・レスポンスのサイクルの中で生成されたコンテンツを保存しておき、次回同じようなリクエストが発生したときのレスポンスでそのコンテンツを再利用することを指します。

キャッシュの手法は 3つ

  • ページキャッシュ (ページ単位のキャッシュ)
  • アクションキャッシュ (アクション単位のキャッシュ)
  • フラグメントキャッシュ (部品単位のキャッシュ)

今回紹介するのは「アクションキャッシュ」です。

アクションキャッシュの機能はRails3までは標準で機能として存在していましたが、Rails4からはGemとして切り出されて独立して存在しています。

actionpack-action_caching

Rails4以降で開発している場合はGemfileに記述しinstallします。

gem "actionpack-action_caching"
bundle install
class BooksController
  caches_action :index  

  def index
    @book = Book.find(1)
  end
end

これでキャッシュを導入できます。 初回アクセス時はindexメソッドが実行されますが、2回目以降は初回アクセス時にレンダリングされた結果が返ってきます。

キャッシュがなかった場合

  1. キャッシュの存在をチェックする
  2. indexアクションを実行
  3. キャッシュを生成

キャッシュがあった場合

  1. キャッシュの存在チェック
  2. キャッシュを表示

キャッシュはアクションよりも先に実行されるため、コールバックで値を取得しておく必要がある。

class BooksController
  before_action :set_book, only: :index
  caches_action :index  

  def index
  end
  
  private
    def set_book
     @book = Book.find(params[:id])
    end
end

表示される内容が変更される場合には少し工夫が必要です。

内容が更新されたにもかかわらず更新前に保存されたキャッシュが効いてしまい、最新の内容を表示することができない。という状況を想定します

その場合には、キャッシュする情報に更新情報を渡すことによって解決します。

class BooksController
  caches_action :index, cache_path: ->(c) { build_path(c, @book) }

  def index
  end
  
  private
    def build_path(controller, model: nil)
        cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller).path
        cache_path = "#{base_path}:#{model.update_at.try(:strftime, "%Y%m%d-%H%M%S")}" if model.present?
        cache_path
    end
end

@bookの値が更新されupdated_atが変更されていればcache_pathは新しくなりキャッシュ自体も変更され最新のキャッシュが呼び出されるようになるため、内容が変更されていてもキャッシュも更新されるます。

開発環境でキャッシュを有効にし、機能しているか確認する場合は以下を config/environments/*.rb に記述

config.action_controller.perform_caching = true

キャッシュを有効にし、キャッシュを効かせたいメソッドを呼び出せばキャッシュが効いていることをログで確認することができる。

その際に生成されるキャッシュファイルは、tmp/caches以下に生成されていきます。

キャッシュをexpireするオプションがあります。

expires_in: 30.minutes

とすることで30ごとにexpireされます。

class BooksController
  before_action :set_book, only: :index
  caches_action :index, cache_path: ->(c) { build_path(c, @book) }, expires_in: 30.minutes

  def index
  end
  
  private
    def set_book
    end
    
    def build_path(controller, model: nil)
        cache_path = ActionController::Caching::Actions::ActionCachePath.new(controller).path
        cache_path = "#{base_path}:#{model.update_at.try(:strftime, "%Y%m%d-%H%M%S")}" if model.present?
        cache_path
    end
end

なかなか早くなるので良いですね。

Rails CarrierWave アップロード画像を良い感じに圧縮するGem作った

開発 Rails Ruby 思考の整理 Gem

Rails CarrierWave アップロード画像を良い感じに圧縮するGem作った

Railsで画像アップロードを行うGemにCarrierWaveがあります。 そのCarrierWaveのextension的位置付けの圧縮機能を簡単に実装するGemを作りました。

carrierwave-optimize_image

Ruby Gems

作った背景

単純に画像圧縮機能が欲しくなった場面があったため作った。

あと、PageSpeed Insightsでサイトの計測を行うと画像の圧縮を推奨されるため最適化のためです。

この手のGemは実は既に何個か作られていたりして、調べた限りでは以下のGemが類似しているものです。 Gem名も似た感じですね。

これらのGemに気になる点がみつかり、自分でそれらの点を解決したものを作ろうと思ったので今回新しく自分で作りました。 それぞれで気になった点を簡単に紹介します。

  • Pietの気になった点

    実際のプロダクトにはPietを採用しました。 Pietを採用した理由は、GemのDL数の多さから信用性があると自分で判断したことと。pngquantに対応しているため圧縮効率が良い 2017/2の時点で 247,136 DL pngquantの圧縮機能はPietに付随した別のGemを流用しており、共通のインターフェースを提供していないため使い勝手が悪い。

    ``` Piet.optimize

    Piet.pngquant ```

    また、現状問題なく動いているものの付随したGemのpng_quantizatorは4年ほど前に作られており、 その後にはほとんど手もつけられていないため不安しかなかった。

  • carrierwave-imageoptimizerの気になった点

    carrierwave-imageoptimizerは最初に導入を検討していました。 2017/2の時点で 108,841 DL 圧縮効率の良いpngquantではなくoptpngを使っていた。pngquantの方がoptpngよりも半分近く圧縮できる。 その結果、パフォーマンス的に良くないため不採用 またGem内で画像拡張子による圧縮対象の分岐(png用, jpeg用)などの処理がなく。 ただ単純にimage_optimizerに処理を丸投げ(委譲)してるだけの仕様はなんか嫌だった。 もともとimage_optimizerがメインで、そのおまけ感覚でcarrierwave用のを作ったみたい

  • carrierwave-imageoptimの気になった点

    一番最近作られたGem 見た感じ色々なオプションを選択できる仕様になっていて良さそう。 今回は単純にjpegpngを効率良く圧縮できるシンプルなインターフェースを持ったものを欲しかった。 なので複雑なオプションなどは不要だったため不採用。

  • まとめると

    「CarrierWaveを簡単に拡張した画像圧縮機能で、かつ圧縮効率が良く。使い勝手の良い統一的なインターフェースを持った。 2017年2月現在の環境で安心して使うことができるGem」を目指して作ったのがcarrierwave-optimize_imageです。

導入方法は簡単で、

brew install for Mac

brew install jpegoptim pngquant

Gem install

gem "carrierwave-optimize_image"

CarrierWave

  class ImageUploader < CarrierWave::Uploader::Base
    include CarrierWave::OptimizeImage
    ...
    process :optimize
  end

optimizeで簡単に画像をアップロードするタイミングで圧縮処理がかかり、画像が圧縮されます。

今後検討したい機能

  • pngjpegそれぞれで画像の圧縮効率をオプションで変更できるようにする。現状は統一した圧縮効率 例: pngが60%圧縮 jpegが40%圧縮

  • エラー処理の対応をもっと親切なものにする

  • pngjpeg以外の拡張子にも対応していく

  • 今後さらに圧縮効率の良いものがそれらを対応していく

他にもあれば随時変更していきたい。

まとめ

  • 初めてGemを作って公開できて良い経験になった
  • Gem作成〜公開〜導入までの流れが一通り理解できた
  • 画像圧縮上手くいった
  • 課題意識とそれを解決する手段を考える意識がついた
  • 今後も何かアイデアが浮かんだときに作りたい

試しに使ってみてもらえたら嬉しいです! PullRequestや要望、フィードバックなども大歓迎です!

オブジェクト指向のこころ 読んだ まとめ

読書 思考の整理 開発

オブジェクト指向のこころを読み終えました

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

オブジェクト指向のこころ (SOFTWARE PATTERNS SERIES)

最近は最適な設計、コードの質を上げることを特に意識して開発を行っています。

まだまだ至らずなところが多いなか、以前と比べると少しづつ質は上がってきてはいるのかなと感じています。

今回は、「デザインパターンを用いたオブジェクト指向開発」について深く学びたかったため本書を手に取りました。

デザインパターンがどういうものなのかがわかった。オブジェクト指向についてもある程度理解はしている。 でも、具体的にそれらをどう組み合わせたら柔軟で堅牢なシステムを作れるのかはわからない。

そういった人にこの本は向いているかもしれません。

この本はデザインパターンオブジェクト指向の中間点を説明しています。

読み終えて、オブジェクト指向開発でよく言われている「インターフェースに対して実装を行う」ということの意味がよくわかりました。

ソフトウェア開発とは全く関係なさそうな、建築学の話が途中入ってきます。

建築学的視点を元にデザインパターンの本質を解き明かしていくのですが、これが非常にわかりやすかったです。

良い設計になるか、悪い設計になるかは視点の置き方によって変わると言われていますが。

その視点の置き方について本書では、「第8章 視野を広げる」で20ページにわたって深く解説しています。

その中でも特に重点的に語られているのが カプセル化です。

従来の考え方でいくとカプセル化とは「データの隠蔽」として考えられており、自分もそのように考えていました。 しかしここではそれを全面的に否定し、以下のように唱えていました。

カプセル化とは、「あらゆるものを隠蔽すること」であると考えるべきです。

考えるべき。という強い口調で語られていますが、読み終えた自分も全くその通りだと考えています。

具体的には、下記のように5つの隠蔽を実践することで堅牢で柔軟なシステムを作り上げていこうということです。

  • データの隠蔽
  • 実装の隠蔽
  • クラスの隠蔽
  • 設計の隠蔽
  • 実体化の隠蔽

これはつまり、「流動的な要素を見つけ出し、それをカプセル化する」ということになります。

この考え方を特に強く反映し、またそのフォースを強く持つのがStrategyパターンとBridgeパターンです。

特にBridgeパターンについては30ページかけて説明されています。

ここを読み終えた後には、Bridgeパターンの目的である

「実装から抽象的側面を切り出して、それを独立して変更できるようにする。」

ということに意味がすごくよく理解でき、かつ解決策に導くための視点の置き方の重要性に気付かされます。

第14章では「デザインパターンの原則と戦略」についてまとめられています。

  • 開放/閉鎖原則 (オープン/クローズド)

    新たな機能を個別に、すなわちモジュール化された形で追加していけるようにすることで、

    結合コストを最小化したソフトウェア設計にするということ。

  • 依存性の逆転原則

 抽象的即名は詳細に依存してはならない。詳細が抽象的側面に依存するべきである

デザインパターンを適用するための問題領域の洗い出しを行う方法に、 「共通性/可変性分析」という方法が紹介されています。

  • 共通性分析

  時間が経ってもあまり変化しないもの

  概念上の観点

  • 可変性分析

  流動的要素

  実装上の観点

読んで理解は深められたものの、まだまだ実践に落とし込めていることが少ないので何度も読み返しながら実践し続けていきたい。

ここに書かれている設計手法、視点の置き方、分析テクニックなどを用いて、 0から柔軟で堅牢なシステムを構築できるようになれば一人前のオブジェクト指向設計者になれると思う。

がんばりたい。

Reactでテストを行う まとめ その2

ES6 JavaScript 開発

実際にテストを書いていきたい

前記事で紹介したツール。

  • React
  • enzyme
  • Mocha
  • sinon
  • power-assert
  • chai
  • jsdom

アサーションライブラリは好みで選ぶ。今回はpower-assertを使ってみる。

前提

今回はReactのコンポーネントに対してのテストを行います。 かつ、ES6で記述し、Babelでトランスパイルしている。

  • ES6
  • Babel
  • React

パッケージ

下記コマンドを実行し必要なパッケージをインストールします。

npm i -D mocha enzyme react-addons-test-utilds jsdom bable-register power-assert

インストールされたパッケージ

react-addons-test-utilsは直接は利用しないが、enzymeで必要とされるのでインストール

package.jsonには以下のように追加されます

ちなみにパッケージ管理にはyarnがオススメです。

yarn

{
  "devDependencies": {
  "babel-register": "^6.22.0",
  "enzyme": "^2.7.1",
  "eslint-plugin-react": "^6.9.0",
  "jsdom": "^9.9.1",
  "mocha": "^3.2.0",
  "power-assert": "^1.4.2",
  "react-addons-test-utils": "^15.4.2",
  "sinon": "^1.17.7"
  }
}

構成

├── package.json
├── src
│   └── components
│       └── index.js
│
└── test
    ├── .setup.js
    └── components
        └── index.test.js

構成は参考までに。 testディレクトリ以下にテストファイルを置きます。 命名として index.test.jsのように拡張子の前にtestを追加します。

スクリプト

テストを実行するためにスクリプトを定義します。

{
  "scripts": {
    "test": "mocha ./app/test/*.test.js -r ./app/test/.setup.js --compilers js:babel-register"
  }
}

package.jsonに記述することにより、

npm run test

で実行することでテストを実行することができます。 -r ./app/test/.setup.jsと記述することでテストの実行前に.setup.jsをrequireし読み込んだ上でテストが実行されるようになります。

.setup.js

.setup.jsを用意します enzymeを用いてフルレンダリングのテストを行いたい場合、documentオブジェクトや、navigatorオブジェクト、windowオブジェクトにアクセスできる必要があります。 アクセスできる環境を設けるために.setup.jsではjsdomを用いて値をセットしておきます。

LINK

import { jsdom } from 'jsdom';
global.document = jsdom;
global.window = document.defaultView;
global.Element = () => {};
Object.keys(document.defaultView).forEach((property) => {
  if (typeof global[property] === 'undefined') {
    global[property] = document.defaultView[property];
  }
});

global.navigator = {
  userAgent: 'node.js'
};

Reactでテストを行う まとめ その1

ES6 React.js JavaScript 開発

Reactでテストを行う まとめ

Reactのテスト環境を構築する際に色々と調べた結果。

もうツールとか色々ありすぎてわけがわからん。

となったので。

もし自分と同じ境遇にいる人がいたら少しでも手助けになれば良いなと思い、自分が調べたことをまとめておく。

技術スタックは以下となった。

  • ES6
  • Babel
  • Browserify / Webpack
  • React
  • enzyme
  • Mocha
  • sinon
  • power-assert

ぱっと見、開発を支えてるもので8つもある。 テストでは4つのツールを上手く組み合わせて行う。

enzyme

enzyme 呼び方: エンザイム 意味: 酵素

2016年2月現在で、GithubStarが8000越え

Airbnbが作ったテスティングツール

Reactのドキュメントには 「Airbnbが作ったやつがあってこっちの方が良さげだよ」 的なことが書いてあって本家顔負けな感じ

Test Utilities

Github LINK

Document LINK

Reactのコンポーネントテストを良い感じにしてくれる。react-addon-test-utilsなどを使わずにテストできる

  • Shallow Rendering
  • Full Rendering
  • Static Rendering

Mocha

Mocha 呼び方: モカ

Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun.

スティングフレームワーク

テスト実行を担う describe/it などのテストのベースとなる機能を提供してくれる

Github LINK Document LINK

RSpecとかに似てる気がする。 describeでテストの大枠を決め、it内でテストの具体的な内容を定義する。

describe('Test Title', () => {
  it('Equal', () => {
    const two = 3;
    assert(two === 3);
  });
}

mochaはアサーションライブラリを自由に選択できる。 chaiかpower-assertで悩んだがpower-assertを採用した 理由は、なるべくシンプルに使えて学習コストが高くないものにしたかった。 power-assertとchaiを比べてみるとchaiの方が文法が豊富いろいろな書き方でテストを書くことができ、柔軟性が高いと感じた。 power-assertはシンプルにassert()だけでいけるのと。テストが失敗した時の出力結果がわかりやすかった。

追記: どうやらpower-assertはNode.jsのAssert APIを拡張したものらしい。

power-assert

power-assert 呼び方: パワーアサート

アサーションライブラリ twadaさんが開発を行っている。

Power Assert in JavaScript. Provides descriptive assertion messages through standard assert interface. No API is the best API.

Github LINK

Chai

Chai 呼び方: チャイ

アサーションライブラリ power-assertと対照的に豊富なAPIがありテストを書くことができる。

Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.

Github LINK Document LINK

Rails Validationについて

データ管理はしっかりやりたい

DBにデータが保存される時に、そのデータが正しいかどうかを検証する仕組みをバリデーションという。

バリデーションのトリガー

バリデーションが走るメソッド群

  • create
  • create!
  • save
  • save!
  • update
  • update!

上記のメソッドはオプションでバリデーションをスキップすることも可能

save(validate: false)

バリデーションがスキップされるメソッド群

  • decrement!
  • decrement_counter
  • increment!
  • increment_counter
  • toggle!
  • touch
  • update_all
  • update_attribute
  • update_column
  • update_columns
  • update_counters

Validate

通常のバリデーションです。 modelに定義して利用します。

class Recipe < ActiveRecord::Base # これでtitleがnullだとデータのInsertが行われず、弾かれます validates :title, presence: true end

valid?invalid?が用意されておりboolean型の値が返ってきます。

Recipe.new(title: "レシピタイトル").valid?
#=> true

Recipe.new(title: null).valid?
#=> false

Recipe.new(title: "レシピタイトル").invalid?
#=> false

Recipe.new(title: null).invalid?
#=> true

カスタムValidate

単体のmodelで独自のvalidationを定義し、使いたい場合に利用する。

validate link_url_cannot_be_load

def link_url_cannot_be_load
  if @obj.url.include?(BASIC_DOMAIN)
    errors[:link_url] << "でURLは利用することができません。可能ドメインか確認してください"
  end
end 

カスタムValidator

複数のmodelで独自のvalidationを定義し、使いまわしたい場合に利用する。

もしくは単体のmodelでも複数の独自validationを定義し使いたい場合に利用する。

開発者にもよるが、自分は上記の理由と定義を決めている。ここは個人の好みにもよるのかもしれない・・・

そしてRailsのapp/以下にvalidators ディレクトリを作ってその中にカスタムvalidatorをまとめておく

カスタムバリデータはActiveModel::Validatorを拡張したクラス。

クラスにはvalidateメソッドが実装されている必要がある。

class Recipe << ActiveRecord::Base
  include ActiveModel::Validations
  validates_with ContentValidator
end

class ContentValidator < ActiveModel::Validator
  # 必ずvalidateメソッドを実装する
  # 引数にはレコードを1つ取り、それに対してのバリデーションを実行する
  def validate(record)
    if record.description.blank?
      record.errors[:description] << "が空です"
    end
  end
end
  • Validates
  • カスタムValidate
  • カスタムValidator

3つのバリデーションを使いこない堅牢なシステムを構築していきたい。

データまわりの定義は厳密に決めていくことが大切だと自分は考えています。

理由としては一度データが入ったデータベースは変更が難しくなってしまうからです。

アプリケーションレイヤーのvalidation制御はもちろん。

DB側での外部キー制約、ユニークキー制約、複数の一意制約、null制約などルールをしっかり決めておくことが大切。

新規開発においては、使用がコロコロ変わるのはしょっちゅうあるのである程度変更があることを前提に開発していくと良い。

ただ、リリース段階まで行く時にはきっちりと制約周りは定義することが守るべきルールとしてある。