- 投稿日:2019-07-04T23:08:39+09:00
関連する ActiveModel::Serializer を切り替える
はじめに
- Associations において利用する Serializer を切り替えました
- AMS で該当する機能は見つからなかったので素朴に実現しました
目次
- はじめに
- TL;DR
- 環境
- 構成
- Model
- API
- やりたいこと
- 実現方法
- Controller
- Serializer
- まとめ
- 参考
TL;DR
- Rendering の際に
optionsを渡すreservations_controller.rbrender json: reservation, include: '**', restaurant_type: :detail
- Serializer の initialize で
optionsを受け取るreservation_serializer.rbdef initialize(object, options = {}) super @restaurant_type = options[:restaurant_type]
optionsの値によって 関連先で利用する Serializer を切り替えるreservation_serializer.rbattributes :id, :restaurant def restaurant serializer = case @restaurant_type when :base then RestaurantSerializer when :detail then RestaurantDetailSerializer else RestaurantSerializer end serializer.new(object.restaurant, @options)環境
- ruby: 2.6.2 - rails: 5.2.3 - active_model_serializers: 0.10.9構成
Model
app/models/restaurant.rb# == Schema Information # # Table name: restaurants # # id :bigint(8) not null, primary key # name :string not null # phone :string not null # address :string not null class Restaurant < ApplicationRecord has_many :reservationsapp/models/reservation.rb# == Schema Information # # Table name: reservations # # id :bigint(8) not null, primary key # restaurant_id :bigint(8) not null class Reservation < ApplicationRecord belongs_to :restaurantAPI
reservations_list.json{ "reservations": [ { "id": 42, "restaurant": { "id": 42, "name": "Domino's Pizza" } } ] }reservation_detail.json{ "reservation": { "id": 42, "restaurant": { "id": 42, "name": "Domino's Pizza", "phone": "03-1234-5678", "address": "東京都XXXXXX" } } }やりたいこと
- Reservations List API では restaurant の
id, nameのみを含める- Reservation Detail API では restaurant の
id, name, phone, addressを含める実現方法
Controller
restaurant_typeを Serializers に渡すreservations_controller.rbmodule API class ReservationsController < API::ApplicationController def index reservations = Reservation.all render json: reservations, include: '**', restaurant_type: :base end def show reservation = Reservation.find(params[:id]) render json: reservation, include: '**', restaurant_type: :detail end end endSerializers
- initialize で
options[:restaurant_type]を受け取るoptions[:restaurant_type]の値によって, Serializer を切り替える@optionsを 渡すことで, options や scope を 引き継ぐreservation_serializer.rbclass ReservationSerializer < ActiveModel::Serializer attributes :id, :restaurant def initialize(object, options = {}) super @options = options @restaurant_type = options[:restaurant_type] end def restaurant serializer = case @restaurant_type when :base then RestaurantSerializer when :detail then RestaurantDetailSerializer else RestaurantSerializer end serializer.new(object.restaurant, @options) end endrestaurant_serializer.rbclass RestaurantSerializer < ActiveModel::Serializer attributes :id, :name endrestaurant_detail_serializer.rbclass RestaurantDetailSerializer < RestaurantSerializer attributes :address, :phone endまとめ
- 今回は牧歌的な方法で切り替えました
- 良い方法があれば教えてください
![]()
参考
- 投稿日:2019-07-04T22:40:59+09:00
ステップバイステップで学ぶCapistrano 3によるRails 5.2 + puma + nginxのデプロイ
このドキュメントを書いた理由
qiita内も含め、同じような事例は数多く公開されているが、残念ながらイチから学ぶ上ではほとんど参考にならなかった。
なぜなら重要なのは、最終的な作業内容や設定ファイルの中身ではないからだ。それらはソフトウェアの構成やバージョンが変われば容易に変化する。
本当に重要なのは、デプロイ作業がどんなステップで成り立っていて、各ステップで何を目的とし、そのために最低限どんな設定が必要なのか、理解することだ。
そういうわけで、自分で考えて調べて実行したので、結果をまとめることにした。
方針
以下の6ステップに分割して作業を進めた。概ね、各ステップがCapistranoの一プラグインに対応している。つまり一つずつプラグインを追加していくイメージである。
- ssh/gitによるファイルの配置
- rbenvの動作確認
- bundlerによるgemインストール
- Railsの設定
- pumaの起動
- nginx と pumaの連携
可能な限り、次の2つの指針をもって進めた。
- デフォルト値が妥当な場合は、設定を書かない
- 1回きりとわかっている手順は、手動で行う
デフォルト値が妥当な場合は、設定を書かない
そのステップで本当に必要な設定が、一目でわかるようにするためである。
もちろん、人によって構成によって必要な設定は異なる。異なるからこそ、他人の設定ファイルをコピペするのではなく、公式ドキュメントを読んで一つずつ妥当性を判断すべきである。全部まとめてやるのは大変だが、一度に1プラグインなら難しくない。
1回きりとわかっている手順は手動で行う
Capistranoは独自のタスクを定義できるから、ついカッコよく自動化してみたくなる。しかしそれは「早すぎる最適化」というものだ。結局今回は、カスタムタスクは作らなかった。
環境情報とドキュメント化の範囲
サーバOS: Debian 9 stretch
クライアントOS: Ubuntu 18.04.2 LTS on WSL 1
- Ruby 2.6.3 on rbenv
- Rails 5.2.3
- puma 3.12.1
- Capistrano 3.11.0
- SQLite
各ソフトウェアのインストールには触れない。ユーザを追加したりSSH鍵をセットアップしたりといった手順にも触れない。またプラグインを追加した後の
bundle installなど、自明な手順はしばしば省略する。使用するサーバはweb/app/dbを兼ねる1台のみ、productionオンリー、しかもDBはsqlite。オモチャのようなアプリであるが、手順を学ぶ題材にはちょうど良かった。
デプロイ手順の説明
前置きが長くなった。具体的な作業に入る。Capstranoのインストールから。
Gemfile# Use Capistrano for deployment group :development do gem 'capistrano' end
bundle installの後、cap installを実行して設定ファイルを作る。今回はproductionの分しか作らない。bundle exec cap install STAGES=productionCapistranoの設定は、Railsアプリと共通の
Gemfileやconfig/以下に書き込んでいく。しかし事実上、「独立したデプロイ用のプログラムを作る」と考えた方がいい。例えばCapistrano関係の設定を変更しても、その都度
git commit / pushする必要はない。Capistranoは実行時のRailsアプリとは無関係だ。1. ssh/gitによるファイルの配置
目的
- 素のCapistranoで、ssh/gitを用いたファイル配置のみを行う。
前提条件
- サーバにSSH接続できること
- 接続したユーザで配置先のディレクトリに書き込めること
- サーバからGitリポジトリに接続できること
方針通り、最低限の設定のみ記す。必要なのはまずアプリケーションの名前と、gitリポジトリのURLと、サーバ上での配置先。
config/deploy.rbset :application, 'myapp' set :repo_url, 'git@github.com:myapp.git' set :deploy_to, "/var/www/apps/myapp"それからサーバのアドレスとユーザ名。Capistranoは実行ユーザの
~/.ssh/configを(完全ではないが)認識するので、sshコマンドで普通に接続できるなら、改めてSSH関連の設定はしなくていい。config/deploy/production.rbserver "myserver", user: "tkyk", roles: %w{app db web}デプロイ実行。
bundle exec cap production deploy指定した配置先にCapistranoが管理するディレクトリ構造が作られ、gitからファイルがチェックアウトされる。サーバにログインして確認しておこう。
2. rbenvの動作確認
目的
- 使用したいバージョンのRubyがrbenv経由で使える(Capistranoが認識する)ことを確認する。
前提条件
- rbenvおよび使用するバージョンのRubyがインストールされていること
capistrano-rbenvプラグインを追加・有効化する。
Gemfilegroup :development do #... gem 'capistrano-rbenv' endCapfilerequire "capistrano/rbenv"今回、rbenvはシステムレベルで(
/usr/local/rbenvに)インストールしており、Rubyのバージョンは.ruby-versionで指定する。この場合、設定は次のようになる。config/deploy.rbset :rbenv_type, :system set :rbenv_ruby, File.read('.ruby-version').striprbenvの構成違いは、
- rbenv_type
- rbenv_ruby
- rbenv_prefix
という3つの設定を組み合わせて対応する。たとえばrbenvをユーザレベルでインストールして、かつ
.ruby-versionを使用しないなら、次のような設定になるはず(動作確認はしてない)。config/deploy.rbset :rbenv_type, :user set :rbenv_ruby, '2.6.3' set :rbenv_prefix, "RBENV_ROOT=#{fetch(:rbenv_path)} RBENV_VERSION=#{fetch(:rbenv_ruby)} #{fetch(:rbenv_path)}/bin/rbenv exec"deployの実行。
bundle exec cap production deployこの段階では実質的に何も実行しないが、設定を間違えたりして必要なバージョンが見つからない場合、警告が出てデプロイが中止される。
3. bundlerによるgemのインストール
目的
- Rails他、アプリケーションが必要とするgemをbundlerでインストールする
前提条件
- 使用するバージョンのRubyにおいてbundlerがインストールされていること
capistrano-bundlerプラグインを追加。
Gemfilegroup :development do #... gem 'capistrano-bundler' endCapfilerequire "capistrano/bundlerアプリケーションで使用するgemはリリース間で共有したいので、プラグインの標準の規約に従い、
.bundleをlinked_dirsに追加する。ここに追加したディレクトリはsharedディレクトリ下に配置され、各リリースからはシンボリックリンクで参照される。config/deploy.rbappend :linked_dirs, '.bundle'他の設定はデフォルト値で問題なかったが、並列数だけはサーバスペックに合わせて設定すると良いと思う。デフォルトは4。
config/deploy.rbset :bundle_jobs, 2デプロイ実行。bundle installが走るので初回はかなり時間がかかるだろう。
bundle exec cap production deploy4. Railsの設定
このステップはやや複雑で、おそらく構成による差異も大きいはず。
目的
- Railsがリリース間で共有するリソースを定義する
- データベースのmigrationを行う
- assetコンパイルを行う
前提条件
- 特になし
まずはcapistrano-railsプラグインを追加し、migrationおよびassetコンパイルを有効化する。
Gemfilegroup :development do #... gem 'capistrano-rails' endCapfilerequire "capistrano/rails/assets" require "capistrano/rails/migrations"第1の目的。railsがリリース間で共有するリソースを定義する。次のファイル・ディレクトリは、基本的にどんなRailsアプリでも該当するだろう。
config/deploy.rbappend :linked_files, "config/master.key" append :linked_dirs, "log", "tmp/pids", "tmp/cache", "tmp/sockets"他にも
tmp/storageとかpublic/uploadsとか、アプリケーションによって必要なものを追加する。こうしたリソースのうち、ディレクトリは自動で作られるが、ファイルはそうではない。よって
config/master.keyは予めサーバのshared以下にコピーしておこう。scp config/master.key myserver:/var/www/apps/myapp/shared/config/第2の目的。データベースのmigration。
独立したデータベースサーバがあるなら、
database.ymlは普通、リポジトリに追加しないはず。その場合はlinked_filesにdatabase.ymlを追加し、何らかの手段でshared/config/database.ymlを設置する。一方、今回の私のアプリではsqliteを使うため、データベースファイル *.sqlite3 自体をリリース間で共有したい。そのため、
database.ymlを次のように書き換え、1段階ディレクトリ階層を深くして……config/database.ymlproduction: <<: *default database: db/production/production.sqlite3このディレクトリを共有リソースとした(
database.ymlはリポジトリ内で管理する)。config/deploy.rbappend :linked_dirs, 'db/production'第3の目的、assetコンパイルについては、特に設定する箇所がなかったのでデフォルトのまま。
デプロイ実行。migration, assetコンパイルが実行され、諸々シンボリックが作られる。
bundle exec cap production deploy5. pumaの起動
目的
- pumaを起動できるようにする
- pumaプロセスのステータスをローカルから確認できるようにする
- デプロイ完了時にpumaプロセスを再起動させる
前提条件
- 特になし
capistrano-pumaプラグインのインストールと有効化。もちろんpuma自体まだインストールしていないなら、それも。
Gemfile# Rails 5.2.3でrails newしたら最初から入っていた gem 'puma', '~> 3.11' group :development do #... gem 'capistrano3-puma' endCapfilerequire 'capistrano/puma' install_plugin Capistrano::Pumaとりあえず動かしてみたいなら、プラグインに設定ファイルを自動生成してもらうのが簡単である。
bundle exec cap production puma:configこのコマンドで、サーバ上の
shared/puma.rbに設定ファイルが作られる。(すでにサーバ上にpuma gemをインストール済みなら)以下のようなコマンドで、起動や終了、ステータスを確認できる。
bundle exec cap puma:start bundle exec cap puma:stop bundle exec cap puma:statusデプロイ実行。完了時に再起動されることを確認しよう。
bundle exec cap production deploy6. nginx と puma の連携
目的
- nginxからUNIXドメインソケットを通してpumaにアクセスできるようにする
前提条件
- nginxがインストールされていること
capistrano-pumaのnginx用プラグインを有効化。
config/deploy.rbinstall_plugin Capistrano::Puma::Nginxこれまた、とりあえず動かしたいなら、設定ファイルは自動生成すると良い。
bundle exec cap production puma:nginx_configこれで
/etc/nginx/sites-available/myapp_pruductionのようなパスに設定ファイルが作られ、sites-enabledからはシンボリックリンクが作られる(当然、ディレクトリへの書き込み権限が必要。もし書き込み権限を与えたくないならrails g capistrano:nginx_puma:configでローカルに設定ファイルを生成できる)。あとは必要に応じて設定ファイルを修整、例えばnginxのデフォルトサイト(
sites-enabled/default)を削除したりして、nginxをリロードすれば完了。sudo nginx -t && sudo systemctl reload nginx前ステップでpumaが起動しているなら再デプロイは不要なはず。httpアクセスして動作確認しよう。
終わりに
pumaプラグインが設定ファイルを自動生成してくれるおかげで、動作させるだけならとても簡単だった。もちろん実運用においては、それら設定ファイルの管理ポリシーが必要だが。
一連の過程において唯一引っかかったのは、以下の問題だった。
capistrano3-pumaのアップデートで起きたバグ解決
結局これも、デフォルトが妥当なら上書きしない、という方針で回避できた。
(とはいえcapistrano-rbenvの場合、デフォルト値が何なのか公式ドキュメントに書いてないのが困りものである。例示されている設定はデフォルト値ではない)
- 投稿日:2019-07-04T20:10:39+09:00
初めてのRuby3 〜クラスとインスタンス、命名規則〜
クラスとインスタンス
クラス : オブジェクトの種類を表すもの、型(クッキーの型)
eg:)Arrayオブジェクト、stringオブジェクト
インスタンス : クラスから作られたもの(クッキー
)
eg:)[1,2,3], "name"newメソッド
new : クラスからインスタンスオブジェクトを作るメソッド(クッキーの型でクッキー
を作る)
eg:) Array.new: []
String.new: ""
(メソッド=お仕事)
インスタンスオブジェクトは直接作ることも可能
eg:) [1,2,3] , "name"クラスの作り方
class クラス名(必ず一文字目は大文字) クラスの定義 end作ったクラスをnewする
class Recipe end recipe = Recipe.newclassにメソッドを作る
メソッドを作ってRecipeクラスが仕事をできるようにしてあげる
class Recipe def title "cheese cake" end end recipe = Recipe.new recipe.title => "cheese cake"タイトルを返せるようになった
インスタンス変数
インスタンス変数 : インスタンスがある間ずっと使える変数。頭に@をつける。
1、インスタンスオブジェクト(クッキー)ごとにインスタンス変数を持っている。
2、オブジェクトの外から直接アクセスできない。アクセスするときはそのオブジェクトのメソッドを通す。
変数のスコープ(有効範囲)
あるメゾット内で作成した変数はそのメソッドの中でしか有効でない。
この有効範囲を広げるためにインスタンス変数を使う。❶ class Recipe def title = (t) @title = t end def title = (t) @title end endrecipe = Recipe.new recipe.title = "cheese cake" p recipe title => "cheese cake"❶はよく使われるのでattr_accessorという書き方が用意されている
書き方 : attr_accessor インスタンス変数のシンボル書き換えるとこうなる↓
❶ class Recipe attr_accessor :title end命名規則
クラス名 : 大文字始まり。2語以上は単語境界文字を大文字にする(Camel Case)
変数名 : 小文字のみ。2語以上は_でつなぐ。(Snake Case)
initializeメゾット
initialize : newメゾットが呼ばれた時に自動で実行するメソッド
newメソッドに引数を渡すとinitializeメソッドで受け取ることができるclass Recipe attr_accessor :title, :author def initialize (title, author) @title = title @author = author end end recipe = Recipe.new("cheese cake", "chef") p recipe.title => "cheese cake" p recipe.author => "chef"インスタンスメソッドとクラスメソッド
インスタンスメソッド
クラスの中で定義したメソッド。インスタンス(クッキー
)に対して呼ぶことができる。
(記法)クラス名#インスタンスメソッド名
クラスメソッド
クラス(クッキーの型)に対して呼び出す。
self.メソッド名で定義。
(記法)クラス名.クラスメソッド名
- 投稿日:2019-07-04T19:24:33+09:00
Goやるなら知っておきたい「Composition over inheritance」
Goを勉強している中で、「Composition over inheritance」という概念が出てきました。ちゃんと理解していなかったので、ここで改めて掘り下げます。
特に、普段 Ruby や Rails を書いている初中級者の方は、Go を勉強する前におさらいしておくことをオススメします。なぜなら、Go では、Rubyで馴染みのある Inheritance(継承)ではなく、Composition(合成)のみが使われるからです。
「Composition over inheritance」とは
「Composition over inheritance」は、日本語だと「継承より合成」と表現されます。
これは、オブジェクト思考プログラミングにおいて、親クラスやベースクラスを「継承」するよりも、「合成」によってコードを共通化・再利用する方が望ましい、という考え方です。
つまり、継承より合成の方が良いということです。
それを理由に、Go では合成のみが採用されており、型の継承はできません。
Composition と Inheritance の比較
Composition と Inheritance、それぞれについておさらいします。
両者の対比は、次のようにまとめられます。
英語 日本語 型の関係 考え方の例 Composition 合成 has-a ソファには綿が入っている Inheritance 継承 is-a ソファは家具である 具体的なコード例
文字だけでは抽象的なので、具体的なコード例で考えてみましょう。
ここでは、挨拶ができる「英語圏の人」と「日本人」をコードで表してみます。
Composition と Inheritance を対比させたいので、どちらも表現しやすい Ruby を用います(独断と偏見)。
Inheritance の例
Inheritance で設計する場合、まず「挨拶ができる」スーパークラスとして
Personクラスを定義し、greetメソッドを持たせます。person.rbclass Person def greet print word end endその
Personクラスを継承するEnglishクラスを定義します。english.rbclass English < Person def word 'Hello!' end end同様に
Japaneseクラスも定義します。japanese.rbclass Japanese < Person def word 'こんにちは!' end end
English・Japanese、どちらでも挨拶できます。e = English.new e.greet # 'Hello!' j = Japanese.new j.greet # 'こんにちは!'Inheritance は、普段 Ruby・Railsを書いてる人にとって、ごく一般的で馴染みのある設計だと思います。
Composition の例
Composition で設計する場合、「挨拶ができる」ことに着目し、そのビジネスロジックを
Greetingクラスとして切り出します。(モジュールをincludeするやり方は、クラス継承ツリーに含まれてしまうので、Composition の純粋な例としては使えません。)greeting.rbclass Greeting def greet(word) print word end endその
GreetingクラスをEnglishクラスに持たせます(合成します)。english.rbclass English attr_reader :greeting def initialize @greeting = Greeting.new end def greet greeting.greet(word) end def word 'Hello!' end end同様に
Japaneseクラスも定義します。japanese.rbclass Japanese attr_reader :greeting def initialize @greeting = Greeting.new end def greet greeting.greet(word) end def word 'こんにちは!' end endこれで先ほどと同様に、
English・Japaneseどちらでも挨拶できます。e = English.new e.greet # 'Hello!' j = Japanese.new j.greet # 'こんにちは!'Composition の例 (Go ver.)
参考として、Go で上の Composition を表すとこのようになります → こちら (Go Playground)
なぜ、Compositionが良いのか?
ここからが本題です。
なぜ「Composition over inheritance」、つまり、継承より合成の方が良いのでしょうか?
Composition のメリット
一般的に考えられているメリットは次の2つです。
1. 考えやすい
Composition の場合、その共通化させるビジネスロジック(上の例では、挨拶できること)に着目してコードを設計します。そのため、Inheritance の場合と違って、抽象化させるためのスーパークラス名や、継承ツリーの構成などに頭を悩まされることはありません。
2. 変更しやすい
コードに変更を加える場合、Composition では、そのビジネスロジックを持っている当事者のみが影響を受けます。また、あるクラスにビジネスロジックを追加する場合も、Composition として共通化していれば簡単に対応できます。
一方 Inheritance では、スーパークラスのコードを変更すると、その継承ツリーの下位クラス全体に影響があります。そのため、スーパークラスに新しい振る舞いを追加することは、意図しないエラーを生むリスクがあります。
例えば、挨拶できるロボットがいた場合...
Composition だと、新たな
RobotクラスにGreetingクラスを持たすだけでOKです。一方 Inheritance では、
RobotはPersonではないので、スーパークラス名や継承ツリーの構成を再検討しないといけません。また、スーパークラスに変更を加えると、その下位クラスで意図しないエラーが起きるリスクがあります。Composition のデメリット
一般的に考えられているデメリットは1つで、合成したメソッドを呼ぶためのメソッドを書かないといけないことです。上の Ruby の例で言う、
English#greetやJapanese#greetですね。一方の Inheritance では、継承したスーパークラスのメソッドをそのまま呼び出せます。したがって、スーパークラスに基本的なビジネスロジックを持たせておけば、Composition よりも少ないコードで同じ振る舞いを実装することが可能です。(ただし、何でもかんでも詰め込むと「神クラス」となって収集がつかなくなるので注意!)
デメリットの回避
この Composition のデメリットを回避するために、Go では Embedding types が用いられます。
先ほどあげた Go Playground のコード例でも
GreetingタイプをEnglish・Japaneseタイプに組み込むことで、それぞれのタイプでgreetメソッドを定義することを回避しています。// 一部抜粋 type Greeting struct{} type English struct { Greeting } type Japanese struct { Greeting }まとめ
継承は、抽象化したコードで効率的な実装ができるので、変更の可能性が少ない部分などにはとても有効な設計手法です。そして、Ruby を使っている人にとっては、とても馴染みがあります。
しかし、Go では、合成のみが採用されており、型の継承はできません。
この言語仕様の違いを、背景にある「Composition over inheritance」と一緒にしっかり理解しておけば、より早く Go の世界に馴染むことができるでしょう。
Sources
- Composition over inheritance - Wikipedia
- Why Go’s structs are superior to class-based inheritance - Ian Macalinao
- オブジェクト指向と10年戦ってわかったこと - @tutinoco
- 継承より合成ってなに? - Mastering Python
- Ruby : Composition over Inheritance because The Force is Strong with Composition - Kartik Jagdale
- I would love to see some composition examples - Ruby Chat
- 投稿日:2019-07-04T16:55:07+09:00
Sequel::Mock::Datasetの使い方(ついでにDatasetとArrayの違い)
すごい雑なコードです。
Qiitaに載せるようにいろいろ変更を加えているのでサンプルコードでの動作確認まではできていないです。
なので参考程度に見ていただけるとrequire 'sequel' require 'sequel/adapters/mock' # sequelのrequireだけではこのファイルはrequireしてくれない模様 class Sample # 0埋め済みの市区町村IDを返す # @return [Array] def get db = Sequel.connect(config) dataset = db.fetch('select city_id from sample_table') # 市区町村コードを0埋めした状態で返したいのでeachで処理する dataset.each do |row| row[:city_id] = '%05%d' % row[:city_id] end dataset.all end private def config # DB接続に必要な設定 end end describe Sample do describe '#get' do subject { described_class.new.get } let(:db) { Sequel::Mock::Database.new(config) } # configメソッドと同内容を引数に取る let(:records) do [ { city_id: 1101 }, { city_id: 1102 } ] end let(:dataset) { Sequel::Mock::Dataset.new(db) } before :each do allow_any_instance_of(Sequel).to receive(:connect).with(config).and_return(db) db.fetch = records # DBに格納されているであろうデータをセットする allow(db).to receive(:fetch).with('select city_id from sample_table').and_return(dataset) end it 'city_idが0埋めされて5桁の文字列になっていること' do subject.each do |row| expect(row[:city_id]).to be_a(String) expect(row[:city_id].size).to eq 5 # start_withも使って0で始まっていることの確認もしたいけど群馬県以南は1〜4で始まってる5桁なのでやらない # 脳死でsizeとか書いたけど正規表現で書けばいいじゃんって思う end end end endざっくり雰囲気は掴んでもらえるでしょうか。
Sequel::Mock::Databaseに対してfetchでデータを投入しておくというのがミソです。
これをしないとdatasetのモックがnilや[]を返すようになります。
モック化、結構手間ですねついでにDatasetとArrayの違い
実はこの
#getは要件を満たせていないです今回はRubyでの参照の値渡しを利用して
#each内でcity_idを上書きする、ということをしようとしています。(サンプルなのでこれのいい悪いは一旦置いといて)arr = [{city_id: 1101}] arr.each {|row| row[:city_id] = '%05d' % row[:city_id]} p arr #=> [{:city_id=>"01101"}]
Sequel::Datasetも#eachを提供しているのでうまく行っているかのように見えますが、実際はそうではありません。dataset = db.fetch('select city_id from sample_table') dataset.each do |r| r[:city_id] = '%05d' % r[:city_id] end dataset.all # [{:city_id=>1101}]この手法は
Arrayであれば成立しますがSequel::Datasetの場合はdataset.eachの操作をするたびにSQLを実行した結果が返されます。(内部的にキャッシュをしていてSQLの実行は1回で済んでいる可能性もあるかもしれませんが、そこまで検証してないです)
そのため上書きした結果を返さずに#allを呼んだ際にまた別の配列を返しているというものになります。
対象のデータ型が何かを理解した上で実装を進めたいですね。
- 投稿日:2019-07-04T16:18:08+09:00
【Rails】RSpecでFactoryBotをcreateしようとしたらErrorが吐き出されたので調べた
RSpecを書いたところ、FactoryBotからuserをcreateするところでエラーが発生してしまうので、原因を調べてみた。
事象
spec/factories/user.rbFactoryBot.define do factory :user do name { "テストユーザー" } email { "test_user@test.co.jp" } password { "12345678" } end enduser_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do let(:user) { user :user } describe "バリデーション" do it "nameが空だとinvalid" do user.name = "" expect(user).not_to be_valid end end endrspecを実行したところ、エラーが発生。
ちなみに今回、Userモデルのvalidatesは:name, presence: trueだけとする。Failures: 1) User バリデーション nameが空だとinvalid Failure/Error: let(:user) { create :user } ActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true # ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>' # ./spec/models/client_spec.rb:9:in `block (3 levels) in <top (required)>' # ------------------ # --- Caused by: --- # ArgumentError: # Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true # ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>' Finished in 0.52519 seconds (files took 2.18 seconds to load) 1 example, 1 failure何やら
Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to trueというエラーが吐き出されている。結論
今回は
deviseを使っているのだが、メール認証機能のconfirmableを有効にしている。user.rb# Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmableメール認証機能とは→アカウント新規作成時に登録したアドレスにメールが送られ、確認用リンクをクリックするとログイン画面からログイン可能となる。
このリンクをクリックしたタイミングで、Userのconfirmed_atカラムにその時刻がsaveされる。
どうやらconfirmableが有効の場合、このconfirmed_atがpresentかnilかを見分けて、そのユーザーがログイン可/不可を条件分けしているらしい。
(詳細までは調べられてません)従って、FactoryBotのファイルに
confirmed_atを書き加えるとspec/factories/user.rbFactoryBot.define do factory :user do name { "テストユーザー" } email { "test_user@test.co.jp" } password { "12345678" } confirmed_at { Date.today } end end. Finished in 0.2486 seconds (files took 4.21 seconds to load) 1 example, 0 failures無事、rspecが通りました。
- 投稿日:2019-07-04T16:18:08+09:00
【Rails】FactoryBotをcreateしようとしたらErrorが吐き出されたので調べてみた
RSpecを書いたところ、FactoryBotからuserをcreateするところでエラーが発生してしまうので、原因を調べてみた。
事象
spec/factories/user.rbFactoryBot.define do factory :user do name { "テストユーザー" } email { "test_user@test.co.jp" } password { "12345678" } end enduser_spec.rbrequire 'rails_helper' RSpec.describe User, type: :model do let(:user) { user :user } describe "バリデーション" do it "nameが空だとinvalid" do user.name = "" expect(user).not_to be_valid end end endrspecを実行したところ、エラーが発生。
ちなみに今回、Userモデルのvalidatesは:name, presence: trueだけとする。Failures: 1) User バリデーション nameが空だとinvalid Failure/Error: let(:user) { create :user } ActionView::Template::Error: Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true # ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>' # ./spec/models/client_spec.rb:9:in `block (3 levels) in <top (required)>' # ------------------ # --- Caused by: --- # ArgumentError: # Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to true # ./spec/models/client_spec.rb:4:in `block (2 levels) in <top (required)>' Finished in 0.52519 seconds (files took 2.18 seconds to load) 1 example, 1 failure何やら
Missing host to link to! Please provide the :host parameter, set default_url_options[:host], or set :only_path to trueというエラーが吐き出されている。結論
今回は
deviseを使っているのだが、メール認証機能のconfirmableを有効にしている。user.rb# Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, :confirmableメール認証機能とは→アカウント新規作成時に登録したアドレスにメールが送られ、確認用リンクをクリックするとログイン画面からログイン可能となる。
このリンクをクリックしたタイミングで、Userのconfirmed_atカラムにその時刻がsaveされる。
どうやらconfirmableが有効の場合、このconfirmed_atがpresentかnilかを見分けて、そのユーザーがログイン可/不可を条件分けしているらしい。
(詳細までは調べられてません)従って、FactoryBotのファイルに
confirmed_atを書き加えるとspec/factories/user.rbFactoryBot.define do factory :user do name { "テストユーザー" } email { "test_user@test.co.jp" } password { "12345678" } confirmed_at { Date.today } end end. Finished in 0.2486 seconds (files took 4.21 seconds to load) 1 example, 0 failures無事、rspecが通りました。
- 投稿日:2019-07-04T13:34:13+09:00
google-api-client,optimist(trollop)を使うと出てくる3つのエラーとその解決法
概要
Youtube APIを使用し動画情報を 取得し一覧表示するサイトを作成中にハマってしまったエラーが3つありましたので、エラーパターンと解決法を書き残しておきます。
Gemのバージョンにも関わってきますし、デプロイしているサーバの環境にも依存しますので、このエラー現象と解決方法は2019年7月段階のものだということを念頭に置いてください。環境
- Ruby 2.4.1
- rails 5.2.3
- google-api-client 0.8.6
- optimist 3.0.0(旧trollop)
- サーバ
- heroku
エラーパターン1:oprimist(trollop)のエラー
herokuのlog2019-06-18T11:44:23.578660+00:00 app[web.1]: Error: unknown argument '-p'. 2019-06-18T11:44:23.578668+00:00 app[web.1]: Try --help for help.このパターンはoptimist::optionsメソッドにコマンドライン引数がぶっこまれていることが原因です。
heroku上だと-pと-eオプションを付けた状態でrails sされます。
参考(optimistのソース):https://github.com/ManageIQ/optimistローカル環境でサーバを起動する際にrails s だけで起動している場合は起きないのにheroku上だと上記のエラーが出てきます。ためしにローカル環境でrails s -p ポート番号(3000) -e development で起動して実行するとheroku上で動かした時と同じエラーが出ることが確認できます。
もしくはbyebugでoptimist::optionsをコールする直前のARGVの値を参照してみると確認できると思います。エラーパターン1:解決法
1.ARGVの値をローカル変数に退避
2.ARGVを空にする
3.optimist::optionsをコール
4.ARGVの値を元にもどす修正コードarg_array_save = [] arg_array_save = ARGV.shift(4) opts = Optimist::options do ・・・中略・・・ arg_array_save.each do |arg_push| ARGV.push(arg_push) endこんな感じになります。ARGVはメソッド内で値を直接操作すること(代入等)ができないので、shift,push等のメソッドを使用していじっています。
もっといい方法があればぜひコメントで教えて頂きたいです。エラーパターン2:ArgumentError
herokuのlog2019-06-24T14:54:19.983862+00:00 app[web.1]: F, [2019-06-24T14:54:19.983799 #4] FATAL -- : [d9d33d3a-e4a5-4ba3-a0f6-7286855ae09a] ArgumentError (header field value cannot include CR/LF):このパターンは上記のパターン1を解決した後にでてくる可能性があります。
原因はgoogle-api-clientのバージョンとrubyのバージョンがうまく対応できてないみたいです。gem内のソースコードレベルの原因は解明できてません。gem内の解析めんどくさい解決法は2つありますが、解決法パターン1推奨です。
エラーパターン2:解決法パターン1
Rubyのバージョンを 2.4.1にする。
なぜかわかりませんが2.4.6から2.4.1にバージョンを落とすと解決しました。エラーメッセ的には改行コードだと思うので、その辺の処理がバージョンによって違うのでしょうか。
わかる人いたら教えてください。エラーパターン2:解決法パターン2
google-api-client を最新のバージョンにする。
2019年7月現在だと最新は0.30.4です。
Gemを最新にするとエラーパターン2のエラーメッセージは消えるのですが、後述するエラーパターン3の状態に陥ってしまうので、この解決法はあまりお勧めできないです。
GitHubのReadmeを全部翻訳してバージョンによる変更点を全部理解すると解決できるかもしれないです。エラーパターン3:LoadError
herokuのlog2019-06-20T16:12:55.170597+00:00 app[web.1]: /app/vendor/bundle/ruby/2.4.0/gems/bootsnap-1.4.4/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:33:in `require': No such file to load -- google/api_client.rb (LoadError)ファイルが読み込めてないみたいですが、heroku内のファイルとかいじれるんでしょうか。
エラーパターン3:解決法
boot.rbのbootsnapをOFFにする。
これでエラーメッセージ自体は消えますが、また同じようなエラーが今度はactivesupportで出てきます。
この解決ルートは永遠と上記の解決法を繰り返す羽目になりそうだったので、やめました。
ですので、エラーパターン3の明確な解決法はわかってない状態です。まとめ
- エラーパターン3の最終的な解決法は不明。エラーパターン2でrubyのバージョンを変えて解決する方が現実的。
- Gemのバージョンアップなどで仕様が変わるのでReadmeを読まないとわからない。
- 英語を勉強する必要がある。
- 投稿日:2019-07-04T13:20:06+09:00
プログラミング初心者はまずRailsチュートリアルをやるべきなのか?
Railsチュートリアルは初心者がやるべきか?
プログラミング初心者の流れとして、最近はProgateやドットインストールをやって基礎を学んで、そして次の段階でRailsチュートリアルをやろう!というようなものが結構多いように感じます。
Railsチュートリアルは、手を動かしものを作りながら学習を進められるとてもいい教材です。しかし、これをプログラミングを始めたばかりの初心者が取り組むべきなのでしょうか?個人的な意見
私の個人的な意見としては、NOです。特に独学の場合はあまりお勧めできないと思っています。
先ほど書いたようにRailsチュートリアルはとてもいい教材です。しかしながら、初心者向けかというとちょっと違うかな、と思います。なぜNO?
なぜ初心者向けではないと思うか、その最たる理由は初心者のレベル感は大きく異なるということです。
初心者といってもできることはかなり違ってくると思っています。
例えば、ProgateでRubyのコースを終えた方はプログラミング初心者と言えるでしょう。また一方で、自分でとりあえずは開発環境を作って、Progateの知識をつかって何か書いてみた!みたいな人もプログラミング初心者ということもできるでしょう。
つまり同じプログラミング初心者といってもできることの幅が大きく違うのです。そもそも初心者の線引きは人それぞれですしね。じゃあいつ、どうやってやるべきなの?
さて、NOとはいったものの、永久に基礎練を続けていては意味がありません。ではいつやるべきなのか。
ベストなタイミングは挑戦を何度もしてみて、行けそうだなと感じたときですが、これだと元も子もないので、ちょっとした自分なりの基準を置きたいと思います。まずは何を勉強したうえでやるべきか。
ここはProgateが個人的にも好きな教材なので、それを例にして書いていきます。
Progateでこれはやっておいておくべき、というものは
- Html&CSS
- Ruby
- Rails
- Command line
- SQL
です。このすべてをやって、少し足りないかな、という感じです。
さらに、ここに加えてWebの知識を付けておくべきだと私は思います。というのも、Railsは簡単にアプリケーションが作れてしまうため、何がどう動いているのかわからなくても、なんとなくものを作れてしまいます。しかし、なんとなくでやっているので知識として定着しなかったり、他の言語への知識の転用ができなかったりします。
なので、どんな形でもいいのでWebの知識をつけておくとベストです。おすすめのやり方
上に書いた勉強をしたうえでいよいよRailsチュートリアルに入っていきます。
そしてその時のおすすめのやり方ですが、
- Heroku(デプロイ)
- Bitbucket(バージョン管理)
- テスト
このすべてを飛ばしてまずは一周することです。
この部分がRailsではとても難しい部分になると思います。
特にテストで詰まると進まなくなってしまうので、まずは何か作り切る!というところに焦点をおいて、作ることに関係のない、上の三つの難しい部分は飛ばしてしまいましょう。
そして二週目でここの部分も余裕があれば触り、勉強していくと詰まらずにすすめると思います!終わりに
私はプログラミングを始めたとき、一緒に勉強する仲間がいました。しかし、Railsチュートリアルで自分以外が挫折してしまい、気づけば一人になってしまっていました。
適切なタイミング、適切な知識で勉強すればRailsチュートリアルはとてもいい教材です。しかし、その一方で、多くのプログラミング初心者を挫折させてしまっているものでもあります。(TwitterでRailsチュートリアルを始めて気づけば消える方の多い事!)
一人でもそんな人を減らせるように願います。
- 投稿日:2019-07-04T12:37:28+09:00
Rails6 のちょい足しな新機能を試す46(multidb connects_to 編)
はじめに
Rails 6 に追加されそうな新機能を試す第46段。 今回は、
multidb connects_to編です。
Rails 6 では、 複数DBに対応したため、DB接続先を切り変えることができるようになっています。Ruby 2.6.3, Rails 6.0.0.rc1 で確認しました。Rails 6.0.0.rc1 は
gem install rails --prereleaseでインストールできます。$ rails --version Rails 6.0.0.rc1今回の準備
今回は、 Rails6 のちょい足しな新機能を試す35(multidb migration --database オプション編)
をベースに作業を進めます。library 側にも users テーブルを追加する
backbone 側に
usersテーブルが存在しますが、library側にもusersテーブルを追加します。$ bin/rails g migration CreateUser name --db=librarydb:migrate を実行する
bin/rails g db:migrateUser モデルを作る
User モデルを作ります。
app/models/user.rbclass User < ApplicationRecord endBook モデルを作る
Book モデルを作ります。
booksテーブルは、backboneにはなく、library側にありますので、DB接続先をconnects_toを使って明示的に設定します。app/models/book.rbclass Book < ApplicationRecord connects_to database: { writing: :library, reading: :library } endRails 6.0.0rc1 では、
:writingと:readingの両方を指定する必要があるようです。connects_to database: :libraryだとエラーになりました。
今回は、Book クラスに直接 DB 接続先を記載しましたが、もし、Bookモデルの他にも、 library 側に接続する接続するモデルがあれば、
ApplicationRecordから派生させた抽象クラスを作成して、抽象クラスの中で、connects_toを使い、 Book クラスは、その抽象クラスから派生させた方が良いでしょう。(Rails Guide ではそういう書き方になってます。)seed データを作る
backbone側library側双方にusersテーブルが存在するため、両方のDBに接続してデータを登録するように、ActiveRecord::Base.connected_toを使います。
booksテーブルは、library側に存在しますが、Bookクラスで接続先をlibraryに指定しているため、ActiveRecord::Base.connected_toを使う必要がありません。db/seeds.rbUser.create(name: 'Taro') ActiveRecord::Base.connected_to(database: :library) do User.create(name: 'Hanako') end Book.create(title: 'Programming Ruby')seed データを登録する
$ bin/rails db:seedcontroller と View を作る
登録した seed データを表示するため、 controller と view を作ります。
bin/rails g controller Dashboard index
Userモデルにメソッドを追加するlibrary の
usersテーブルを検索できるようにUserモデルにall_in_libraryメソッドを追加します。
ActiveRecord::Base.connected_toを使ってlibrary側に接続先を変更してからallメソッドを呼び出します。app/models/user.rbclass User < ApplicationRecord def self.all_in_library ActiveRecord::Base.connected_to(database: :library) do all end end endDashboardController#index メソッドを修正する
登録したデータを取得するように
indexメソッドを修正します。app/controllers/dashboard_controller.rbclass DashboardController < ApplicationController def index @books = Book.all @users = User.all.to_a @users_in_library = User.all_in_library end endここで、 メソッドの2行目を
@user = User.all.to_aとしている理由は後述します。データを表示する View を作成する
各変数の値を表示するための View を作成します。
app/views/dashboard/index.html.erb<h1>Dashboard#index</h1> <table> <thead> <tr> <th> valiable </th> <th> value </th> </tr> </thead> <tbody> <tr> <td> @books </td> <td> <%= @books.map(&:title) %> </td> </tr> <tr> <td> @users </td> <td> <%= @users.map(&:name) %> </td> </tr> <tr> <td> @users_in_library </td> <td> <%= @users_in_library.map(&:name) %> </td> </tr> </tbody> </table>rails server を起動してブラウザで表示する
$ bin/rails shttp://localhost/dashboard/index にアクセスすると値が表示されます。
User.all ではなく User.all.to_a な理由
User.all にすると、 Taro の値が表示されず Hanako になってしまいます。
これは、
allメソッドがActiveRecord::Relationを返すだけで、 実際にSQLを発行しないためです。 このため、allにすると、 View を rendering するときに、SQLが発行されてしまい、接続先がlibraryに設定された状態で SQLが発行されてしまうようです。
これを防ぐために、DB の接続が library に変わる前に SQLを発行するため、意図的にto_aをつけました。app/controllers/dashboard_controller.rbdef index @books = Book.all @users = User.all # => ActiveRecord::Relation が返され SQLは実行されない。 @users_in_library = User.all_in_library # 接続先が library に変わる # このあと index View をレンダリングする際に、全てのSQLが発行される。 end
User.allの場合、View のレンダリングの際にすべての SQL が発行されていることがログからもわかります。Rendering dashboard/index.html.erb within layouts/application Book Load (0.3ms) SELECT "books".* FROM "books" ↳ app/views/dashboard/index.html.erb:20:in `map' User Load (0.3ms) SELECT "users".* FROM "users" ↳ app/views/dashboard/index.html.erb:28:in `map' User Load (0.5ms) SELECT "users".* FROM "users" ↳ app/views/dashboard/index.html.erb:36:in `map'
User.all.to_aとした場合、controller の中で SQL が発行されていることがわかります。User Load (0.4ms) SELECT "users".* FROM "users" ↳ app/controllers/dashboard_controller.rb:4:in `index' Rendering dashboard/index.html.erb within layouts/application Book Load (0.3ms) SELECT "books".* FROM "books" ↳ app/views/dashboard/index.html.erb:20:in `map' User Load (0.4ms) SELECT "users".* FROM "users" ↳ app/views/dashboard/index.html.erb:36:in `map'今回、試したコードが特殊な例かも知れませんが、挙動が少し紛らわしいので、今後のバージョンで修正されるかも知れませんね。
試したソース
試したソースは以下にあります。
https://github.com/suketa/rails6_0_0rc1/tree/try046_multidb_connection_switching参考情報
- 投稿日:2019-07-04T11:49:17+09:00
rubyの配列について
今更だけどrubyの配列の使い方について。Qiitaデビューを兼ねて勉強になったことを復習。
⭐map関数
配列の要素ごとに処理を実行して、
新たな配列を作成できます。例:各要素に対して、1を足した配列を返す。
???
%w|1 2 3 4 5|.map{|x| x.to_i+1}
???
結果:[2, 3, 4, 5, 6]⭐二つの配列の要素が、
全く同じ順番かどうかを比較する
???
if [1, 2, 3] == [1, 2, 3]
end
???
これでOK⭐ハッシュも比較が可能です。
???
if {a:1, b:2, c:3} == {a:1, b:2, c:3}
end
???⭐ハッシュと配列を合わさった配列でも大丈夫です。
???
If [{a:1, b:2}, {array:[3, 4, 5]}] == [{a:1, b:2}, {array:[3, 4, 5]}]
???⭐配列の中で重複しているものを削除する
???
arr = [1, 2, 5, 5, 1, 3, 1, 2, 4, 3]
arr.uniq
???
結果:[1, 2, 5, 3, 4]⭐配列が生成されていない時だけ代入する
???
list ||= ["1","2"]
???
- 投稿日:2019-07-04T11:20:00+09:00
今日の学習0703
今日やったこと
①(https://railstutorial.jp/chapters/beginning?version=5.1#cha-beginning) を参考にわからない単語のピックアップ。
→Ruby gem
bundle install
bundle updateの実行方法
MVCやREST
ジェネレータ、マイグレーション
ERB
scaffold
MITライセンス
DSL (ドメイン固有言語)
API
gem (ページネーションや画像アップロードといった特定の問題を解決するためのgemなど)、
クラウド統合開発環境 (クラウドIDE)
AWS Cloud9
Bashなどの標準的なシェルコマンドラインインターフェイス
②授業の動画の復習
③実際に演習
Ⅰ、Ruby on Railsで使うRuby gemはどのWebサイトにありますか?
→ (RubyGems.org)として置かれている
Ⅱ、現時点でのRailsの最新バージョンはいくつですか?
→https://rubyonrails.org/ で確認。現在は5.2.3
Ⅲ、Ruby on Railsはこれまでに何回ダウンロードされたか?
→ RubyGems.orgで確認。現在は37,303,000,945。学んだこと
①開発環境を大別すると、テキストエディタやコマンドラインを使う環境と、IDE (統合開発環境) の2つに分けられる。
②ローカルサーバーはコントロール+Cで閉じることが可能。
③Railsのアプリを表示するには、http://localhost:3000/
- 投稿日:2019-07-04T10:46:21+09:00
fields_forについて
はじめに
fields_forとはform内で異なるモデルを編集することができるようになるもの。
今回はitemテーブルとimageテーブルが1対多の関係の時にimageテーブルに画像を保存できるようにするために使用した。使い方
new.html.haml.item-contents__image %label .item-contents__image__box .input-area .item-contents__image__box__text ドラッグアンドドロップ またはクリックしてファイルをアップロード #file-preview = f.fields_for :images do |i| =i.file_field :url, class: 'item-contents__image__drop-file', type: "file"item.controller.rbdef new @item = Item.new @item.images.build end def create Item.create(item_params) redirect_to root_path, notice: '商品を出品しました' end private def item_params params.require(:item).permit(:name, :description, :price, images_attributes: [:url, :item_id]) enditem.rbhas_many :images accepts_nested_attributes_for :imagesimage.rbbelongs_to :item mount_uploader :url, ImageUploaderこれらの記述をすることでimageテーブルに画像を保存することが可能になります。
- 投稿日:2019-07-04T00:21:37+09:00


