Bloggerでのブログポストはコードの貼り付けをサポートしていなかったり、Markdownでかけなかったりなかなか辛いところがあったのではてなブログへ移行しました。
Blogger頑張れ!
https://ashigaru-works.hatenablog.com/
足軽☆コンピュータ道
Software Engineer 技術とかキャリアとか
2018年8月25日土曜日
2017年11月12日日曜日
Fine tuning (VGG16モデル) (Keras + Jupyter Notebook + tensorflow)
概要
Keras公式サイトの
をやってみる。
少ないサンプルからディープラーニングで優位なモデルを作る。
ステップとしては、
- スクラッチで作る。
- bottleneck featureで学習済みモデル使う
- Fine tuningを使う
- スクラッチで作る。
- 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>
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
# 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>
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と呼ぶらしい
ここはよく理解できなかったのでスキップ
<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
参考
- ディープラーニング実践入門 〜 Kerasライブラリで画像認識をはじめよう!
- 公式ドキュメント
- FindYourCandyで転移学習を使ったのでそれ関連の話
- A Comprehensive guide to Fine-tuning Deep Learning Models in Keras (Part I)
- A Comprehensive guide to Fine-tuning Deep Learning Models in Keras (Part II)
- Deep Learning 事始め(第5回 転移学習入門)
- Deep Learning 事始め(第6回[※最終回] 転移学習応用編)
- Clifar10の説明
- Building powerful image classification models using very little data
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をコーディングなしでサクッと立てられる。公式に書いてあった
単純なREST APIを構築したいときは以下のようなJsonを用意する。
以下コマンドで立ち上げ
リクエストを投げてみるとjsonが返却される。
そのときにサクッと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でパッケージのリスト作る。
グローバルに入れたくない場合、まず適当なフォルダを作って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
いろいろパッケージが入るのでこれで完了
使用方法
{ "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。
環境はUbuntu, Nginx, Unicorn。
手順
まず、すでに動いているrails5.x系, ruby 2.3.xの環境には影響を与えたくなかったので、ruby 2.2.7をインストールして、ruby周りの環境を整える。
Unicornの設定ファイルを作成
新しく設定したいアプリの下で、vim config/unicorn.rbでファイルを新規作成。
railsアプリのルートパスを変える。refinery-cmsのパスを変えるには下記ファイルを変更。
vim config/initializers/refinery/core.rb
わかりやすくなるように、静的ファイルのパスを元のアプリと変える。
vim config/environments/production.rb
Nginxの設定ファイルは以下
refinery-cmsのpluginからリクエストされる静的ファイルのパスを/から/kinsume_blogへ変更することができなかったので、nginxのrewriteを駆使して対応。
あまりアクセスパスを変更されることを想定されてない作りっぽいので、PR出していきたい。
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プロジェクトと共存させたりしたいのでいろいろ設定が必要そうだが、それはまた今度
この後本番環境で、既存のrailsプロジェクトと共存させたりしたいのでいろいろ設定が必要そうだが、それはまた今度
Pros And Cons
- rails 4.x系でしか動かない(2017/07/06)
- ただしrails 5.x系への対応PRは出ている
- ドキュメントが古い
- スタイル適応の仕組みがないのでデザイン変えたい時、CSS頑張るしかない。
- これ自分的には相当面倒なんだが、他の人はどうなんやろ?
- Rails wayから外れていないので、Rails知っている人には非常にとっつきやすい
- 未だにメンテされている(活発ではなさそう)
ラベル:
CSM,
OSS,
rails,
Refinerycms,
ruby
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にテーブルを作る。
そしてCompanyにCompanyとUserの関連を持たせるのと、コードを追うためのメソッドを追加。
実行して追いかけてみる。buildはcollection_proxy.rbのbuildメソッドが呼ばれているもよう。
@associationの実態は以下のようActiveRecord::Associations::HasManyAssociationというクラスのインスタンスでフィールドにCompanyとUserに関する情報をいろいろ持っている。
Userのインスタンス化自体は、このパターンだとObjectのnewが使われていた。
こので戻ってきたメソッドないでは、Userがインスタンス化されている。
before_addとかafter_addはここで呼ばれているのか、へー
targetというArrayにこのレコードを入れているが、最終的にこれがCompanyと関連をもって保存されるのかな?
この次はもう呼び出し元へ戻ってきて、usersの中身は入ってた。
whereの実態はココらへん
その後 self.build_users へ戻ってきてしまったので、com.users.where(name: 'test_company')の段階では検索の条件などを組み立てるだけで、実際の値の検索はしていないようなので(遅延評価ってやつかな?)ちょっとコードを変えて再実行。
以下のように最後にwhereで組み立てたusersをカウントすることによって、どこの値をみてるかわかるはず。
追ってくとこんなところがあって、SQLを組み立ててるのでDBに取りに行く模様
結局SQLが発行されてDBの値が検索されてそれが検索される。
実際にその値が必要になった場合に、「DBから」検索されるので、紐付いているオブジェクトは検索結果には入ってこない。
上を参考にしつつ、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でも拾ってコードリーディングしてみるかな。
登録:
投稿 (Atom)