2017年11月12日日曜日

Fine tuning (VGG16モデル) (Keras + Jupyter Notebook + tensorflow)

概要

Keras公式サイトの 
をやってみる。
少ないサンプルからディープラーニングで優位なモデルを作る。
ステップとしては、
  • スクラッチで作る。
  • bottleneck featureで学習済みモデル使う
  • Fine tuningを使う

画像の用意

まず、クラス分けする画像の用意をする。今回は猫と犬のクラス分け。
kaggleからcats and dogsのデータをダウンロードする。このデータセットは25000枚あるが、今回は少ないデータセットでのモデル構築が目的なので、トレーニングデータとしてcats, dogs 1000枚ごと、テストデータとして400枚ごと取り出してフォルダに分ける。

- cats_and_dogs_1000
  - train
    - cats (1000枚)
    - dogs (1000枚)
  - validation
    - cats (400枚)
    - dogs (400枚)

切り分けたコマンド
<code>
ll dog* | head -1000 | awk '{print $9}' | xargs -i cp -p {} ../../cats_and_dogs_1000/train/dogs/
ll cat* | head -1000 | awk '{print $9}' | xargs -i cp -p {} ../../cats_and_dogs_1000/train/cats/
ll dog* | tail -400 | awk '{print $9}' | xargs -i cp -p {} ../../cats_and_dogs_1000/validation/dogs/
ll cat* | tail -400 | awk '{print $9}' | xargs -i cp -p {} ../../cats_and_dogs_1000/validation/cats/
<code>

前処理とデータ増加

画像を少し加工しながらデータを増やす。これには過学習を防いでモデルを、一般化する効果があるらしい。
Kerasでは keras.preprocessing.image.ImageDataGenerator class を使ってこれを実現できる。

必要なライブラリのインポート
<code>
from keras.preprocessing.image import ImageDataGenerator
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D
from keras.layers import Activation, Dropout, Flatten, Dense
from keras import backend as K
<code>

画像増やす。
<code>
# this is the augmentation configuration we will use for testing:
# only rescaling
test_datagen = ImageDataGenerator(rescale=1. / 255)

train_generator = train_datagen.flow_from_directory(
    train_data_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
    validation_data_dir,
    target_size=(img_width, img_height),
    batch_size=batch_size,
    class_mode='binary')
<code>

スクラッチの畳み込み演算トレーニング



<code>
model = Sequential()
model.add(Conv2D(32, (3, 3), input_shape=input_shape))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Flatten())
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.5))
model.add(Dense(1))
model.add(Activation('sigmoid'))

model.compile(loss='binary_crossentropy',
              optimizer='rmsprop',
              metrics=['accuracy'])
<code>

正解率は以下のようになった。

Epoch 50/50
125/125 [==============================] - 13s - loss: 0.4843 - acc: 0.7900 - val_loss: 0.4923 - val_acc: 0.7950

Bottleneck feature

学習済みモデルの最終層を削除して、そのモデルを特徴抽出として使うことをbottleneck featureと呼ぶらしい
ここはよく理解できなかったのでスキップ

Fine Tuning

最後に、FineTuningで予測して見る。
ステップとして

  • VGG16モデルを読み出して、パラメータをロードする。
  • 前で作ったモデルをトップに積んで、パラメータをロードする。
  • VGG16の層をfreezeする。
  • モデルのコンパイル
  • トレーニング


必要なKerasのクラスロード
<code>
from keras import applications
from keras.preprocessing.image import ImageDataGenerator
from keras import optimizers
from keras.models import Model
from keras.models import Sequential
from keras.layers import Dropout, Flatten, Dense
<code>

使用する変数の定義
<code>
# path to the model weights files.
weights_path = '../keras/examples/vgg16_weights.h5'
top_model_weights_path = 'bottleneck_fc_model.h5'
# dimensions of our images.
img_width, img_height = 150, 150

train_data_dir = 'images/cats_and_dogs_1000/train'
validation_data_dir = 'images/cats_and_dogs_1000/validation'
nb_train_samples = 2000
nb_validation_samples = 800
epochs = 50
batch_size = 16
<code>

VGG16モデルをロードして、bottleneck featureで作成したモデルをトップに積む。
<code>
# build the VGG16 network
base_model = applications.VGG16(weights='imagenet', include_top= False, input_shape=(150, 150, 3))
print('Model loaded.')

# build a classifier model to put on top of the convolutional model
top_model = Sequential()
top_model.add(Flatten(input_shape=base_model.output_shape[1:]))
top_model.add(Dense(256, activation='relu'))
top_model.add(Dropout(0.5))
top_model.add(Dense(1, activation='sigmoid'))

# note that it is necessary to start with a fully-trained
# classifier, including the top classifier,
# in order to successfully do fine-tuning
top_model.load_weights(top_model_weights_path)

# add the model on top of the convolutional base
# model.add(top_model)
model = Model(input= base_model.input, output= top_model(base_model.output))
<code>

最初の25レイヤーはパラメータをアップデートしないようにする。
<code>
# set the first 25 layers (up to the last conv block)
# to non-trainable (weights will not be updated)
for layer in model.layers[:25]:
    layer.trainable = False
<code>

モデルのコンパイル
<code>
# compile the model with a SGD/momentum optimizer
# and a very slow learning rate.
model.compile(loss='binary_crossentropy',
              optimizer=optimizers.SGD(lr=1e-4, momentum=0.9),
              metrics=['accuracy'])
<code>

画像データの用意
<code>
# prepare data augmentation configuration
train_datagen = ImageDataGenerator(
    rescale=1. / 255,
    shear_range=0.2,
    zoom_range=0.2,
    horizontal_flip=True)

test_datagen = ImageDataGenerator(rescale=1. / 255)

train_generator = train_datagen.flow_from_directory(
    train_data_dir,
    target_size=(img_height, img_width),
    batch_size=batch_size,
    class_mode='binary')

validation_generator = test_datagen.flow_from_directory(
    validation_data_dir,
    target_size=(img_height, img_width),
    batch_size=batch_size,
    class_mode='binary')
<code>

モデルの学習
<code>
# fine-tune the model
model.fit_generator(
    train_generator,
    samples_per_epoch=nb_train_samples,
    epochs=epochs,
    validation_data=validation_generator,
    nb_val_samples=nb_validation_samples)
<code>

 結果は以下のようになり、精度が上がったことが確認できた。

Epoch 50/50
125/125 [==============================] - 151s - loss: 0.5791 - acc: 0.9250 - val_loss: 1.1987 - val_acc: 0.8813

参考

2017年10月16日月曜日

Android Studio でプロジェクト新規作成、読み込みできない時

Macを新しいものに変えたごAndroid Studioでプロジェクトが新規作成、読み込みできなくなって調べたのでメモ

なかなかググっても解決できなかったが、Stuck over flowのこのページに答えがズバリ書いてあった。

裏でgradleのダウンロードが走っていてそれがstuckしてるように見えるようだ。
しかし10minも反応が帰ってこなければ、普通強制終了してしまうな。
「xxxダウンロード中」と表示させるだけで解決しそうな問題だ、UX, UI大事。

2017年9月2日土曜日

Web API Mockサーバをサクッと立てたいときのjson-server

フロントの開発や、サーバサイドで他Web APIを叩くときにAPI側の開発が完了してないときや、いろいろな返却値を試したいときがある。

そのときにサクッとMockを立てられるOSSがあったので紹介する。


json-server
node.jsで書かれたWeb API MockサーバでREST APIをコーディングなしでサクッと立てられる。公式に書いてあった
Get a full fake REST API with zero coding in less than 30 seconds (seriously)
 も嘘でなかった。

環境構築

必要なのはnode.js
この辺を参考にnvmを入れて環境作っておくとよい。

その後npmでjson-serverを入れる。
グローバルに入れたくない場合、まず適当なフォルダを作ってnpm initでパッケージのリスト作る。

jun-mac:json-server jun-ishioka$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.

See `npm help json` for definitive documentation on these fields
and exactly what they do.

Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.

Press ^C at any time to quit.
name: (json-seerver-test) json-server-test
version: (1.0.0)
description: for test
git repository:
keywords:
author:
license: (ISC)
About to write to /Users/jun-ishioka/temp/json-server/package.json:

{
  "name": "json-server-test",
  "version": "1.0.0",
  "description": "for test",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}


Is this ok? (yes) yes

その後json-serverを入れる。

jun-mac:json-server jun-ishioka$ npm install --save-dev json-server

いろいろパッケージが入るのでこれで完了


使用方法

単純なREST APIを構築したいときは以下のようなJsonを用意する。

{
  "users": [
    { "id": 1, "name": "kageki", "genger": "man" },
    { "id": 2, "name": "kageki_2", "genger": "man" }
  ]
}

以下コマンドで立ち上げ

jun-mac:json-server jun-ishioka$ node node_modules/json-server/bin/index.js -w db.json

  \{^_^}/ hi!

  Loading db.json
  Done

  Resources
  http://localhost:3000/users

  Home
  http://localhost:3000

  Type s + enter at any time to create a snapshot of the database
  Watching...

リクエストを投げてみるとjsonが返却される。

jun-mac:~ jun-ishioka$ curl -O GET http://localhost:3000/users
curl: Remote file name has no length!
curl: try 'curl --help' or 'curl --manual' for more information
[
  {
    "id": 1,
    "name": "kageki",
    "genger": "man"
  },
  {
    "id": 2,
    "name": "kageki_2",
    "genger": "man"
  }
]


応用的な使い方

今回自分がそうだったが、たまにPOSTリクエストだがデータ取得に使われるAPIなどがある。json-serverはRESTの原則に従っているので、POSTが投げられるとこのjson.dbにデータが追加されてしまう。

それを避けるためにPOSTをGETに曲げる必要があるが、それは独自ファイルを作り起動するとできる。

このページに詳しく書いてある。







2017年7月17日月曜日

1つのサーバに2つのrailsアプリを共存させる方法

個人で作成しているKindleセール本まとめサイトで、railsアプリを1サーバに共存させる必要が出てきたのでその方法。

概要

http://kinsume.infoにアクセスした時はrails5.0.1のアプリに飛ばして、http://kinsume.info/kinsume_blogにアクセスされた時は、rails4.2.7のアプリに飛ばしたい。
kinsume_blogで動くアプリはオープンソースのCMS refinery-cmsを使用したかったのだが、rails4.x系にしか対応していなかったので、苦肉の策ではある。

環境はUbuntu, Nginx, Unicorn。

手順

まず、すでに動いているrails5.x系, ruby 2.3.xの環境には影響を与えたくなかったので、ruby 2.2.7をインストールして、ruby周りの環境を整える。


cd ~/repos/kinsume_blog
rbenv local install 2.2.7
gem install bundler
bundle install

Unicornの設定ファイルを作成

新しく設定したいアプリの下で、vim config/unicorn.rbでファイルを新規作成。


app_path = File.expand_path(File.dirname(__FILE__) + '/..')$
$
# workerをいくつ立ち上げるか。ここではCMSであまりアクセスないことを$
# 想定していて、メモリの空きもないので1にしている。$
worker_processes 1$
$
# どのソケットで連携するかNginxの設定ファイルにも書くので覚えておく$
listen app_path + '/tmp/kinsume_blog.sock', backlog: 64$
timeout 300$
working_directory app_path$
$
# この辺もすでに動いているアプリと被らないようにする$
pid app_path + '/tmp/kinsume_blog.pid'$
stderr_path app_path + '/log/kinsume_blog.log'$
stdout_path app_path + '/log/kinsume_blog.log'$
$
preload_app true$
$
GC.respond_to?(:copy_on_write_friendly=) &&$
GC.copy_on_write_friendly = true$
$
before_fork do |server, worker|$
defined?(ActiveRecord::Base) &&$
ActiveRecord::Base.connection.disconnect!$
end$
$
after_fork do |server, worker|$
defined?(ActiveRecord::Base) &&$
ActiveRecord::Base.establish_connection$
end$

railsアプリのルートパスを変える。refinery-cmsのパスを変えるには下記ファイルを変更。

vim config/initializers/refinery/core.rb


  # Specify a different Refinery::Core::Engine mount path than the default of "/".$
  # Make sure you clear the `tmp/cache` directory after changing this setting.$
  config.mounted_path = "/kinsume_blog"$


わかりやすくなるように、静的ファイルのパスを元のアプリと変える。

vim config/environments/production.rb


  config.assets.prefix = '/static'$

Nginxの設定ファイルは以下


upstream tagosaku{$
    server unix:/home/ishioka/repos/tagosaku/tmp/tagosaku.sock fail_timeout=0;$
}$

# 先ほど作成したアプリのソケットファイルをここで指定
upstream kinsume_blog{$
    server unix:/home/ishioka/repos/kinsume_blog/tmp/kinsume_blog.sock fail_timeout=0;$
}$
$
server {$
  error_log /var/log/nginx/error.log debug;$
  listen 80;$
$
  root /home/ishioka/repos/tagosaku;$
  index index.html index.htm;$
$
  keepalive_timeout 300;$
  client_max_body_size 4G;$

 # kinsume_blogないで使っているgemから走るアクセスパスをどうしても変えられなかったので悲しみのrewriteで対応
  rewrite ^/wymiframe$ /kinsume_blog/wymiframe last;$
$
  # ここでもkinsume_blogないのgemから走るアクセスを変えられなかったので、いったんassetsを見てふぁいるがなければ/static/を見に行くように変更
  location ~ ^/assets/(.*) {$
    root /home/ishioka/repos/tagosaku/public/;$
    try_files $uri /static/$1 =404;$
  }$
$
 # staticへのアクセスはkinsume_blogの静的ファイルへのアクセスなのでロケーションを変更
  location /static/ {$
    root /home/ishioka/repos/kinsume_blog/public/;$
  }$

  location / {$
    # First attempt to serve request as file, then$
    # as directory, then fall back to displaying a 404.$
    #try_files $uri $uri/ =404;$
$
    # Uncomment to enable naxsi on this location$
    # include /etc/nginx/naxsi.rules$
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;$
    proxy_set_header Host $http_host;$
    proxy_set_header X-Forwarded_Proto $scheme;$
    proxy_redirect off;$
$
    # This passes requests to unicorn, as defined in /etc/nginx/nginx.conf$
    proxy_set_header Host $http_host;$
    proxy_pass http://tagosaku;$
    proxy_read_timeout 300s;$
    proxy_send_timeout 300s;$
  }$
$
  location /kinsume_blog {$
    # First attempt to serve request as file, then$
    # as directory, then fall back to displaying a 404.$
    #try_files $uri $uri/ =404;$
$
    # Uncomment to enable naxsi on this location$
    # include /etc/nginx/naxsi.rules$
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;$
    proxy_set_header Host $http_host;$
    proxy_set_header X-Forwarded_Proto $scheme;$
    proxy_redirect off;$
$
    # This passes requests to unicorn, as defined in /etc/nginx/nginx.conf$
    proxy_set_header Host $http_host;$
    proxy_pass http://kinsume_blog;$
    proxy_read_timeout 300s;$
    proxy_send_timeout 300s;$
  }$
$
  error_page 500 502 503 504 /500.html;$
$
  location = /500.html {$
    root /home/ishioka/repos/tagosaku/public;$
  }$
}


refinery-cmsのpluginからリクエストされる静的ファイルのパスを/から/kinsume_blogへ変更することができなかったので、nginxのrewriteを駆使して対応。 

あまりアクセスパスを変更されることを想定されてない作りっぽいので、PR出していきたい。





2017年7月6日木曜日

Ruby on rails で動くCMS Refinerycmsをセットアップする


趣味で作っているキンドルセールまとめサイトにブログコンテンツを載せたくなったので、いろいろ調べてRuby on rails で動く Refinerycmsを使用することにした。そのセットアップのメモ

環境構築

サービスがrails 4.2.xにしか対応していないので、その環境構築。現在のメイン環境ruby2.3.1 rails 5.xと共存させたいのでruby 2.7.xを入れてそこにいろいろ入れる。



# ruby 2.2.7 のインストール
rbenv install 2.2.7
rbenv rehash
rbenv global 2.2.7

# 動作に必要なgem install
gem install bundle
gem install refinerycms

# railsのプロジェクト作成
refinerycms blog_test
cd blog_test

# このフォルダは 2.2.7で動かしたいのでversion指定
touch .ruby-version; echo "2.2.7" > .ruby-version

# 開発用サーバ動かす
bundle exec rails s -b 0.0.0.0


ここまでやるとアクセスできるようになる。

この後本番環境で、既存のrailsプロジェクトと共存させたりしたいのでいろいろ設定が必要そうだが、それはまた今度

Pros And Cons

  • rails 4.x系でしか動かない(2017/07/06)
    • ただしrails 5.x系への対応PRは出ている
  • ドキュメントが古い
  • スタイル適応の仕組みがないのでデザイン変えたい時、CSS頑張るしかない。
    • これ自分的には相当面倒なんだが、他の人はどうなんやろ?
  • Rails wayから外れていないので、Rails知っている人には非常にとっつきやすい
  • 未だにメンテされている(活発ではなさそう)

2017年6月24日土曜日

Ruby on Rails Active Recordのソースコードリーディング

調べた問題

ActiveRecordでCompanyモデルに紐づくUserという物があったときに、関連のあるUserを作成するときにbuildを使って以下のようにする。その際、まだDBには保存されていないUserが次の検索ででくるか気になったので実験しつつ、ActiveRecordのソースを調べてみた。

Company.users.build(name: 'xxxxx')


調査

railsのソースコードリーディングについてここ参考にさせてもらった。AZS

上を参考にしつつ、railsのコードを落としてコードリーディング用のプロジェクトを作成した後、モデルを作成して、DBにテーブルを作る。

rails g model Company name:string
rails g model User company_id:integer name:string

bundle exec rails db:migrate RAILS_ENV=development
== 20170617020543 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0027s
== 20170617020543 CreateUsers: migrated (0.0028s) =============================

== 20170617020644 CreateCompanies: migrating ==================================
-- create_table(:companies)
   -> 0.0010s
== 20170617020644 CreateCompanies: migrated (0.0015s) =========================

そしてCompanyにCompanyとUserの関連を持たせるのと、コードを追うためのメソッドを追加。


class Company < ApplicationRecord

  has_many :users

  def self.build_users
    binding.pry
    com = Company.find_or_create_by(name: 'test_company')
    com.users.build(name: 'test_user')
    com.users.where(name: 'test_company')
  end
end

実行して追いかけてみる。buildはcollection_proxy.rbのbuildメソッドが呼ばれているもよう。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_proxy.rb @ line 316 ActiveRecord::Associations::CollectionProxy#build:

    315: def build(attributes = {}, &block)
 => 316:   @association.build(attributes, &block)
    317: end

 @associationの実態は以下のようActiveRecord::Associations::HasManyAssociationというクラスのインスタンスでフィールドにCompanyとUserに関する情報をいろいろ持っている。


[1] pry(#<User::ActiveRecord_Associations_CollectionProxy>)> @association
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."company_id" = ?  [["company_id", 1]]
=> #<ActiveRecord::Associations::HasManyAssociation:0x007f752027be50
 @association_scope=nil,
 @inversed=false,
 @loaded=false,
 @owner=#<Company:0x007f753ec2a668 id: 1, name: "test_company", created_at: Sat, 17 Jun 2017 02:30:35 UTC +00:00, updated_at: Sat, 17 Jun 2017 02:30:35 UTC +00:00>,
 @proxy=[],
 @reflection=
  #<ActiveRecord::Reflection::HasManyReflection:0x007f753f044988
   @active_record=Company(id: integer, name: string, created_at: datetime, updated_at: datetime),
   @active_record_primary_key="id",
   @association_scope_cache=
    {true=>
      #<ActiveRecord::StatementCache:0x007f753efcb060
       @bind_map=
        #<ActiveRecord::StatementCache::BindMap:0x007f753efcb5d8
         @bound_attributes=
          [#<ActiveRecord::Relation::QueryAttribute:0x007f753efc5b10
            @name="company_id",
            @original_attribute=nil,
            @type=#<ActiveModel::Type::Integer:0x007f753efa4a00 @limit=nil, @precision=nil, @range=-2147483648...2147483648, @scale=nil>,
            @value=#<ActiveRecord::StatementCache::Substitute:0x007f753efc6880>,
            @value_before_type_cast=#<ActiveRecord::StatementCache::Substitute:0x007f753efc6880>>],
         @indexes=[0]>,
       @query_builder=#<ActiveRecord::StatementCache::Query:0x007f753efcb088 @sql="SELECT \"users\".* FROM \"users\" WHERE \"users\".\"company_id\" = ?">>},
   @automatic_inverse_of=false,
   @class_name="User",
   @constructable=true,
   @foreign_key="company_id",
   @foreign_type="users_type",
   @klass=User(id: integer, company_id: integer, name: string, created_at: datetime, updated_at: datetime),
   @name=:users,
   @options={},
   @plural_name="users",
   @scope=nil,
   @scope_lock=#<Thread::Mutex:0x007f753f044618>,
   @type=nil>,
 @stale_state=nil,
 @target=[]>

Userのインスタンス化自体は、このパターンだとObjectのnewが使われていた。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/inheritance.rb @ line 65 ActiveRecord::Inheritance::ClassMethods#new:

    48: def new(*args, &block)
    49:   if abstract_class? || self == Base
    50:     raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
    51:   end
    52:
    53:   attrs = args.first
    54:   if has_attribute?(inheritance_column)
    55:     subclass = subclass_from_attributes(attrs)
    56:
    57:     if subclass.nil? && base_class == self
    58:       subclass = subclass_from_attributes(column_defaults)
    59:     end
    60:   end
    61:
    62:   if subclass && subclass != self
    63:     subclass.new(*args, &block)
    64:   else
 => 65:     super
    66:   end
    67: end

[9] pry(User)> self.parent
=> Object


こので戻ってきたメソッドないでは、Userがインスタンス化されている。


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 281 ActiveRecord::Associations::CollectionAssociation#add_to_target:

    277: def add_to_target(record, skip_callbacks = false, &block)
    278:   if association_scope.distinct_value
    279:     index = @target.index(record)
    280:   end
 => 281:   replace_on_target(record, index, skip_callbacks, &block)
    282: end

[11] pry(#<ActiveRecord::Associations::HasManyAssociation>)> record
=> #<User:0x007f753e614440 id: nil, company_id: 1, name: "test_user", created_at: nil, updated_at: nil>


before_addとかafter_addはここで呼ばれているのか、へー


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 441 ActiveRecord::Associations::CollectionAssociation#replace_on_target:

    440: def replace_on_target(record, index, skip_callbacks)
 => 441:   callback(:before_add, record) unless skip_callbacks
    442:
    443:   set_inverse_instance(record)
    444:
    445:   @_was_loaded = true
    446:
    447:   yield(record) if block_given?
    448:
    449:   if index
    450:     target[index] = record
    451:   elsif @_was_loaded || !loaded?
    452:     target << record
    453:   end
    454:
    455:   callback(:after_add, record) unless skip_callbacks
    456:
    457:   record
    458: ensure
    459:   @_was_loaded = nil
    460: end

targetというArrayにこのレコードを入れているが、最終的にこれがCompanyと関連をもって保存されるのかな?


From: /home/ishioka/repos/rails/activerecord/lib/active_record/associations/collection_association.rb @ line 452 ActiveRecord::Associations::CollectionAssociation#replace_on_target:

    440: def replace_on_target(record, index, skip_callbacks)
    441:   callback(:before_add, record) unless skip_callbacks
    442:
    443:   set_inverse_instance(record)
    444:
    445:   @_was_loaded = true
    446:
    447:   yield(record) if block_given?
    448:
    449:   if index
    450:     target[index] = record
    451:   elsif @_was_loaded || !loaded?
 => 452:     target << record
    453:   end
    454:
    455:   callback(:after_add, record) unless skip_callbacks
    456:
    457:   record
    458: ensure
    459:   @_was_loaded = nil
    460: end

[17] pry(#<ActiveRecord::Associations::HasManyAssociation>)> target
=> []
[19] pry(#<ActiveRecord::Associations::HasManyAssociation>)> target.class
=> Array

この次はもう呼び出し元へ戻ってきて、usersの中身は入ってた。


From: /home/ishioka/repos/CodeReading/app/models/company.rb @ line 9 Company.build_users:

     5: def self.build_users
     6:   binding.pry
     7:   com = Company.find_or_create_by(name: 'test_company')
     8:   com.users.build(name: 'test_user')
 =>  9:   com.users.where(name: 'test_company')
    10: end

[24] pry(Company)> com.users
=> [#<User:0x007f753e614440 id: nil, company_id: 1, name: "test_user", created_at: nil, updated_at: nil>]

whereの実態はココらへん

From: /home/ishioka/repos/rails/activerecord/lib/active_record/relation/query_methods.rb @ line 600 ActiveRecord::QueryMethods#where:

    599: def where(opts = :chain, *rest)
 => 600:   if :chain == opts
    601:     WhereChain.new(spawn)
    602:   elsif opts.blank?
    603:     self
    604:   else
    605:     spawn.where!(opts, *rest)
    606:   end
    607: end



From: /home/ishioka/repos/rails/activerecord/lib/active_record/relation/query_methods.rb @ line 610 ActiveRecord::QueryMethods#where!:

    609: def where!(opts, *rest) # :nodoc:
 => 610:   opts = sanitize_forbidden_attributes(opts)
    611:   references!(PredicateBuilder.references(opts)) if Hash === opts
    612:   self.where_clause += where_clause_factory.build(opts, rest)
    613:   self
    614: end

その後 self.build_users へ戻ってきてしまったので、com.users.where(name: 'test_company')の段階では検索の条件などを組み立てるだけで、実際の値の検索はしていないようなので(遅延評価ってやつかな?)ちょっとコードを変えて再実行。

以下のように最後にwhereで組み立てたusersをカウントすることによって、どこの値をみてるかわかるはず。


class Company < ApplicationRecord

  has_many :users

  def self.build_users
    binding.pry
    com = Company.find_or_create_by(name: 'test_company')
    com.users.build(name: 'test_user')
    users = com.users.where(name: 'test_company')
    _count_users = users.count
  end
end
~



追ってくとこんなところがあって、SQLを組み立ててるのでDBに取りに行く模様


From: /home/ishioka/repos/rails/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @ line 34 ActiveRecord::ConnectionAdapters::DatabaseStatements#select_all:

    31: def select_all(arel, name = nil, binds = [], preparable: nil)
    32:   arel, binds = binds_from_relation arel, binds
    33:   sql = to_sql(arel, binds)
 => 34:   if !prepared_statements || (arel.is_a?(String) && preparable.nil?)
    35:     preparable = false
    36:   else
    37:     preparable = visitor.preparable
    38:   end
    39:   if prepared_statements && preparable
    40:     select_prepared(sql, name, binds)
    41:   else
    42:     select(sql, name, binds)
    43:   end
    44: end

[2] pry(#<ActiveRecord::ConnectionAdapters::SQLite3Adapter>)> sql
=> "SELECT COUNT(*) FROM \"users\" WHERE \"users\".\"company_id\" = ? AND \"users\".\"name\" = ?"


結局SQLが発行されてDBの値が検索されてそれが検索される。


From: /home/ishioka/repos/CodeReading/app/models/company.rb @ line 10 Company.build_users:

     5: def self.build_users
     6:   binding.pry
     7:   com = Company.find_or_create_by(name: 'test_company')
     8:   com.users.build(name: 'test_user')
     9:   users = com.users.where(name: 'test_company')
 => 10:   _count_users = users.count
    11: end

[1] pry(Company)> n
   (0.8ms)  SELECT COUNT(*) FROM "users" WHERE "users"."company_id" = ? AND "users"."name" = ?  [["company_id", 1], ["name", "test_company"]]

From: /home/ishioka/.rbenv/versions/2.3.1/lib/ruby/gems/2.3.0/gems/pry-0.10.4/lib/pry/pry_instance.rb @ line 356 Pry#evaluate_ruby:

    351: def evaluate_ruby(code)
    352:   inject_sticky_locals!
    353:   exec_hook :before_eval, code, self
    354:
    355:   result = current_binding.eval(code, Pry.eval_path, Pry.current_line)
 => 356:   set_last_result(result, code)
    357: ensure
    358:   update_input_history(code)
    359:   exec_hook :after_eval, result, self
    360: end

[1] pry(#<Pry>)> c
=> 0


結論

whereでは検索条件が作成されるだけであって、まだ検索はされない。
実際にその値が必要になった場合に、「DBから」検索されるので、紐付いているオブジェクトは検索結果には入ってこない。

最後に

ActiveRecordのコードを少し追っかけてみたが、結構複雑で呼び出し順や、各オブジェクトの関係などすぐには理解できなそう。
ただすごい参考になりそう(rubyの使い方、設計思想など)なので、issueでも拾ってコードリーディングしてみるかな。