- 投稿日:2019-12-21T23:55:56+09:00
1年業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- Javascript (業務ではTypeScriptを使っています)
- React
- Redux
- axious
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
ChromeやFirefox, Safariなどのブラウザに対応
ブラウザからXMLHttpsRequestsを作成
リクエストとレスポンスデータを変換
Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッドを定義しています。componentDidMount
メソッドは、Reactのライフサイクルメソッドの一種で、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なっています。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、完結なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
todo_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
半年間業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
- eslint
- prettier
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
ChromeやFirefox, Safariなどのブラウザに対応
ブラウザからXMLHttpsRequestsを作成
リクエストとレスポンスデータを変換
Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッド,render
メソッドを定義しています。componentDidMount
メソッド,render
メソッドは、Reactのライフサイクルメソッドの一種で、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
todo_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
業務で学んだ技術を使ってTODOアプリ作ってみた ~ 備忘録も兼ねて ~
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
- eslint
- prettier
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
- ChromeやFirefox, Safariなどのブラウザに対応
- リクエストとレスポンスデータを変換
- Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッド,render
メソッドを定義しています。componentDidMount
メソッド,render
メソッドは、Reactのライフサイクルメソッドの一種です。componentDidMount
メソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
大半の方がご存知かと思いますが、Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルからレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
todo_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
React+Railsでモダン()なTODOアプリを作ってみた ~ 備忘録も兼ねて ~
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったWEBアプリの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
- eslint
- prettier
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
- ChromeやFirefox, Safariなどのブラウザに対応
- リクエストとレスポンスデータを変換
- Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッド,render
メソッドを定義しています。componentDidMount
メソッド,render
メソッドは、Reactのライフサイクルメソッドの一種です。componentDidMount
メソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルのレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
controllers/todos_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
config/routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:55:56+09:00
React+Railsでモダン()なTODOアプリを作ってみた 〜備忘録も兼ねて〜
Opt Technologies Advent Calendar 2019の21日目の記事です。
はじめに
2019年4月に株式会社オプトに入社したsh1okohと申します。
なんやかんやありまして、今はReact, Redux, Railsなどを使ったプロダクトで、日々奮闘しております。
今回は、お仕事中に出会った技術スタックを使ってモダン()なTODOアプリを作ってみたので、各種ライブラリの紹介をしつつ、Todoアプリの紹介をしていきたいと思います。(備忘録的な目的もあります)ちなみに、今回のTODOアプリのソースコードは下記リンクにありますので、
興味のある方はみてみてください。https://github.com/sh1okoh/adventcalendar
対象読者
今回は、テーマ的にも内容自体は広く浅くになっております。
各種ライブラリを使ったSPAの全体像を把握したい方などを対象としており、各ライブラリを深く学びたいといった方は対象外となっておりますので、よろしくお願いします。今回使用した技術
フロントエンド
- JavaScript (業務ではTypeScriptを使っています)
- React
Redux- axios
- eslint
- prettier
バックエンド
- Ruby 2.5.7
- Ruby on Rails 5.2.4
- rubocop
- guard
- rspec
- rack-cors
CI/CD
- Circle CI
フロントエンド編
React とは
A JavaScript library for building user interfaces
Facebookの作ったJavascriptライブラリです。
MVC(Model View Controller)モデルでいうとVの部分を提供しています。Reactの考え方
Component-Based
Build encapsulated components that manage their own state, then compose them to make complex UIs.Reactでは、画面をComponentの親子関係を持つツリーとして構成します。
各コンポーネントは、親から渡されたprops(プロパティ)、State(自分の状態)を元に、renderメソッドでDOM(Virtual DOM)を生成します。axios とは
Promise based HTTP client for the browser and node.js
https://github.com/axios/axios
Http通信を簡単に行うことができるJavascriptライブラリです。
主な特徴としては、
- ChromeやFirefox, Safariなどのブラウザに対応
- リクエストとレスポンスデータを変換
- Promise APIをサポート
と言った特徴があります。
今回は、このライブラリを使ってAPIとの通信を行なっていきます。
実装
以下がコードになります。
import React from 'react'; import axios from 'axios'; const Title = ({todoCount}) => { return ( <div> { todoCount > 0 ? <h1>{todoCount}つのタスクがあります</h1> : <h1>タスクがありません</h1> } </div> ); } const TodoForm = ({addTodo}) => { let input; return ( <form onSubmit={(e) => { e.preventDefault(); addTodo(input.value); input.value = ''; }}> <input className="form-control col-md-12" ref={node => { input = node; }} /> <br /> </form> ); }; const Todo = ({todo, remove}) => { return ( <li> <a href="#" className="list-group-item">{todo.contents}</a> <button onClick={() => {remove(todo.id)}}>削除</button> </li> ); } const TodoList = ({todos, remove}) => { const todoNode = todos.map((todo) => { return (<Todo todo={todo} key={todo.id} remove={remove} />) }); return (<div className="list-group" style={{marginTop:'30px'}}>{todoNode}</div>); } window.id = 0; class TodoApp extends React.Component{ constructor(props){ super(props); this.state = { data: [] } this.apiUrl = 'http://localhost:3000/api/todos/' } componentDidMount(){ axios.get(this.apiUrl) .then((res) => { this.setState({ data: res.data }); }); } addTodo(val){ const todo = {contents: val} axios.post(this.apiUrl, todo) .then((res) => { this.state.data.push(res.data); this.setState({data: this.state.data}); }); } handleRemove(id){ const remainder = this.state.data.filter((todo) => { if(todo.id !== id) return todo; }); axios.delete(this.apiUrl+id) .then((res) => { this.setState({data: remainder}); }) } render(){ return ( <div> <Title todoCount={this.state.data.length}/> <TodoForm addTodo={this.addTodo.bind(this)}/> <TodoList todos={this.state.data} remove={this.handleRemove.bind(this)} /> </div> ); } } export default TodoApp上記のコードを見てもらうと分かりやすいと思うのですが、
TodoApp
コンポーネントにconstructor
メソッド、componentDidMount
メソッド,render
メソッドを定義しています。componentDidMount
メソッド,render
メソッドは、Reactのライフサイクルメソッドの一種です。componentDidMount
メソッドは、出力が DOM にレンダーされた後に実行されます。ここではライフサイクルの話は割愛しますので、気になる方は下記リンクを参考にしてみてください。
- https://ja.reactjs.org/docs/state-and-lifecycle.html
- https://qiita.com/Julia0709/items/3c3fc8d29fd2e56ed7a9
で、各関数コンポーネントに、propsを渡し、その値に応じた描画を行なうという流れになっております。
APIの疎通に関しては、
axios.getや
axios.postで行なって、APIからのレスポンスデータを取得したりしています。
で、実際に見てみると、初期の出力結果が、こうなります。(めっちゃ殺風景・・・。)
で入力欄に値を入力してEnterを押すと、タスクが追加できて、削除までできます。
本当は最低限CRUDの実装はしたかったのですが、諸々の事情により、updateの機能は後日追記いたします。
バックエンド編
Ruby on Rails とは
Rails is a web-application framework that includes everything needed to create database-backed web applications according to the Model-View-Controller (MVC) pattern.
https://github.com/rails/rails
Model-view-controller パターンを採用したフレームワークです。
特にActiceRecordは個人的には強力な機能だと思っています。今回はView側はReactで実装していますので、RailsのAPIモードを使って実装しました。
APIモードは、下記のようにコマンドライン引数に
--api
とつけるだけで、できてしまいます(さすがRails!)rails new my_api --apiAPIモード
https://guides.rubyonrails.org/api_app.htmlActive Record とは
Active Record is the M in MVC - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
https://guides.rubyonrails.org/active_record_basics.html
MVCモデル言うところの、Mの部分に相当するものです。
ORM(O/Rマッピング)システムに記述されている「Active Recordパターン」を実装したもので、このパターンと同じ名前が付けられています。ORM(O/Rマッピング)とは、アプリケーションが持つオブジェクトを、RDBMSのテーブルに接続することです。
ORMを用いると、SQL文を書かずに、簡潔なコードだけで、テーブルのレコードのデータを取得できたり、更新できたりしてとても便利です。例えば、ターミナル上で、
rails consoleを叩き
Todo.allなどとすると、下記のような出力結果を得ることができます。
またワンライナーでも書くことができるので、
SQL文を書くより遥かに楽だと言うことが分かると思います。他にも
- モデル同士のアソシエーションを表現する
- 関連付けられているモデル間の継承階層を表現する
- データをデータベースで永続化する前にバリデーションを行う
などの特徴を持っています。
RuboCop とは
RuboCop is a Ruby static code analyzer and code formatter.
https://github.com/rubocop-hq/rubocop
rubyのコードアナライザーであり、フォーマッターです。
例えば、チーム開発をするときにコード規約を定めると思うのですが、RuboCopを用いると捗ります。
また、警告を出すのみに止まらず、いくつかの問題を自動で直してくれることもしてくれます。
例えば、ターミナル上で下記のコマンドを実行すると、一括でフォーマットをかけてくれたりしますので、とても便利です。bundle exec rubocop -a参考
https://rubocop.readthedocs.io/en/stable/実装
実装はとても簡単で、ターミナル上で、下記のコマンドをバーン!すると、
ルーティングやコントローラー、モデルからテーブル定義までの雛形を作ってくれます。bundle exec rails generate scaffold Todo contents:string実際のコード
controllers/todos_controller.rb
class TodosController < ApplicationController before_action :set_todo, only: [:show, :edit, :update, :destroy] def index @todos = Todo.all render json: @todos end def show render json: @todo end def new @todo = Todo.new end def edit end def create @todo = Todo.new(create_params) if @todo.save render json: @todo else render :new end end def update if @todo.update!(update_params) render json: @todo else render :edit end end def destroy @todo.destroy render json: @todos end private def set_todo @todo = Todo.find(params[:id]) end def create_params params.require(:todo).permit(:contents) end def update_params params.require(:todo).permit(%i[id contents]) end def destroy_params params.require(:todo).permit(:id) end end
config/routes.rb
Rails.application.routes.draw do scope :api, defaults: { format: :json } do resources :todos, only: %i[show index create update destroy] end end
models/todo.rb
class Todo < ApplicationRecord def as_json(options = {}) super(options.reverse_merge(except: %i[created_at updated_at])) end end
db/schema.rb
ActiveRecord::Schema.define(version: 2019_12_15_104719) do create_table "todos", force: :cascade do |t| t.string "contents" t.datetime "created_at", null: false t.datetime "updated_at", null: false end end今回のAPIの設計方針として、REST APIを採用しました。
REST APIについては、下記を参照してみてください。
https://restfulapi.net/CI編
後日書きます
まとめ
半年間、自分が使ってきた技術を用いて、アプリーケーションを開発するのは良い振り返りにもなるし、一年の節目にも良さそうと思いました。今後も、何か学ぶ機会があれば、やってみるといいかもと思いました。
実はまだまだ書きたいこともあるので、後日追記するか、別の記事に載せることにします。その他の参考資料
https://qiita.com/matzkoh/items/90baab22ad489b78384b
https://restful-api-guidelines-ja.netlify.com/
https://scotch.io/tutorials/create-a-simple-to-do-app-with-react
- 投稿日:2019-12-21T23:00:30+09:00
[Rails]特定のインスタンスに対して前後のレコードを取得したりランダムで取得したりするあれを試してみた
はじめに
こんにちはどうも、pirikaraです。
髪の毛を切りました。特定の投稿(今回はitemの出品)に関して、DBから前後のレコードを取得して表示させたりリンクを飛ばす感じのあれです。
あとDBからランダムにレコードを取得して表示させたりリンク飛ばしたりする感じのあれです。まずは特定の投稿に関してDBから前後レコードを取得する奴から実装していきます。
Rails標準のAPIでは見つからなかったので、今回はmodelに対してメソッドを書き込んでいきます。いざ、実装
今回はItemクラスのインスタンスに関して、その前後レコードを取得するメソッドをmodels/item.rbに記述していきます。
前のレコードを取得するメソッドを『previous』
後のレコードを取得するメソッドを『next』
としてmodelに定義していきます。item.rbclass Item < ApplicationRecord #......省略 def previous Item.where("id < ?", self.id).order("id DESC").first end def next Item.where("id > ?", self.id).order("id ASC").first end end現在のインスタンス(self)のidより大きいか小さいかで前後レコードを判定・取得します。
作成したメソッドをview側で呼び出します。items/show.haml#......省略 - if @item.previous.present? =link_to item_path(@item.previous.id) do = @item.previous.name - else - else .none - if @item.next.present? =link_to item_path(@item.next.id) do = @item.next.name - else .none #......省略controller側で@item = Item.find(params[:id])などとし、Itemのshow画面に遷移。
@itemに対して『previous』 『next』メソッドを使用することで、@itemの前後レコードを取得できます。また、前後レコードがない場合(最新のレコード、もしくは最古のレコードの場合)にリンクを表示しないよう『present?』メソッドによって真偽判定、条件分けしています。
左右感がちょっと気持ち悪いですが、実装できました。(あとで直します。)
変化球
前後レコードではなく、DB内のデータをランダムで取得するあれを実装を試みてみます。
ネットサーフィンしながらいくつか方法を見つけたので試してみます。1. RAND()関数
MySQLのネイティブ関数RAND()を使用してみます。
items_controller.rbdef show #......省略 @random1 = Item.order("RAND()").limit(1) @random2 = Item.order("RAND()").limit(1) #......省略 end左右のリンクがあるので、それぞれ@random1、 @random2として変数をインスタンス変数を定義してみました。
viewはこちらitems/show.haml- if @random1.present? =link_to item_path(@random1.ids) do - @random1.each do |random1| = random1.name - else .none - if @random2.present? =link_to item_path(@random2.ids) do - @random2.each do |random2| = random2.name - else .noneRAND()関数で取得したデータにlimitつけてましたが、配列で取得されるみたいなので『ids』としないとid取れませんでした。
また、nameについてもeach文で取り出してあげないと表示されませんでした悲しい。そして結果がこちら。
......ねこもうさぎも被りました。このあと何回もページ遷移してみました。
確かにランダムで表示されるようにはなったようですが、左右の値が毎回同じでした。
実際にDBテーブル内に存在する値からランダムに取得してくれるのはありがたいですが、
randam1とrandam2で別々に値を取得してくれるなんてそんな都合よく世界はできていなかったようです。別の方法を試します。
2. Model.all.sample
モデルから全てのレコードを取得した上で1件抽出する感じ。
ゴリ押ししてみます。items_controller.rbdef show #......省略 @random1 = Item.all.sample @random2 = Item.all.sample #......省略 enditems/show.haml- if @random1.present? =link_to item_path(@random1.id) do = @random1.name - else .none - if @random2.present? =link_to item_path(@random2.id) do = @random2.name - else .none今回は配列でなくデータ単体を取ってこれたので、idsとかeach文なんてまどろっこしいことをせずに済みました。やったね。
気になる結果は......
で!!!け!!!た!!!
random1とrandom2で別々の値が取ってこれてます。さすがゴリ押し。
これにて実装完了......と思ったその時。見つけてしまいました。ActiveRecord でランダムなレコードが欲しい(1件、複数件)
『全件取ってランダムに1件取り出すのはダサい』
.........ダサい???
確かに全部とってランダムに1件はメモリやらなんやらに理解の浅い僕でも効率が悪いのは理解できます。
こういう時にindexが便利なのか......?とにかくダサいのは嫌なので別の方法を試します。(終わりが見えない)
3. Model.offset(rand(Model.count)).limit(1)
記事の方法をパクり...参考にさせていただきました。ありがとうございます。
レコード未満のランダム生成された整数をoffsetでレコード取得の開始位置とし、1個だけデータを取ってくる感じです。items_controller.rbdef show #......省略 @random1 = Item.offset( rand(Item.count) ).limit(1) @random2 = Item.offset( rand(Item.count) ).limit(1) #......省略 enditems/show.haml- if @random1.present? =link_to item_path(@random1.ids) do - @random1.each do |random1| = random1.name - else .none - if @random2.present? =link_to item_path(@random2.ids) do - @random2.each do |random2| = random2.name - else .none......each文でないと取り出せませんでした......idsも復活しました......
しかしoffsetで開始を指定している分、Model.all.sampleの時よりはスマートなデータ取得ができている気がします。
さて結果は......
......ランダムなってるやん。
さすがスマート。『自分のデータは除いて......』の実装はしていないのでGIFでは可愛いうさぎが延々と出現してしまっていますが、
きちんとランダムにリンクが生成されています。これで終わりにしようと思いましたが、僕は根に持つタイプなのでダサくない方法をもう一つ考えてみます。
4. Model.where('id >= ?', rand(Model.first.id..Model.last.id)).limit(1)
offset、 limitを使った書き方の書き換えとしてwhereに置き換える方法が紹介されていたので、試してみます。
randの引数でランダム数値生成の範囲指定を行いますが、今回は先頭レコードと最終レコードのidを範囲として指定しています。Rails: データベースのパフォーマンスを損なう3つの書き方(翻訳)
items_controller.rbdef show #......省略 @random1 = Item.where('id >= ?', rand(Item.first.id..Item.last.id)).limit(1) @random2 = Item.where('id >= ?', rand(Item.first.id..Item.last.id)).limit(1) #......省略 enditems/show.haml- if @random1.present? =link_to item_path(@random1.ids) do - @random1.each do |random1| = random1.name - else .none - if @random2.present? =link_to item_path(@random2.ids) do - @random2.each do |random2| = random2.name - else .noneこちらもeach文、idsを用いて配列からデータを取り出して出力しています。
結果は......
ランダムになってました。満足。
比較
rails consoleからSQL文とSQL発行の所要時間が確認できるので、
上記4つのものを確認・比較してみます。......DBにデータが7つしかないので速さは大差ないですね。そりゃそうか。
SQL文は4個目では3回発行されています。あまりよろしくないですね......
スマートな実装としては3個目推奨でしょうか。という訳でseeds.rbを読み込んでデータを1000件作ってみました。
seeds.rb1000.times do |index| Item.create!(name: "アイテム#{index}", seller_id: 1, description: "内容#{index}", category_id: index, condition_id: index, prefecture_id: index, sendingmethod_id: index, postageburden_id: index, shippingday_id: index, price: index, profit: index) endSQL発行の所要時間に大きく差が出ました。
1個目、2個目は3個目、4個目に比べて10倍以上の時間がかかってしまっています。
クエリ文が3回発行されてしまっていることを考えると、
今回試した手法の中ではoffsetを用いた3番目の方法がベストプラクティスとなります。おわりに
今回はSQLについて理解を深めることができました。
Indexを用いた検索でも検証を行ってみたいですね。
大規模なWebアプリケーションになればなるほど『どんな手法で検索をするか』で処理スピードに大きな差が生まれてしまうことを実感できました。また気が向いたらなんか書きます。
おわり。
- 投稿日:2019-12-21T22:17:39+09:00
しまうまプリントからEPSON Photo+への住所録の移行
背景
- これまでプリンタを自宅に持ってなかった1が、今年嫁がどうしても欲しいとのことでEPSONのプリンタ Colorio EP-881AB を購入した
- これまで年賀状はSaaSの しまうまプリント を利用していたが、せっかくプリンタを購入したので今年からは自分で印刷してみようと思った
- EPSONプリンタ用の無料アプリとして EPSON Photo+ というものがあり、これを使えば年賀状が簡単に作成/印刷できる
ということで、しまうまプリントからEPSON Photo+へ住所録を移行する必要があり、format変換用のruby scriptも書いたので記録しておく。
(やる前はまー普通にできるやろと思ってたが、 EPSON Photo+の住所録用CSVフォーマット仕様が開示されておらず 、ヘルプデスクと何往復かのメールのやり取りが必要だった。。。)Software version
Windows
- Windows 8.1 Enterprise
- EPSON Photo+: Version 3.1.0.0
Mac
- macOS Catalina(version 10.15.1)
- ruby 2.3.5p376 (2017-09-14 revision 59905) [x86_64-darwin18]
やり方
しまうまプリントからの住所録ダウンロード
- ここ の 「宛名データのダウンロード」ボタンをクリックすることで xlsx ファイルをダウンロードできる (address_data.xlsx)
- 手持ちのExcel等でダウンロードしたファイルを開き、CSVファイル形式で保存する (ここではaddress_data.csvという名前で保存されたと仮定する)
(参考) しまうまプリントのCSVフォーマット
headerの各カラムを列挙したのが以下。
印刷(する・しない) グループ※必須(全角30文字まで) 送付先※必須(自宅・会社) 姓※送付先自宅の場合必須 (全角10文字まで) 名※送付先自宅の場合必須 (全角10文字まで) 姓カナ(全角10文字まで) 名カナ(全角10文字まで) 会社名※送付先会社の場合必須(30文字まで) 会社名カナ(全角30文字まで) 部署名(全角20文字まで) 役職名(全角20文字まで) 敬称※必須※送付先自宅の場合(様・殿・先生・君・ちゃん・さん・御中)※送付先会社の場合(御中・様・殿・先生・御一同様) 郵便番号※必須(7桁) 都道府県※必須 住所1※必須(全角20文字まで) 住所2(全角20文字まで) 住所3(全角20文字まで) 連名1姓※送付先自宅の場合のみご利用可能(10文字まで) 連名1名※送付先自宅の場合のみご利用可能(10文字まで) 連名1敬称※送付先自宅の場合のみご利用可能(様・殿・先生・君・ちゃん・さん・御中) 連名2姓※送付先自宅の場合のみご利用可能(10文字まで) 連名2名※送付先自宅の場合のみご利用可能(10文字まで) 連名2敬称※送付先自宅の場合のみご利用可能(様・殿・先生・君・ちゃん・さん・御中) 連名3姓※送付先自宅の場合のみご利用可能(10文字まで) 連名3名※送付先自宅の場合のみご利用可能(10文字まで) 連名3敬称※送付先自宅の場合のみご利用可能(様・殿・先生・君・ちゃん・さん・御中) その他連名※送付先自宅の場合(ご家族御一同様)※送付先会社の場合(スタッフ御一同様、従業員御一同様、職員御一同様、の皆様へ)マルチバイト文字(日本語)が入っているかつ一行が長いのでプログラムとしては扱いづらい。
以下script内では列数をハードコードしているため、もし上記フォーマットが変更されていたら、合わせて以下scriptのハードコード部分の列数を変更する必要がある住所録のformat変換
- script をローカルにcopy
- 以下を実行
$ chmod +x convert_shimauma_to_epson.rb $ ./convert_shimauma_to_epson.rb address_data.csvこれで
output.csv
というファイルが同じディレクトリに生成されます。EPSON Photo+にインポート
ここ にインポート手順が記載されています。
既知の問題
- 連名1については敬称1で指定した値がちゃんと反映されるが、連名2以降の敬称が反映されない。
- 私の場合連名2以降が殆どなかったためimportしてから手入力してしまった。
- もしここも変換したい方は是非EPSONのヘルプデスクに問い合わせて頂きたい。わかったらこちらにもコメントつけてもらえれば本記事も更新します。
(2019-12-22 更新)
うおー、outputの
住所1
は30文字制限なのかよ!
CSV importするときはエラー等出ず無条件で後ろが切られる模様。30文字超える場合はちゃんと住所2
(マンション名等入力する欄)に入れてあげるようにせねば・・・。めんどくさい。
すでに印刷してしまったんだが。。。。(2019-12-22 更新2)
- とりあえず住所1 or 2が30文字超えてたら例外上げて落とすようにした。元データを修正して収まるようにしてね。
- コードをgithubにあげた。 https://github.com/shoutm/convert-shimauma-to-epson
そもそも自宅では殆どプリンタなんて使わなかった(電子ファイルで事足りる)し、必要であればコンビニ等のネットプリントを利用していた ↩
- 投稿日:2019-12-21T21:56:45+09:00
2019年フレームワークのトレンドが見れるサイトの紹介
hotframeworks.com
URLはこちらになります。
http://hotframeworks.com/
- 投稿日:2019-12-21T21:06:55+09:00
Codeanywhereを使ってスマホでプログラミングをしよう!
前置き
こんにちは
通勤時間などのスキマ時間で、スマホでプログラミングの勉強が出来ないかと思いcodeanywhereというアプリを使ってみました。
課金が前提となりますが、結論としてはかなり使えますcodeanywhereは、いわゆるクラウドIDEというもので、ブラウザさえあればどんなマシンでもプログラミングができますよという代物です。
似たような製品にAWSのCloud9がありますが現時点でそちらはスマホ対応してないみたいです。今回は、codeanywhereを使ってスマホのみでRubyのハロワをする手順を解説します。
(ちなみに、このQiitaの記事自体スマホから投稿してます。)なおcodeanywhereのプランには無料のプランもあるのですが、7日間しか使えないので諦めて課金しましょう。
有料プランにも複数ありますが、僕はFreelancerを選択しました。
8ドルとの事ですが、日本円だと2019/12時点で1,116円でした。
手順
まずは、プロジェクトを作成します。
countainerを選択します。
余談ですがcloud9だと環境がEC2インスタンスだったのですが、こちらではコンテナとなります。
今回はRubyのコードを書きたいのでRubyのコンテナを選択します。
とりあえずUbuntuを選択します。(CentOSでも、お好みで。)
Deploying containerと表示されます。
しばらく待ちましょう。
コンテナが出来たら、こんな風に詳細が表示されます。
RVMもRuby on Railsも最初から入ってるみたいです。
作成したコンテナをターミナルで操作してみましょう。
コンテナの画面の右にある、縦に並んだ・をクリックしてメニューを表示します。
SSH Terminalを選択します。
↓このようにターミナルが操作出来ます。
rubyやrailsのコマンドも、ちゃんと通る事を確認できます。ruby -vrvm list knownで、使えるRuby version の確認もできますね。
Codeanywhereの画面からrubyファイルを作れないか試したのですが、ちょっとやり方がわからなかったので↓のようにターミナルから作成します。
touch index.rbハロワを入力してあげます。
puts “Hello world”右上の番号のついた□で、タブを移動できます。
ターミナルのタブに移動して、↓の通りRubyファイルを実行します。ruby index.rb↑の通り、普通に実行できましたね。
以上
- 投稿日:2019-12-21T19:18:38+09:00
[Ruby on Rails]slickの矢印の画像を変更する方法[JQuery]
実現したいこと
slickの画像(スライド)を次に送るための両側の矢印の画像を好きなものにする。
色々参考にしたがうまくいかなかったので、自分がうまくいったやり方を残しておきます。
前提
Ruby on Railsで開発を行っています。slickは導入できているものとします。
Rails 5.2.4
ruby 2.5.1
jquery-rails 4.3.5手順
画像の用意
矢印にしたい画像をapp/asset/imagesフォルダに入れます。
矢印のデフォルト画像の変更
おそらく\2190と\2192の部分が、矢印のデフォルトの画像を指定している部分だと思います。なので、この部分を任意の矢印の画像に取り替えます。
slick-theme.scss変更前 $slick-prev-character: "\2190" !default; $slick-next-character: "\2192" !default; 変更後 $slick-prev-character: image-url("left.png") !default; $slick-next-character: image-url("right.png") !default;矢印の表示と位置調整
位置の調整は以下の記事を参考にさせていただきました。
https://qiita.com/milneo/items/3560cb01cba92c2ccb6f
これで、矢印が表示されたと思います。slider.scssはslickに対してcssを記述しているファイルです。名前などは人それぞれ違うと思うので気をつけてください。slider.scss.slick-prev{ left: 50px; height: 80px; width: 80px; z-index: 10; } .slick-next{ right: 50px; height: 80px; width: 80px; z-index: 10; }最後に
なんとか、矢印の画像の変更はできました。
ただ、まだコードの意味や自分が行ったことの意味を理解していないので、もっと勉強しなけらばなと思いました。参考
- 投稿日:2019-12-21T17:48:32+09:00
ランダムで”いい感じ”にグループ分けしてSlack通知するbotをRubyで開発してみた
はじめに
こちらはSlack Advent Calendar 2019の21日目の記事となります。
今回は、仕事を通じて作成したグループ分けのbotについて詳しく書いていこうと思います。
当初は「悪用厳禁! Slack活用における情報収集 黒魔術」というタイトルで、Slackを導入している企業や組織での情報収集の小ネタについて書こうと思いましたが、転職して日が浅いのでブラックなジョークは止めておこう(※違法ではないです)という保守的思考が働いたのと、どうせならエンジニアらしいことを優先して書きたいと考えたためお蔵入りとなりました。
そちらの方も、今後機会があればコラム的な感じで書いていきたいと思います(リアクション頂ければ書くためのモチベーションが高まります)。
背景・目的
作るに至った理由
元々は前職での”社内のシャッフルランチのグループ分け”を補助するために、個人的に開発したものです。
グループ分けを簡単に行うことができるWebサービスはもちろん存在していましたが、ポチポチする時間が長く工数が掛かったり、メンバーの抜け漏れが多いという課題がありました。
そこで、コマンド一発でグループ分けを実行するスクリプトを一度書いてしまって、業務負荷を減らせないかと考えたことが背景にあります。
”いい感じ”とは?
自前で実装した理由の一つに、「剰余をグループにしない」というものがありました。
例を挙げて少し詳しく説明します。全員で27人のチームがあり、ランチのためにチームを5つのグループに分けたいとしましょう。この場合「6人グループを2つと、5人グループを5つ」がいわゆる”いい感じ”に当たります。
しかし、様々なグループ分けのコードをググって探してみると、「6人グループが4つと、3人グループが1つ」というような、余りで構成されたグループが出来上がってしまうものが多くありました。
これでは各グループで均等にメンバーが割り振られず残念なランチになってしまうので、そのような”余りものグループを作らない”という実装を行いました。
ソースコード
※社内ランチの社内呼称が”クアトロランチ”だったため、リポジトリ名もそれにあやかる形になっております
実装
依存関係・バージョン
- Ruby:2.5.5(そこまで気にしなくて大丈夫)
- slack-ruby-client
- dotenv
- csv
スクリプト
以下のスクリプトをコマンドラインから実行することで、グループ分けを行います。
シャッフルをしている部分は、27行目の
members.shuffle
メソッドの部分です。あと、個人的な嗜好によりメソッドを細かい責務で分けておりますので、読みづらい方も居るかと思います。script/slack.rbrequire 'slack-ruby-client' require 'dotenv' require 'csv' Dotenv.load today = Date.today.strftime("%Y/%m/%d") groups, members = [], [] numbers_of_group = ENV['NUMBERS_OF_GROUP'].to_i comment = ENV['COMMENT'].to_s channel = ENV['CHANNEL'].to_s def setup_slack set_slack_api_token client = Slack::Web::Client.new return client end def set_slack_api_token Slack.configure do |conf| conf.token = ENV['SLACK_API_TOKEN'] end end def load_members(members) read_csv(members) members.shuffle end def read_csv(members) CSV.foreach('data/members.csv') do |member| members << member end end def create_groups(numbers_of_group, groups, members) numbers_of_group.times { create_group(groups) } generate_csv_file(members, numbers_of_group, groups) end def create_group(groups) empty_group = Array.new groups << empty_group end def generate_csv_file(members, numbers_of_group, groups) grouped_members_csv = CSV.generate do |csv| split_members(members, numbers_of_group, groups, csv) end save_csv_file(grouped_members_csv) end def split_members(members, numbers_of_group, groups, csv) members.each_with_index do |member, i| number = i % numbers_of_group group = groups[number] assign_member_into_groups(group, number+1, member[0], csv) end push_groups_into_csv_file(csv, groups) end def assign_member_into_groups(group, number, member, csv) group << number unless group.include?(number) group << member end def push_groups_into_csv_file(csv, groups) groups.each do |group| csv << group end return csv end def save_csv_file(grouped_members_csv) File.open('data/grouped_members.csv', 'w') do |file| file.write(grouped_members_csv) end end def send_slack_api_with_csv(client, today, comment, channel) client.files_upload( channels: channel, as_user: true, file: Faraday::UploadIO.new('data/grouped_members.csv', 'text/csv'), title: "#{today} quattro groups", filename: 'grouped_members.csv', initial_comment: comment ) end client = setup_slack members = load_members(members) create_groups(numbers_of_group, groups, members) send_slack_api_with_csv(client, today, comment, channel)コード:https://github.com/f-teruhisa/quattro-bot/blob/master/script/slack.rb
.envファイル
以下の
.env
ファイルに設定情報を追記し、ルートディレクトリに格納しておきます。SLACK_API_TOKEN= NUMBERS_OF_GROUP= CHANNEL= COMMENT=設定する情報はそれぞれ以下の通りです。
envファイルのため、文字列をクォートで囲ったりする必要はありません。SLACK_API_TOKEN=後述するSlackのAppを作成し取得するAPIトークン NUMBERS_OF_GROUP=グループの数 CHANNEL=Slackのチャンネル(#は省略) COMMENT=Slack通知する際に加えたい文章ちなみに、
COMMENT=Slack通知する際に加えたい文章
に関しては、改行を含む場合のみシングルクォーテーションで囲むことで、改行を含んだメッセージを表現することができます。csv
このスクリプトを実行するにあたり、以下の2つのcsvファイルを活用します。
members.csv
data
フォルダに、元データとなるグループ分けする対象のmember.csv
ファイルを格納します。以下のサンプルデータの通り、1つのデータを1行1カラムで格納してください。members(sample).csvFukumoto Tanaka Yamamoto Inoue Aida Ueda Suzuki Sato Nakamura Fujioka Ito Hara Ohara Yamasaki Kawai Kondo※元が社内ランチ用のプログラムなため
member
という名前になっていますgrouped_members.csv
こちらは、スクリプトを実行した後に自動生成されるcsvファイルです(よって最初は存在しないファイルです)。
元データをグループ分けした結果が、
grouped_members.csv
として吐き出されます。Slackにはこのcsvファイルをメッセージに添付してPOSTするという形を取っております。以下のサンプルデータは、先程のmember.csv
を5グループに分ける設定をし実行してみた結果です。grouped_member(sample).csv1,Kawai,Yamamoto,Nakamura,Inoue 2,Tanaka,Yamasaki,Ueda 3,Fukumoto,Hara,Ohara 4,Aida,Fujioka,Ito 5,Sato,Suzuki,Kondo便宜上”グループ○○(数字)”という指定ができコミュニケーションが円滑に行われるように、各グループに番号を振っています。また、番号を振ることで、番号を先頭とした序列が各グループ内で暗黙的に作られるように設計しました。
実行手順
基本的にはGithubに乗せています...が、英語なのとSlackの操作までは記載しておらず、若干不親切なのでこちらで詳細に記載します。
「SLACK_API_TOKENの取得」と「ワークスペースへのアプリケーション導入」の2つがゴールとなります。
Custom IntegrationsからSlack botを作成
Slackで開発できるbotにはいくつか種類があり、設定が複雑なので以下より画面を追って説明していきます。
今回は「Custom Integrations」からbotを開発していきます。
ちなみに、以下のような画面です。上記の「Bots」をクリックすることで、Slackに登録するbotを設定していくことが可能です。
ちなみに、Slackのワークスペースが分かる場合は、以下のURLから直接遷移することができます。
https://{Slackのワークスペース名}.slack.com/apps/manage/custom-integrations
SLACK_API_TOKENを取得
「Bots」をクリックすると、以下のような画面が出てきます。
「新規作成」と「編集」のどちらでも、SLACK_API_TOKENは取得できます。「新規作成」の場合はbotの名前を入力した後に、「編集」の場合は直接以下の画面に遷移するので、この画面でSLACK_API_TOKENを取得できます。後ほどenvファイルに書き込むので、コピーしておきましょう。
リポジトリをcloneする
冒頭に紹介したリポジトリを、ローカル環境の任意のフォルダにcloneしてください。
$ git clone https://github.com/f-teruhisa/quattro-bot.git
.envファイルに設定情報を記載
前半に紹介した
.env
ファイルにSLACK_API_TOKEN含む各設定情報を追記し保存してください。グループの数やメッセージの内容は任意のもので構いません。csvファイルに元データを格納
data/members.csv
に、前半に紹介した通り、グループ分けしたいデータを一列に格納し保存してください。ワークスペースへのアプリケーション導入
作成したbotからSlackの任意のチャンネルに通知されるように、アプリケーションを導入しましょう。
通知を飛ばしたいSlackのチャンネルに入り画面の左上、チャンネル名をクリックするとメニューバーが現れます。
このメニューから「Add an app」をクリックし、作成したbotを導入します。「Add an app」をクリックすると以下のような画面が表示されるので、先程設定したbotの名前で検索すると表示されるはずです。選択して「Add」を選びましょう。
これで、Slack側でもbotから通知を受けるための準備が整いました。
コマンド実行
さて、ここまでで準備は終わりです。
以下のコマンドを実行して、グループ分けとSlack通知を行いましょう。$ ruby script/slack.rbちなみに、実行には依存gemが必要ですがGemfileにまとめておりますので、そちらを参考にしてgemのインストールも合わせて行ってください。以下のコマンドを一度に実行すれば、インストールが完了するかと思います。
$ gem install dotenv\ csv\ slack-ruby-clientSlack通知
設定した情報が正しければ、以下のようにSlackに通知が飛んでくると思います。
一度設定してしまえば、あとはコマンドを実行するだけなので、グループ分けに悩まずに済みます。※実際に、仕事でアップデートを担当するgemを分けた時に使った通知メッセージ
今後考えていること
それなりに使う機会に恵まれたbotですが、以下のような課題感もあるので、今後
ヒマがあれば徐々に対処していきたいです。定期運用に向いていない
今回のbotはインスタントなグループ分けには重宝するのですが、部署や名前などの属性は考慮していないため、普通にガンガン衝突します。そのため、(偶然ですが)同じヒトやモノと何度も同じグループになることが多々あります。
衝突を防ぐためには、属性値の考慮や過去の結果を格納するDBを用意するなどの工夫が考えられます。
また、コマンドを叩かないとグループ分けやSlackへの通知が行われないため、実行者が忘れたり寝坊すると死にます。
これは、後にも説明するGoogle App Script(GAS)への移行やLamda化などで対応が可能であると考えておりmす
GUIで実行できない
工数を考慮しGUIは実装しなかったのですが、ターミナル操作を行うため非エンジニアのメンバーが利用するハードルは高いです(一応セットアップと実行するコマンドを教えれば事足りますが)。
この辺は軽めのUIを作って、herokuにデプロイすることでブラウザでの操作を行う拡張ができるでしょう。
csvの管理工数
社員情報など、マスタ情報がスプレッドシートやエクセルで管理され、氏名が縦に並んでいる(≒まんまコピペして
member.csv
に格納できる)ことを前提にcsvからデータを取り出す処理を書いていますが、「新入社員など新しいデータが必要な際に追加を忘れ、ナチュラルにハブってしまう」可能性があります。これは、「(前職で)社内にRubyエンジニアが多くメンテナンスしやすいだろう」という安易な理由でRubyを選定したことがやや裏目に出てしまいました。
マスタデータをスプレッドシートで管理している会社も多い(前職はそうでした)ので、GASで実装するほうがシンプルに保てたと感じます(Git管理しにくい、スプレッドシートに依存するというデメリットはありますが)。また、GASではサーバレスに定期実行を行うことも可能なので、先に上げた”定期運用に向いていない”という課題にも対処できます。
さいごに
最後までお付き合い頂きありがとうございました。
筆者はこのbotスクリプトの開発が、人生初のいわゆる”コーポレート・エンジニアリング”だったのですが、自分の書いたスクリプトを何度も使ってもらい、実際に価値を生んでいる瞬間を何度も見ることができるのは、エンジニアとしても成功体験であったと振り返っています。
これからも「イケてないなあ...この作業」というプロセスを見かけたら、積極的にハックしていけるような精神を大事にエンジニアとして成長していきたいと思いました。
みなさんもSlackというすばらしいチャットツールを最大限に活用し、ときにはハックしてみると面白い経験ができるかもしれません。ぜひ試してみてください。
- 投稿日:2019-12-21T17:37:17+09:00
まつもとゆきひろさんの講演【若手エンジニアが市場価値を上げるために必要なこと】まとめ
こんにちは。KENと申します。
タイトルの通り、まつもとゆきひろさんさん(本文中ではMatzさんと呼ばせていただいています)の講演会にオンラインで参加してきましたので、そこで仰っていたことを忘れないようにまとめようと思います!
この講演の目的
- 参加者にエンジニアとして今後を考えてもらう。
- 参加者に何かを始めるきっかけを与える。
そもそもなぜ市場価値を高めた方が良いのか?
市場価値が高いと得をする。
主に下の2点で得をする。
- 給料を上げられる。(正しい判断をしてくれる会社であれば)
- ストレスを減らせる。イライラすることを減らせる。
なぜストレスが減る?
本当にやめるかどうかは別としていつでも今の会社を辞められる状態になれるから。
→我慢しなくても良くなる。つまり心理的安全を確保できる。
→笑顔で働くことができる。Matzさんオススメのストレスを減らす技
There must be a reasonを3回唱えること!
例えば運転中後ろの車にすごく煽られて、しまいには猛スピードで抜かれた時。イライラしますよね。
そういうときに、「あーあの人はたぶん、今会社に遅刻しそうで、遅れたらこっぴどく?られるから急いでいるのかな?」など理由を考えてみる。
こうすることでイライラする自分に言い聞かせることができる。では市場価値とは何か
自分の価値?スキル?知識?
市場価値とはつまり他人からの評価です。
市場価値 = 他人からの評価つまり自分で市場価値をコントロールするのは不可能に近い。
相手目線の判断だから。
自分の視点と他人の評価にはギャップがある。
だからスキルさえ上がれば市場価値が上がるというのは幻想にすぎない。Rubyも同じ。
MatzさんがRubyのヒットをコントロールすることはできなかった。コントロールはできないけど、市場価値を高める努力はできる
MatzさんがRubyを作る際にできたのは精一杯良い言語を作ることのみ。
つまり「人事を尽くして天命を待つ」!特に下記の要素を得る努力はとても大事。
- インデックスを作る(誰に聞けば、どう調べればその情報を得られるのかを知っていること)
- 経験
- キャリア (掛け合わせは強い エンジニアの後エンジニア採用人事とか)
- コネクション (例えばgoogleの採用。普通のルートだとコンピューターサイエンスのmaster持っていないと面接してもらえないが、リファラルだと持っていなくても面接してくれることもあったり。)
でもこれらは全部自分の視点になりがち。
市場価値を高める方法
- インベントリ
- コネクティング・ドッツ
- 情報発信(Twitter, Qiita, GitHub, Quora, OSS Github)
①インベントリ
自分のことについての棚卸し。まとめをする。
つまりちゃんと自分興味、関心、自分と他人の違う点を知ろう。他人との違いは大切
日本では同調圧力とかもあるが、同調圧力への鈍感さも大事。
違うからこそ価値が生まれる!
もちろん労働の管理者側からすると、同じような人材である方が管理しやすいし、交換しやすいという同質性のメリットはある。でも同質性を重視する組織とはWin Winの関係が築けない。
Matzさん的には同調圧力への鈍感さを伸ばし、違いを認めてくれる企業とのWinWinの関係性を作ることをオススメする。7つの習慣によると持続可能な取引は2種類のみ。Win WinかNo Deal。つまり同質性を重要視するような企業とはWin Loseになる必要はない。No Dealで良いのだ。
②コネクティング・ドッツ
スティーブ・ジョブズの有名な言葉。
好きなことや関心ごとを追求していった結果、それが繋がって線となり何かが新しいモノが生まれる。つまり自分の興味、関心をインベントリで知ったら、それを全力でやろう。
Matzさんの場合
プログラミング、心理学への興味があり、英語などもたまたま使う機会に恵まれて勉強していった。そういう組み合わせがあってRubyが生まれた。まさにコネクティング・ドッツの人生。実は高校時代、理系なのにも関わらず英語、国語が得意で、数学や物理は苦手分野だった。③情報発信
何か行動し始めたら、それをTwitterでも何でもいいから外に発信しよう。
他人の視点に立ち、あなたが人を採用する側だとすればどんな人が欲しいだろうか?と考え、自分を魅力的に見せることを意識しよう。あなたが全く外に発信してもないのに、あなたの才能をたまたま見出してくれるという漫画のようなことはまず起こらない。。
だから他人があなたを見つけられるように、発見してもらえるように発信しよう。ちゃんと魅せる相手を選ぶ。
ちゃんとWin Winの関係を築けるところ(会社など)にアピールしよう。
相手を侮ったり、下に見てはいけない。
全員を自分よりスゴイ所を持っていると思っておく。すごく大切なあと二つのこと
①笑顔
笑ってみよう
ゴキゲンでいよう。あなたの魅力がプラスになる。②ストーリーの力
自分のエピソードを紐づけて話す人は色んな人を惹きつける力がある。
話が魅力的な人は自分が体験したストーリーをたくさん持っている。ネタ帳を持とう!まとめ
講演に参加する前は、市場価値なんてスキルさえ上げれば上がるんじゃないかと安直に考えていました。
しかし価値や評価というのは自分ではなく、他の人が決めるもの。
いかに自分という人間を他人に伝えるのか。この大切さを改めて認識できた良い機会でした。では最後まで読んでくださりありがとうございました!!
- 投稿日:2019-12-21T17:01:58+09:00
herokuにruby on railsアプリをmysqlを使用してデプロイする。deploy
前提条件
nodeとyarnは必ず最新にしておいてください。
それは書きません。プロジェクトの作成
noracorn-railsというプロジェクトで作成します
ghq get https://github.com/noracorn/noracorn-rails.git rails new noracorn-rails cd noracorn-railsscaffoldで、適当にサイトを作成
rails generate scaffold person name:string age:integer rake db:migrateトップページをいったんscaffoldで作成したものにする
noracorn-rails/config/routes.rbRails.application.routes.draw do resources :people root 'people#index' end立ち上がることを確認しましょう
以下のコマンドを打ってから、http://localhost:3000/にアクセス
Personのscaffoldにアクセスできることを確認するbundle install rails serverherokuでアプリを作成する
heroku login heroku create noracornrailsherokuにmysqlを追加
(herokuにクレジットカードを登録してないと追加できません。)
heroku addons:add cleardb:ignite -a noracornrailsdatabase設定
noracorn-rails/database.ymlproduction: <<: *default url: <%= ENV['DATABASE_URL'] %> username: <%= ENV['DATABASE_USERNAME'] %> password: <%= ENV['DATABASE_PASSWORD'] %>以下のコマンドを打つと、データベース接続情報が表示されます。
heroku configこんな感じで表示されます
mysql://user_xxx:password_xxx@host_xxx/dename_xxx?reconnect=true表示された接続情報をherokuの環境変数に入れていく
heroku config:set DATABASE_URL="mysql2://user_xxx:password_xxx@host_xxx/dename_xxx?reconnect=true" heroku config:set DATABASE_USERNAME="user_xxx" heroku config:set DATABASE_PASSWORD="password_xxx"config.assets.initialize_on_precompile = falseを、application.rbに入れる
noracorn-rails/config/application.rbmodule NoracornRails class Application < Rails::Application config.load_defaults 6.0 config.assets.initialize_on_precompile = false end endproduction.rbのconfig.assets.compileをtrueにする
noracorn-rails/config/environments/production.rbconfig.assets.compile = trueGem Fileを編集する。
ローカル環境とテストでは、sqlite3を使用してherokuではmysqlを使用するようにした。
上のほうで定義されているsqlite3は、コメントアウトしておいてください。#gem 'sqlite3', '~> 1.4' group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] gem 'sqlite3', '~> 1.4' end group :development do # Access an interactive console on exception pages or by calling 'console' anywhere in the code. gem 'web-console', '>= 3.3.0' gem 'listen', '>= 3.0.5', '< 3.2' # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'spring' gem 'sqlite3', '~> 1.4' gem 'spring-watcher-listen', '~> 2.0.0' end group :production do gem 'mysql2' endbundle installする
bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include" bundle installすべてコミットして、herokuにpush
git add . git commit git push origin master heroku git:remote --app noracornrails git push heroku masterherokuのmysqlをマイグレーション
heroku run rails db:migrateアプリが動いているか確認する
heroku open
- 投稿日:2019-12-21T16:27:01+09:00
Ruby on Railsでドメイン駆動設計をやってもいいじゃないか。
挨拶
松下と申します。みんなの株式を運営しています。
ドメイン駆動設計の導入した動機
最初にドメイン駆動設計を導入しようとしたのは2017年の末頃でした。
ドメイン駆動設計を導入しようとした最初の動機はフレームワークやgemに依存しないPORO(Plain Old Ruby Object)なコードを増やす事によって、保守性(テスト容易性、理解容易性、変更容易性)を高めようとした事だったと思います。
10年近くRuby on Railsのアプリを無計画に実装を繰り返した結果、肥大したコントローラやモデルやヘルパー、改修する事による影響範囲が読めないライブラリや自作gemなどが散見され、テストのカバレッジも酷い状況でした。
最初はデザインパターンを部分的に導入してみたりもしてみましたが、レイヤードアーキテクチャを元にしたドメイン駆動設計をしっかりやろうという事になるまではそれほど時間はかかりませんでした。
現在ではバッチ処理や複雑なドメインは単体でのマイクロサービス化する事で本体アプリをスリムにする事を平行で行いつつ、クリーンアーキテクチャのオニオンアーキテクチャを参考にした本体アプリケーションのリファクタリングを進めています。
ユビキタス言語とリファクタリング
ドメイン駆動を導入するに当たって大事にしようと決めた事は、チームの全員がユビキタス言語を意識して使う事と、ドメイン知識は納得いくまで話し合いながら実装する事です。
ユビキタスは日本語名と英語名(クラスやメソッドとして利用する)の両方の視点からDomainやValueObjectを細かく決め、スプレッドシートで管理しています。
一度決めたユビキタス言語でも、必要があって全員の同意が取れれば、必要に応じてアップデートします。(その場合はソースも全て書き換えるのでそれなりの作業にはなります)
それまで各技術者が自由に実装していたのですが、共通の理解を得るための話し合いをする時間が増えました。設計レビューもしっかり行いましたが、一旦形にした後でも、納得がいかなければ何度も話し合いながらリファクタリングを行う事が当たり前になりました。
ディレクトリ構成と登場クラス
だいたい下記の様に配置しています。
app/ ├─ assets/ # CSS ├─ controllers/ # コントローラ ├─ domains/ # ドメイン │ └─ users/ │ ├─ user.rb # ドメインモデル │ ├─ factory.rb # ファクトリ │ └─ repository.rb # レポジトリ ├─ helpers/ # ヘルパー ├─ models/ │ └─ user_record.rb # データモデル ├─ services/ # アプリケーションサービス ├─ use_cases/ # ユースケース │ └─ users/ │ ├─ show_interactor.rb # インタラクタ │ ├─ show_presenter.rb # プレゼンタ │ └─ show_view_model.rb # ビューモデル └─ views/ # ビューテンプレート └─ users/ └─ show.html.erbController
class UsersController < ApplicationController def show @vm = Users::ShowInteractor.new.call(params[:id].to_i) end endコントローラからはユースケースに合わせたインタラクタを呼びます。
一つの画面に複数のユースケースが有る場合は複数のインタラクタを呼びます。CleanArchitecture的にはインタラクタの返り値はvoidにしてオブザーバパターンなどでビューを描画した方が良いのかもしれませんが、
フレームワークの制約で描画処理(render)が一度しか呼べないといった事もあり、インスタンス変数にビューモデルをアサインしてテンプレート側で表示する様にしています。Interactor
class Users::ShowInteractor include UseCaseInteractor def initialize(params = {}) @repository = params.fetch(:repository, Users::Repository) @presenter = params.fetch(:presenter, Users::ShowPresenter) end # @param [Integer] user_id # @return [Users::ShowViewModel] def call(user_id) user = @repository.find(user_id) @presenter.new.call(user) end endコンストラクタで依存性の注入をできる様にしているのでテストが書きやすくなります。
同じドメインを取得しても表示側の都合が違う場合はプレゼンタを置き換える事で表示側の都合だけを切り替える事ができます。
複数のレポジトリからドメインモデルを集める必要が有る場合はDTO(OutputData)を組み立ててプレゼンタに渡す場合もあります。Repository / Factory / User / UserRecord
class Users::Repository include DomainRepository def self.find(id) record = UserRecord.find_by(id: id) Users::Factory.build(record) end end class Users::Factory include DomainFactory def self.build(record) Users::User.new( nickname: Nickname.new(record.nickname), age: to_age(record.birthday), ) end private def to_age(birthday) age = ((Time.zone.now - date_of_birth.to_time) / 1.year.seconds).floor Age.new(age) end end class Users::User include DomainEntity attr_reader :nickname, :age def initialize(params) @nickname = params.fetch(:nickname) @age = params.fetch(:age) end end class UserRecord < ApplicationRecord self.table_name = 'users' endレポジトリはドメインモデル(集約)を返します。
ActiveRecordはDAOとしてのみ利用していています。
複雑なテーブル取得の条件が必要な場合はQueryObjectクラスを作成する事もあります。
ValueObjectも必要に応じて作成しています。Presenter / ViewModel
class Users::Presenter include UseCasePresenter def call(user) Users::ViewModel.new( nickname: with_nickname_title(user.nickname), age: with_age_unit(user.age) ) end private def with_nickname_title(nickname) "#{nickname}さん" end private def with_age_unit(age) "#{age}歳" end end class Users::ViewModel include UseCaseViewModel # @return [String] ニックネーム attr_reader :nickname # @return [String] 年齢 attr_reader :age def initialize(params) @nickname = params.fetch(:nickname) @age = params.fetch(:age) end endヘルパーはViewのグローバルを汚染するのでなるべく利用しない様にしています。
UIの都合はプレゼンタに実装して、ビューモデルを組み立てます。
テンプレート側ではビューモデルの属性を表示するだけの実装になる様にしています。Ruby on Rails と ドメイン駆動設計
Ruby on RailsのRailに乗る事こそが至上であるという方には、この様な取り組みを疑問視する方もいらっしゃると思います。
class UsersController < ApplicationController def show @user = User.find(params[:id]) end end確かに今回のソースの例では、上記の様にコントローラに一行書いて、年齢の処理はモデルに書けばそれだけで同じ動きが得られます。
ですが、同じサービスを長く続けていれば、データプロバイダが変わる事によってテーブルの定義は何度も見直されますし、意図してこなかった用途で貯めたデータを再利用したいという企画も出ますし、外部のDBやAPIから直接データを取得して作るサービスなどを作るという事もでてきます。
データプロバイダ側の都合や既存データの設計上の都合に合わせてコントローラやビューを一々組み直す事には大変な労力がかかります。
改修による影響範囲が読めなければそもそも改修する許可すら出す事が難しいという事になりかねません。
保守性の高い状態を保つ為にはRuby on Railsのレールを外す必要は有ると私は思いますし、長期的には保守性を保つ事こそが開発スピードを上げるという事にもなるのではないでしょうか。ドメイン駆動設計というと難しいイメージがあるとよく言われますし、エヴァンス本を読むと眠くなると言う話も良く聞きますが、重要なのは丁寧なオブジェクト指向とコミュニケーションとイテレーションです。
Rubyだってオブジェクト指向言語なんです。
静的型付け言語の良さが押される昨今の風潮も理解できますが、動的型付けだってしっかりチームでコミュニケーションを取って扱えば記述量を抑えた見通しのよいコードにできます。スピード感重視でRuby on Railsでサービスを作って、
気がついたらコードが無秩序に肥大化してメンテナンスに困っているという事例はそれなりに有るのではないでしょうか。それで幸せになれるなら、Ruby on Railsでドメイン駆動設計をやってもいいじゃないか。と思います。
- 投稿日:2019-12-21T15:59:36+09:00
マイグレーションファイルの作成、変更、削除まで
今回は、物件のファイルを作ることを想定します。
流れ
マイグレファイル作成→カラムすべて間違い→もう一度入れ直したい
1.マイグレファイルを作成する
$ rails g migration CreateHomes2.作成したマイグレファイルをエディターで編集する
create_homes.rbclass CreateHomes < ActiveRecord::Migration[5.2] def change create_table :homes do |t| t.string :"物件名" t.string :"賃料" t.string :"住所" t.text :"備考" end end end3.マイグレーションをターミナル上で入力
$ rails db:migrate4.マイグレファイルを削除するため、削除用のファイルを作る
rails g migration RemoveColumnsFromeHomes5.作ったファイルを編集する
remove_columns_from_homes.rbclass RemoveColumnsFromHomes < ActiveRecord::Migration[5.2] def change remove_column :Homes, :"物件名", :string remove_column :Homes, :"賃料", :text remove_column :Homes, :"住所", :text remove_column :Homes, :"備考", :text end end6.マイグレーションをターミナル上で入力
$ rails db:migrate※こうすることで、再び作り直すことができます。
rollbackでも削除は、可能ですが複数開発のときに何を編集したのか分からなくなるのでやめましょう。7. カラムを追加するためのマイグレファイル作成
$ rails g migration AddColumnsToHomes8.マイグレファイルを編集
add_columns_to_homes.rbclass AddColumnsToHomes < ActiveRecord::Migration[5.2] def change add_column :homes, :property, :string add_column :homes, :rent, :text add_column :homes, :address, :text add_column :homes, :age, :text add_column :homes, :remarks, :text end end何かを分からないことがありましたら、コメントを頂ければ1日以内に返信するように致します。
- 投稿日:2019-12-21T14:16:14+09:00
Railsをherokuでデプロイする方法
herokuでのデプロイは、AWSに比べてめちゃめちゃ簡単だったので、まとめました!
(githubへのpush、会員登録、クレジットカード登録など完了後)
herokuにログインする
デプロイしたいアプリに移動したら
$ heroku login何かキーを押してくださいと言われるので、適当にエンターキーを押す。
↓
herokuのページに推移する。
↓
loginを押してターミナルに戻る
↓
ログイン完了作ったアプリをherokuとリンクさせる
デプロイしたいアプリに移動したら
$ heroku create アプリ名アプリ名がURLに入るのでしっかり考えましょう
すでにそのアプリ名がherokuで使われている場合はできませんデータベースを作成する
※データベースが必要な場合のみ
ターミナル $ heroku addons:create cleardb:igniteこれでデータベース作成は完了しました。
mysql2の環境変数を指定
初期設定だとmysql2の設定ができていないのでそこを設定していきます。
現在のmysqlの設定を確認します。$ heroku config以下のような長い文字列が出てくるので、それに応じて環境変数を入力していきます。
CLEARDB_DATABASE_URL: mysql://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=trueheroku configで表示された文字列をもとに、以下のように入力していきます。
$ heroku config:add DB_USERNAME='<ユーザー名>' $ heroku config:add DB_PASSWORD='<パスワード>' $ heroku config:add DB_HOSTNAME='<ホスト名>' $ heroku config:add DB_NAME='<データベース名>' $ heroku config:add DB_PORT='3306' $ heroku config:add DATABASE_URL='mysql2://<ユーザー名>:<パスワード>@<ホスト名>/<データベース名>?reconnect=true'エラー対策の微調整
config/environments/production.rb#初期はfalseなので変更 config.assets.compile = true #config.assets.js_compressor = :uglifierを変更 config.assets.js_compressor = Uglifier.new(harmony: true)デプロイ
$ git push heroku masterデータベースのmigrate
$ heroku rake db:migrateアプリの立ち上げ
$ heroku openこれでブラウザに、作ったアプリが表示されます!
補足
万が一、エラーが出てしまった場合は
$ heroku logs -tで確認できるので、修正し、ローカルからgithubへのプッシュ。
そして再度$ git push heroku masterをすれば大丈夫です。
- 投稿日:2019-12-21T14:11:05+09:00
Ruby 2.7 rc1 の動作確認をdocker上で行う
以下のようにコマンドを実行する。
% docker run --name ruby_2_7_0_rc1 -it ruby:2.7.0-rc1 /bin/bash % irb
- 投稿日:2019-12-21T13:33:57+09:00
Minitest::Assersionsのassert系メソッドを全部試してみた
はじめに
この記事は Ruby Advent Calendar 2019 21日目の記事です。
今回はRubyのテストフレームワークであるMinitestのassert系メソッドを全部試してみました。
この記事がMinitestを使っている人に少しでも役立てばいいなと思います。
環境は以下のとおりです。Ruby
ruby 2.6.5p114 (2019-10-01 revision 67812) [x86_64-linux]Minitest
Minitest::VERSION #=> "5.13.0"それでは頑張っていきましょう。
assert(test, msg = nil)
test
が真(nil
、false
以外)の場合にアサーションが成功します。
msg
を指定するとアサーション失敗時に指定したメッセージを表示します。
(これ以降紹介するメソッドでもmsg
を引数に取れるメソッドがありますが、すべて同じ機能のため、ここでのみ紹介します。)成功例
assert true assert 0 assert :sym assert "hello"失敗例
assert falseExpected false to be truthy.失敗例(メッセージあり)
assert nil, "nilだよ"nilだよドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assertassert_empty(obj, msg = nil)
obj.empty?
が真の場合にアサーションが成功します。
empty?
を実装していないオブジェクトを渡した場合はメッセージが表示され、アサーションに失敗します。成功例
assert_empty({}) # {}がブロックと判定されてしまうため、()で括る assert_empty [] assert_empty ""失敗例
assert_empty({name: "TomoProg"}) assert_empty [1, 2, 3]Expected {:name=>"TomoProg"} to be empty. Expected [1, 2, 3] to be empty.失敗例(empty?を実装していない)
assert_empty 0Expected 0 (Integer) to respond to #empty?.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_emptyassert_equal(exp, act, msg = nil)
exp == act
が真の場合にアサーションが成功します。
Minitestのバージョン6からはexp
にnil
を指定するとアサーションが失敗するようになるようです。
そのため、exp
にnil
を指定した場合、assert_nil
を使うように警告が出ます。成功例
assert_equal 1, 1 assert_equal "sample", "sample"失敗例
assert_equal 2, 3Expected: 2 Actual: 3expにnilを指定した場合
assert_equal nil, nilDEPRECATED: Use assert_nil if expecting nil from test/sample_test.rb:5. This will fail in Minitest 6.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_equalassert_in_delta(exp, act, delta = 0.001, msg = nil)
exp
とact
の差分((exp - act).abs
)がdelta
以下の場合にアサーションに成功します。
浮動小数点数の比較の際に誤差を許容する場合に使うようです。成功例
assert_in_delta 0.001, 0.002 assert_in_delta 0.3, (0.1 + 0.2), 0.000001失敗例
assert_in_delta 1.256, 1.255Expected |1.256 - 1.255| (0.001000000000000112) to be <= 0.001.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_in_deltaassert_in_epsilon(exp, act, epsilon = 0.001, msg = nil)
誤差に関しての理解不足もあり、このメソッドに関してはよく分かりませんでした。
assert_in_delta
と同じように浮動小数点数の誤差を許容する場合に使うようですが、
assert_in_delta
が絶対誤差を許容するのに対し、こちらは相対誤差を許容するようです。参考になりそうなリンクを貼っておきます。
Minitestで絶対誤差と相対誤差を利用したテストを実行するドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_in_epsilonassert_includes(collection, obj, msg = nil)
collection.include?(obj)
が真の場合にアサーションに成功します。
include?(obj)
を実装していないオブジェクトを渡した場合はメッセージが表示され、アサーションに失敗します。成功例
assert_includes [1, 2, 3], 2 assert_includes({name: "TomoProg"}, :name) # Hash#include?(key)はハッシュにkeyがあれば真となる失敗例
assert_includes [1, 2, 3], 0Expected [1, 2, 3] to include 0.失敗例(include?(obj)を実装していない)
assert_includes 1, 1Expected 1 (Integer) to respond to #include?.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_includesassert_instance_of(cls, obj, msg = nil)
obj.instance_of?(cls)
が真の場合にアサーションに成功します。成功例
assert_instance_of Array, [1, 2, 3] assert_instance_of Integer, 1失敗例
assert_instance_of Integer, 1.2Expected 1.2 to be an instance of Integer, not Float.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_instance_ofassert_kind_of(cls, obj, msg = nil)
obj.kind_of?(cls)
が真の場合にアサーションに成功します。
assert_instance_of
に似ていますが、assert_kind_of
はサブクラスでもアサーションに成功します。成功例
class Base; end class SuperClass < Base; end assert_kind_of Base, SuperClass.new assert_kind_of SuperClass, SuperClass.new失敗例
assert_kind_of Integer, 1.2Expected 1.2 to be a kind of Integer, not Float.assert_instance_ofとの違い
class Base; end class SuperClass < Base; end assert_instance_of Base, SuperClass.new # assert_instance_ofではサブクラスを指定すると失敗する assert_kind_of Base, SuperClass.new # assert_kind_ofではサブクラスでも成功するドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_kind_ofassert_match(matcher, obj, msg = nil)
matcher =~ obj
が真の場合にアサーションに成功します。成功例
assert_match /rin/, "string" assert_match /\d{3}-\d{4}-\d{3}/, "123-4567-890"失敗例
assert_match /rig/, "string"Expected /rig/ to match # encoding: UTF-8 # valid: true "string".ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_matchassert_mock(mock)
mock.verify
が真の場合にアサーションに成功します。
Minitestではメソッド呼び出しが正しく行われているかを確認するために、モックを作ることができます。
verify
はモックに定義したメソッドがすべて呼び出されていればtrue
を返し、呼び出されていないメソッドがあればMockExpectationError
を返します。サンプルクラス
class Sample def initialize(obj) @obj = obj end def call @obj.method1 end end成功例
# モックにメソッドを定義する # (第1引数にメソッド名、第2引数に戻り値を指定する) mock = Minitest::Mock.new mock.expect "method1", true # Sample#callを呼び出す Sample.new(mock).call # モックに定義したすべてのメソッドが呼ばれたため、このアサーションは成功する assert_mock mock失敗例
# モックにメソッドを定義する # (第1引数にメソッド名、第2引数に戻り値を指定する) mock = Minitest::Mock.new mock.expect "method1", true mock.expect "method2", true # Sample#callを呼び出す Sample.new(mock).call # method2が呼ばれていないため、このアサーションは失敗する assert_mock mockMockExpectationError: expected method2() => trueドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_mockassert_nil(obj, msg = nil)
obj.nil?
が真の場合にアサーションに成功します。成功例
assert_nil nil失敗例
assert_nil 1Expected 1 to be nil.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_nilassert_operator(o1, op, o2 = UNDEFINED, msg = nil)
o1.__send__(op, o2)
が真の場合にアサーションに成功します。
2項演算子をテストするためのアサーションです。
o2
がUNDEFINED
の場合は後述するassert_predicate
が呼び出されます。
UNDEFINED
定数はMinitest::Assersions
で定義されており、Object.new
が代入されていました。成功例
assert_operator 5, :<=, 6失敗例
assert_operator 5, :<=, 4Expected 5 to be <= 4.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_operatorassert_output(stdout = nil, stderr = nil) { || ... }
標準出力が
stdout
、標準エラー出力がstderr
の場合にアサーションに成功します。
ブロックに標準出力、標準エラー出力を扱う処理を記述して使います。
正規表現を渡すことも可能です。
また、nil
を指定した場合はテストされません。成功例
# 標準出力、標準エラー出力をテストする assert_output("sample\n", "stderr_sample\n") do puts "sample" warn "stderr_sample" # Kernel.#warnは指定された文字列を標準エラー出力に出力する end # 正規表現も指定できる assert_output(/str/) do puts "string" end # nilを指定した場合はテストされない assert_output(nil, "stderr_sample\n") do warn "stderr_sample" end失敗例
assert_output("sample\n") do puts "saaaample" endIn stdout. --- expected +++ actual @@ -1,4 +1,2 @@ -# encoding: UTF-8 -# valid: true -"sample +"saaaample "ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_outputassert_path_exists(path, msg = nil)
File.exist?(path)
が真の場合にアサーションに成功します。ディレクトリ構成
sample/ test1.txt成功例
assert_path_exists "sample/test1.txt"失敗例
assert_path_exists "sample/test2.txt"Expected path 'sample/test2.txt' to exist.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_path_existsassert_predicate(o1, op, msg = nil)
o1.__send__(op)
が真の場合にアサーションに成功します。
テストしたいop
が複数ある場合は配列で持たせると綺麗に書けそうです。成功例
ops = %i(all? any?) ops.each { |op| assert_predicate [1, 2, 3], op }失敗例
assert_predicate [1, 2, 3], :empty?Expected [1, 2, 3] to be empty?.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_predicateassert_raises(*exp) { || ... }
*exp
に指定した例外がどれか一つでもブロック内で発生した場合にアサーションに成功します。
最後に文字列を指定するとメッセージとして扱われ、アサーションの失敗時に出力されます。
また、アサーションに成功した場合は例外クラスを返します。成功例
assert_raises(ZeroDivisionError) do 1 / 0 end # 例外が戻り値となる exp = assert_raises(ZeroDivisionError, ArgumentError) do [1, 2, 3].include? end assert_kind_of ArgumentError, exp # ブロック内で例外が複数起きる場合は先に起きた例外が戻り値となる exp = assert_raises(ZeroDivisionError, ArgumentError) do 1 / 0 [1, 2, 3].include? end assert_kind_of ZeroDivisionError, exp失敗例
assert_raises(ZeroDivisionError) do 1 / 1 endZeroDivisionError expected but nothing was raised.失敗例(メッセージ有り)
assert_raises(ZeroDivisionError, ArgumentError, "No Error") do [1, 2, 3].include?(1) endNo Error. [ZeroDivisionError, ArgumentError] expected but nothing was raised.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_raisesassert_respond_to(obj, meth, msg = nil)
obj.respond_to?(meth)
が真の場合にアサーションに成功します。成功例
assert_respond_to([1, 2, 3], :shift) assert_respond_to({name: "TomoProg"}, :dig)失敗例
assert_respond_to([1, 2, 3], :unknown_method)Expected [1, 2, 3] (Array) to respond to #unknown_method.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_respond_toassert_same(exp, act, msg = nil)
obj.equal?(act)
が真の場合にアサーションに成功します。
equal?
はobject_id
で比較するため、assert_equal
とは違う結果となる場合があります。成功例
assert_same 1, 1 assert_same :sym, :sym assert_same nil, nil失敗例
assert_same [1, 2, 3], [1, 2, 3]Expected [1, 2, 3] (oid=47357985755040) to be the same as [1, 2, 3] (oid=47357985755100).assert_equalと違う結果になる例
# 中身が同じ配列を準備する array1 = [1, 2, 3] array2 = [1, 2, 3] # assert_equalでは成功する # ==で比較するため、中身が同じであれば真となる assert_equal array1, array2 # assert_sameでは失敗する # equal?で比較するため、object_idが異なると偽となる assert_same array1, array2ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_sameassert_send(send_ary, m = nil)
このメソッドは非推奨のようです。
理由を探したところ、下記のサイトに記述がありました。
日々雑記 — Minitest 5.10.0でdeprecateになった機能assert_sendがdeprecateになった理由がちょっとわからなかったのでtwitterで聞いてみた所、単純に使っている所を見たことが無い為、との事でした。
send_ary
に第一要素にレシーバとなる任意のオブジェクト、第二要素にメソッド名、 第三要素にパラメータをそれぞれ指定した配列を指定し、指定したオブジェクトのメソッドが真であればアサーションに成功します。なぜかこのメソッドだけメッセージの引数名が
msg
ではなくm
でした。成功例
assert_send [[1, 2, 3], "include?", 1]DEPRECATED: assert_send.失敗例
assert_send [[1, 2, 3], :include?, 4]DEPRECATED: assert_send. Expected [1, 2, 3].include?(*[4]) to return true.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_sendassert_silent() { || ... }
標準出力、標準エラー出力に何も出力されない場合にアサーションに成功します。
assert_output("", "")
と書くのと同じ意味です。成功例
assert_silent { 1 + 1 }失敗例(標準出力)
assert_silent { puts "hello" }In stdout. --- expected +++ actual @@ -1,3 +1,2 @@ -# encoding: UTF-8 -# valid: true -"" +"hello +"失敗例(標準エラー出力)
assert_silent { warn "warning" }In stderr. --- expected +++ actual @@ -1,3 +1,2 @@ -# encoding: UTF-8 -# valid: true -"" +"warning +"ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_silentassert_throws(sym, msg = nil) { || ... }
指定したブロック内で
sym
に対してthrow
されるとアサーションに成功します。成功例
assert_throws(:exit) do [1, 2, 3].each do |a| [4, 5, 6].each do |b| throw :exit end end end失敗例
assert_throws(:exit) do [1, 2, 3].each do |a| [4, 5, 6].each do |b| throw :unknown end end endExpected :exit to have been thrown, not :unknown.ドキュメント
http://docs.seattlerb.org/minitest/Minitest/Assertions.html#method-i-assert_throwsまとめ
Minitest::Assersionsのassert系のメソッドをすべて試してみました。
Minitestを業務で使い始めてもうすぐ3年が経ちますが、こんなにあったとは知りませんでした。様々なassertメソッドを試してみて一番感じたのは
適切に使い分けることで失敗の原因がより分かりやすくなる
という点です。例えば、以下のアサーションは結果はどちらも失敗です。
しかし、後者のほうが何が原因で失敗したのかを分かりやすく伝えてくれます。assert [1, 2, 3].include?(0) # Expected false to be truthy. assert_includes [1, 2, 3], 0 # Expected [1, 2, 3] to include 0.適切なメソッドを使い分け、分かりやすいテストコードを書いていきましょう。
それではまた。
TomoProg
- 投稿日:2019-12-21T13:26:16+09:00
Railsでerbをhamlに変換する方法
- 投稿日:2019-12-21T13:11:55+09:00
Railsでアプリを作る簡単な流れ
一からアプリを立ち上げるとなると、意外と、何からしていいか分からない人もいると思うので、まとめました。
(アプリのダウンロードなど、環境構築などは完了していること)
アプリを作成する
保存したい場所に移動して
私はデータベース名は、mysqlでやってます。
指定しなければ、SQLiteというものが入ります。ターミナル $ rails new アプリ名 -d mysql(データベース名)gemのインストール
ターミナル $ bundle installコントローラーを作成する
投稿サイトを作る場合のコントローラーを作成します。
※コントローラー名は複数形にしましょうターミナル $ rails g controller posts(コントローラー名)posts.controller.rbclass PostsController < ApplicationController def index(アクション名) #空でいいので作成する end endデータベースを作成する
データベースが必要であれば作成します。
通常であれば2つのファイルが生成されます。ターミナル $ rails db:createモデルを作成する
データベースが必要な場合はモデルを作成します。
※モデル名は単数形にしましょうターミナル $ rails g model post(モデル名)モデルを作成すると、マイグレーションファイルも同時生成されます。
必要なカラムがあれば追加します。20190000_xx_xx.rbclass CreatePosts < ActiveRecord::Migration[5.2] def change create_table :posts do |t| #t.(型名) :(カラム名), (オプション) t.text :content, null: false t.timestamps end end endカラムの追加が完了したら
ターミナル $ rails db:migrateこれでデータベースの準備は完了です。
トップページのルーティングを作成する
routes.rb#root to: "コントロ-ラー名#アクション名" root to: "posts#index"トップページのviewを作成する
app/views/posts(コントローラー名)の中に index(アクション名).html.erbを作成index.html.erbHELLO WORLDrailsを起動する
ターミナル $ rails slocalhost:3000で起動します
- 投稿日:2019-12-21T12:12:03+09:00
railsでapplication.scssと同じディレクトリのscssファイルが読み込まれないときの対処法
背景と対象
usersコントローラを作成したときに生成された、app/assets/users.scssに記述したscssが反映されない。つまりはアセットパイプラインの仕組みが適用されていない状況になってしまっている状況。全部app/assets/application.scssに書けばcssの適用は出来ますがめっちゃ見ずらいのでやめた方がいいです。このような状況でつまっている方の役に立てればと思い書きました。ちなみにアセットパイプラインについては以下のページがおすすめです!
https://diveintocode.jp/blogs/Technology/AssetPipeline解決方法
とっても簡単です!
app/assets/application.scssのコメントアウトの最下部に*= require_selfとrequire_tree を以下のように追加してください。それぞれのコードの意味はコード内に書いてあります。ちなみにこれらの順番はどっちでもよく、require_selfを先に書けばapplication.scssが先に読み込まれます。後に書けばapplication.scss以外のscssファイルが先に読み込まれます。
app/assets/application.scss/* * This is a manifest file that'll be compiled into application.css, which will include all the files * listed below. * * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's * vendor/assets/stylesheets directory can be referenced here using a relative path. * * You're free to add application-wide styles to this file and they'll appear at the bottom of the * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS * files in this directory. Styles in this file should be added after the last require_* statement. * It is generally better to create a new file per style scope. * *= require_self まずはこのファイルを読み込むという記述 追加! *= require_tree app/assets以下のcssファイルを読み込んでいる 追加! */ }これでアセットパイプラインの仕組みを利用して分かりやすく、すっきりしたコードが書けます!
- 投稿日:2019-12-21T11:55:56+09:00
RubyVM::AbstractSyntaxTree を使って AASM の定義を抽出する
はじめに
本記事は STORES.jp Advent Calendar 2019 21日目の記事です。
Ruby2.6 から
RubyVM::AbstractSyntaxTree
という、 Ruby コードを抽象構文木に変換できる API を提供する module が使えるようになり、今年の RubyKaigi 2019 でも関連したセッションが行われていました。
これを使って何かできないものかと考えて思いついたのが、最近業務で利用している AASM という gem を使った、状態遷移の定義を取り出す...というものでした。そこでこの記事では、 RubyVM::AbstractSyntaxTree を利用して AASM を使った状態遷移定義を取得するコードについて書きます。1
そもそも AASM とは?
AASM はオブジェクトの状態遷移を定義・管理する機能を提供する gem です。
class にステートの一覧や状態遷移のためのイベントを DSL によって記述することで、各ステートに対応した定数や状態遷移を行うためのメソッドを生成してくれます。また、定義と異なる遷移を行おうとすると状態遷移に失敗するので、意図しない状態遷移を防ぐことができます。
具体的な使い方を簡単に解説すると、aasm
メソッドに渡すブロック内で
state
メソッドにステート名を渡すとその名前のステートを管理できるようになる
- ステート名に対応した定数が生成される
initial_state: true
オプションを付与することで、初期ステートが定義できるevent
メソッドにイベント名を渡すことで、ステートを遷移させるメソッドが生成される
- どのステートからどのステートへ遷移できる、というフローはブロック内の
transitions
によって定義する複雑になりがちな状態管理を宣言的に書くことで見通しを良くしつつ、秩序を保ちやすく実装できるので便利です。
task.rbclass Task < ApplicationRecord include AASM aasm column: :status do state :waiting, initial: true state :in_progress, :pending, :finished event :start do transitions from: :waiting, to: :in_progress transitions from: :pending, to: :in_progress end event :stop do transitions from: :in_progress, to: :pending end event :finish do transitions from: :in_progress, to: :finished end end end task = Task.create task.status # => "waiting" task.start! task.status # => "in_progress" task.start! # AASM::InvalidTransition: Event 'start' cannot transition from 'in_progress'.状態遷移の定義を抽出する
本題です。
先ほどの例のTask
クラスの状態遷移の定義を抽象構文木にしてごにょごにょし、初期ステート、ステートのリスト、イベントを取得できるようなオブジェクトを返すことができるようにしました。GitHub - ta-chibana/aasm_parser: Parsing AASM definition with RubyVM::AbstractSyntaxTree
以下のように利用します。
aasm = AasmParser.parse_file('./task.rb') aasm.initial_state # => :waiting aasm.state_names # => [:waiting, :in_progress, :pending, :finished] aasm.events.map(&:name) # => [:start, :stop, :finish] aasm.events.flat_map(&:transitions).map { |e| "#{e.from} => #{e.to}" } # => ["waiting => in_progress", "pending => in_progress", "in_progress => pending", "in_progress => finished"]内部では RubyVM::AbstractSyntaxTree を利用して
task.rb
を抽象構文木に変換し、目的の値を取得しています。
本記事では今回書いたコードのうち、初期ステートの取得を行う処理についてだけ解説します。初期ステートの取得は以下の手順で段階的に行ってみました。
task.rb
を抽象構文木に変換aasm
メソッドの呼び出し時に渡されている block を抽出- block から
state
メソッドの呼び出しを抽出state
メソッドの呼び出しからinitial_state: true
オプションが付いているものを抽出state
メソッドの呼び出しの第一引数を取得(= 初期ステート)手順1:
task.rb
を抽象構文木に変換状態遷移の定義は
aasm
メソッドに渡された block 内にあり、これを RubyVM::AbstractSyntaxTree を使って取り出せる状態にするためにtask.rb
を抽象構文木に変換します。変換は以下の1行で行うことができます。node = RubyVM::AbstractSyntaxTree.parse_file('./task.rb')結果として
RubyVM::AbstractSyntaxTree::Node
のインスタンスが返されます。RubyVM::AbstractSyntaxTree::Node
は#type
と#children
を持つので、それらを利用しつつ目的のノードを探していきます。手順2:
aasm
メソッドの呼び出し時に渡されている block を抽出
RubyVM::AbstractSyntaxTree::Node
インスタンスに対してpp node
と実行すると以下のように出力され、ノードの type やノード間の親子関係を把握するのに便利です。開発時はこの構造を見ながら目的の値を抽出する道筋を立てました。
以下は手順1で変換した結果をpp
に渡した結果です。(SCOPE@1:0-21:3 tbl: [] args: nil body: (CLASS@1:0-21:3 (COLON2@1:6-1:10 nil :Task) (CONST@1:13-1:30 :ApplicationRecord) (SCOPE@1:0-21:3 tbl: [] args: nil body: (BLOCK@2:2-20:5 (FCALL@2:2-2:14 :include (ARRAY@2:10-2:14 (CONST@2:10-2:14 :AASM) nil)) (ITER@4:2-20:5 (FCALL@4:2-4:22 :aasm (ARRAY@4:7-4:22 (HASH@4:7-4:22 (ARRAY@4:7-4:22 (LIT@4:7-4:14 :column) (LIT@4:15-4:22 :status) nil)) nil)) (SCOPE@4:23-20:5 tbl: [] args: nil body: (BLOCK@5:4-19:7 (FCALL@5:4-5:33 :state (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting) (HASH@5:20-5:33 (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial) (TRUE@5:29-5:33) nil)) nil)) (FCALL@6:4-6:43 :state (ARRAY@6:10-6:43 (LIT@6:10-6:22 :in_progress) (LIT@6:24-6:32 :pending) (LIT@6:34-6:43 :finished) nil)) (ITER@8:4-11:7 (FCALL@8:4-8:16 :event (ARRAY@8:10-8:16 (LIT@8:10-8:16 :start) nil)) (SCOPE@8:17-11:7 tbl: [] args: nil body: (BLOCK@9:6-10:50 (FCALL@9:6-9:50 :transitions (ARRAY@9:18-9:50 (HASH@9:18-9:50 (ARRAY@9:18-9:50 (LIT@9:18-9:23 :from) (LIT@9:24-9:32 :waiting) (LIT@9:34-9:37 :to) (LIT@9:38-9:50 :in_progress) nil)) nil)) (FCALL@10:6-10:50 :transitions (ARRAY@10:18-10:50 (HASH@10:18-10:50 (ARRAY@10:18-10:50 (LIT@10:18-10:23 :from) (LIT@10:24-10:32 :pending) (LIT@10:34-10:37 :to) (LIT@10:38-10:50 :in_progress) nil)) nil))))) (ITER@13:4-15:7 (FCALL@13:4-13:15 :event (ARRAY@13:10-13:15 (LIT@13:10-13:15 :stop) nil)) (SCOPE@13:16-15:7 tbl: [] args: nil body: (FCALL@14:6-14:50 :transitions (ARRAY@14:18-14:50 (HASH@14:18-14:50 (ARRAY@14:18-14:50 (LIT@14:18-14:23 :from) (LIT@14:24-14:36 :in_progress) (LIT@14:38-14:41 :to) (LIT@14:42-14:50 :pending) nil)) nil)))) (ITER@17:4-19:7 (FCALL@17:4-17:17 :event (ARRAY@17:10-17:17 (LIT@17:10-17:17 :finish) nil)) (SCOPE@17:18-19:7 tbl: [] args: nil body: (FCALL@18:6-18:51 :transitions (ARRAY@18:18-18:51 (HASH@18:18-18:51 (ARRAY@18:18-18:51 (LIT@18:18-18:23 :from) (LIT@18:24-18:36 :in_progress) (LIT@18:38-18:41 :to) (LIT@18:42-18:51 :finished) nil)) nil)))))))))))雑な見方としては、以下の部分を例にすると、1行目のノードは type が
FCALL
のノードで、後ろに続く:include (ARRAY@2:10-2:14 (CONST@2:10-2:14 :AASM) nil)
が子のノードとなります。それぞれRubyVM::AbstractSyntaxTree::Node#type
,RubyVM::AbstractSyntaxTree::Node#children
の戻り値に対応しています。(FCALL@2:2-2:14 :include (ARRAY@2:10-2:14 (CONST@2:10-2:14 :AASM) nil))これを図にすると以下のようになり、
include AASM
の行を表すノードであることがわかります。
(開発当初は、 type の意味については解説されたドキュメントが見つからなかったのであくまで推測だったのですが、社内で共有したところ--dump=parsetree_with_comment
オプション付きで実行すれば各ノードの説明が表示されるとの情報をいただけました。圧倒的感謝です)
--dump=parsetree_with_commentで実行$ ruby --dump=parsetree_with_comment -e 'include AASM' ########################################################### ## Do NOT use this node dump for any purpose other than ## ## debug and research. Compatibility is not guaranteed. ## ########################################################### # @ NODE_SCOPE (line: 1, location: (1,0)-(1,12)) # | # new scope # | # format: [nd_tbl]: local table, [nd_args]: arguments, [nd_body]: body # +- nd_tbl (local table): (empty) # +- nd_args (arguments): # | (null node) # +- nd_body (body): # @ NODE_FCALL (line: 1, location: (1,0)-(1,12))* # | # function call # | # format: [nd_mid]([nd_args]) # | # example: foo(1) # +- nd_mid (method id): :include # +- nd_args (arguments): # @ NODE_ARRAY (line: 1, location: (1,8)-(1,12)) # | # array constructor # | # format: [ [nd_head], [nd_next].. ] (length: [nd_alen]) # | # example: [1, 2, 3] # +- nd_alen (length): 1 # +- nd_head (element): # | @ NODE_CONST (line: 1, location: (1,8)-(1,12)) # | | # constant reference # | | # format: [nd_vid](constant) # | | # example: X # | +- nd_vid (constant): :AASM # +- nd_next (next element): # (null node)これを踏まえると、
aasm
メソッドのブロックに対応している部分は以下の部分が怪しそうな気配が感じられます。(ITER@4:2-20:5 (FCALL@4:2-4:22 :aasm (ARRAY@4:7-4:22 (HASH@4:7-4:22 (ARRAY@4:7-4:22 (LIT@4:7-4:14 :column) (LIT@4:15-4:22 :status) nil)) nil)) (SCOPE@4:23-20:5 tbl: [] args: nil body: (BLOCK@5:4-19:7 (FCALL@5:4-5:33 :state (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting) (HASH@5:20-5:33 (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial) (TRUE@5:29-5:33) nil)) nil))このノードを抜き出すために、まず以下の条件でノードを抽出し、その後
BLOCK
要素を取得する流れにしました。
type
がITER
(ブロック付きメソッド呼び出し?)- 子要素に
type
がFCALL
(関数呼び出しを表す?)の要素を持つ- ↑の
FCALL
要素の最初の子要素が:aasm
そのために書いたコードが以下で、
find_from_node
でノードを掘り下げつつaasm_node?
でtrue
を返すノードが見つかる、もしくはすべてのノードを走査し終えるまで処理を行います。lib/aasm_parser/aasm_node_finder.rbdef find_from_node(node) return nil unless node.respond_to?(:type) return node if aasm_node?(node) find_from_children(node.children) end def find_from_children(children) return nil if children.blank? head, *tail = children result = find_from_node(head) return result if result find_from_children(tail) end def aasm_node?(node) return false unless node&.type == :ITER first_child = node.children.first return false unless first_child.type == :FCALL first_child.children.first == :aasm end条件に該当するノードが見つかれば、取得できた
ITER
ノードの2つ目の子要素であるSCOPE
ノードからBLOCK
ノードを取得すればOKです。手順3:
aasm
の block からstate
メソッドの呼び出しを抽出手順2で取得した
BLOCK
ノードの構造を見てみます。(BLOCK@5:4-19:7 (FCALL@5:4-5:33 :state (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting) (HASH@5:20-5:33 (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial) (TRUE@5:29-5:33) nil)) nil)) (FCALL@6:4-6:43 :state (ARRAY@6:10-6:43 (LIT@6:10-6:22 :in_progress) (LIT@6:24-6:32 :pending) (LIT@6:34-6:43 :finished) nil))
BLOCK
ノード直下にstate
メソッドの呼び出しを表していそうなノードが見えるので、BLOCK
ノードの子要素から以下の条件で抽出すれば良さそうです。
type
がFCALL
- 最初の子要素が
:state
そのための実装が以下です(↑の条件で
select
しているだけ)。lib/aasm_parser/aasm/block.rbdef state_nodes ast_block.children.select do |node| node.type == :FCALL && node.children.first == :state end end手順4:
state
メソッドの呼び出しのリストからinitial_state: true
オプションが付いているものを抽出手順3で取得できた
state
メソッドの呼び出しは、元のコードのtask.rb
でいうと以下の部分です。state :waiting, initial: true state :in_progress, :pending, :finishedまた、取得できた
state
メソッド呼び出しのノードは以下ような構造になっています。(FCALL@5:4-5:33 :state (ARRAY@5:10-5:33 (LIT@5:10-5:18 :waiting) (HASH@5:20-5:33 (ARRAY@5:20-5:33 (LIT@5:20-5:28 :initial) (TRUE@5:29-5:33) nil)) nil)) (FCALL@6:4-6:43 :state (ARRAY@6:10-6:43 (LIT@6:10-6:22 :in_progress) (LIT@6:24-6:32 :pending) (LIT@6:34-6:43 :finished) nil))どちらの
FCALL
ノードも子要素としてARRAY
ノードを持っており、その中には:waiting
や:in_progress
などの Symbol が含まれていることから、引数を表していそうです。
また、1つ目のstate
メソッドの呼び出しではオプション引数としてinitial: true
を渡していますが、ノードの構造では(LIT@5:10-5:18 :waiting)
の他にHASH
ノードが含まれていたり、HASH
ノードの孫要素に(LIT@5:20-5:28 :initial)
や(TRUE@5:29-5:33)
があり、これらが含まれていることを条件にinitial_state: true
であるかどうかが判断できそうです。以上をまとめると
initial_state
オプションが付いている要素かどうかを調べるためには、
FCALL
ノードの子要素であるARRAY
ノードの子要素にHASH
ノードが存在する(= オプション引数が存在する)- ↑の
HASH
ノードの子要素であるARRAY
要素に、子要素として:initial
を持つLIT
ノードが存在する- ↑の
LIT
ノードの次の要素がTRUE
ノードであるという条件で判断します。これらの条件を確認するコードが以下の
initial_state?
です。lib/aasm_parser/aasm/state.rbdef initial_state? return false if options.blank? lit_node_index = options.find_index do |e| e.type == :LIT && e.children[0] == :initial end return false if lit_node_index.nil? options[lit_node_index + 1].type == :TRUE end # state メソッドに渡された引数のノードを抽出する def state_arguments # @state_node に、 FCALL ノードの # RubyVM::AbstractSyntaxTree::Node インスタンスが格納されている @state_node .children .last # state メソッドに渡された引数を表す ARRAY ノード .children .compact end def options @options ||= begin hash_node = state_arguments.find { |e| e.type == :HASH } return [] if hash_node.nil? hash_node .children .first # オプション引数の内容を表す ARRAY ノード .children end end # state の名前を取得する # 手順5で名前を取得するときに利用します def names state_arguments .select { |e| e.type == :LIT } .flat_map(&:children) end
#names
を呼び出すと state の名前を抽出することができるようにしました(手順5で使います)。手順5:
initial_state: true
オプション付きのstate
メソッド呼び出しの第一引数を取得手順3で抽出した
state
メソッドの呼び出しを表すノードの中から、initial_state: true
オプションが付いているかを判定するには手順4のinitial_state?
を実行すれば良いです。
また、その名前を得るためには手順4のコードにチラッと出てきたnames
を利用します。state
メソッドには複数のステート名が渡せるので、その都合上複数の名前が返るようになっていますが、initial_state
オプションが付いている場合はステートは一つしか渡されていないはず?なので最初の要素を取得すれば初期ステート名を得ることができます。lib/aasm_parser/aasm/block.rbdef initial_state state = states.find(&:initial_state?) return nil if state.nil? state.names.first end def states state_nodes.map { |node| State.new(node) } endおわりに
無事に目的の値を取得することができた一方、以下のようなあらゆる問題が存在します。
- AASM のあらゆるオプションに対応できていない(
:if
,:unless
やコールバックが設定できます)- AASM に破壊的な変更があるとほぼ作り直しのような状態になる
- どうしても複雑な処理になってしまうので修正が容易でない
- AST生成時に対象のコードは実行されないので、実行しないと目的の値が取れない(ステートが Symbol でなく定数・変数で記述されているなど)ケースだと詰む
- などなど...
これらの問題を解決するにはとても良い方法があって、 RubyVM::AbstractSyntaxTree を使わずに AASM を使う という方法が有効です。つまりこうです。
Task.aasm.states.map(&:name) # => [:waiting, :in_progress, :pending, :finished] Task.aasm.events.map(&:name) # => [:start, :stop, :finish] Task.aasm.initial_state # => :waiting Task.aasm.events.flat_map(&:transitions).map { |e| "#{e.from} => #{e.to}" } # => ["waiting => in_progress", "pending => in_progress", "in_progress => pending", "in_progress => finished"]これですべての問題が解決しそうです。
結果的に作らなくても良いものを生んでしまったわけですが、 RubyVM::AbstractSyntaxTree を使ってみるという目的は達せられたのでヨシとします。感想としては、取りたいノードの構造がわかっているなら段階的に抽出するのではなく一発で抽出するようにできたのではないかなあとか、この知識を使える現実的な場面(なさそう)に遭遇したらまた挑戦してみたいなあと思うなどしました。ただ、普段書かないようなコードなので楽しく取り組めました。
以上、 RubyVM::AbstractSyntaxTree を使ってみたくてコードを書いた記録でした。
参考
おや?と思った方、お察しください。釘に見えてしまったのです。 ↩
- 投稿日:2019-12-21T11:40:48+09:00
文言最初の一歩
0.はじめに
初投稿となります
まずは「漢文風プログラミング言語」
文言 を紹介させていただきます漢文風プログラミング言語「文言」の基本文法を読み解いてみた - @GandT
導入
GitHub よりソース取得
npm install ではないらしい
実行
npm startではないらしい
当方windowsユーザーです
実行方法わからないので
‘’’
cd site
ruby -run -e httpd . -p 3000
‘’’ブラウザより
http://localhost:3000/ide.html
でエディター起動サンプルアプリ実行
おわりに
わからなすぎで、実行だけならオンラインからも出来たのですが、縦書きレンダリングとruby出力を夢見て頑張ってみます
- 投稿日:2019-12-21T11:20:13+09:00
#Stripe API + #Ruby gem で定期支払いの請求を作成する例 ( 顧客作成・デフォルトの支払い方法を登録・プランの作成・税率情報の登録など含む )
Code
No need to payment intent, payment source attache.
require 'stripe' Stripe.api_key = ENV['STRIPE_SECRET_KEY'] product = Stripe::Product.create(name: 'Gold plan') plan = Stripe::Plan.create(interval: 'month', currency: 'jpy', amount: 1000, product: product.id) tax_rate = Stripe::TaxRate.create(display_name: 'Tax Rate', percentage: 10.0, inclusive: false) customer = Stripe::Customer.create payment_method = Stripe::PaymentMethod.create(type: 'card', card: { number: '4242424242424242', exp_year: 2030, exp_month: 01}) customer_payment_method = Stripe::PaymentMethod.attach(payment_method.id, customer: customer.id) subscription = Stripe::Subscription::create( customer: customer.id, default_payment_method: customer_payment_method.id, items: [{ plan: plan.id }], default_tax_rates: [tax_rate] ) customer = Stripe::Customer.retrieve(customer.id) subscription_invoice = Stripe::Invoice.retrieve(customer.subscriptions.data[0].latest_invoice) p subscription_invoiceResult
$ STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxxxxxxxxxxxx pry ~/tmp/stripe.rb #<Stripe::Invoice:0x3fc7e35d01ac id=in_1FrZnyCmti5jpytUqUqqcjVX> JSON: { "id": "in_1FrZnyCmti5jpytUqUqqcjVX", "object": "invoice", "account_country": "JP", "account_name": "yumainaura", "amount_due": 1100, "amount_paid": 1100, "amount_remaining": 0, "application_fee_amount": null, "attempt_count": 1, "attempted": true, "auto_advance": false, "billing_reason": "subscription_create", "charge": "ch_1FrZnyCmti5jpytUQ9PMH3z1", "collection_method": "charge_automatically", "created": 1576803442, "currency": "jpy", "custom_fields": null, "customer": "cus_GOMXfIqzJKSF12", "customer_address": null, "customer_email": null, "customer_name": null, "customer_phone": null, "customer_shipping": null, "customer_tax_exempt": "none", "customer_tax_ids": [ ], "default_payment_method": null, "default_source": null, "default_tax_rates": [ {"id":"txr_1FrZnwCmti5jpytUlv7jF2HT","object":"tax_rate","active":true,"created":1576803440,"description":null,"display_name":"Tax Rate","inclusive":false,"jurisdiction":null,"livemode":false,"metadata":{},"percentage":10.0} ], "description": null, "discount": null, "due_date": null, "ending_balance": 0, "footer": null, "hosted_invoice_url": "https://pay.stripe.com/invoice/invst_uCN38P2xjLu5iSsXHOU6LCozKT", "invoice_pdf": "https://pay.stripe.com/invoice/invst_uCN38P2xjLu5iSsXHOU6LCozKT/pdf", "lines": {"object":"list","data":[{"id":"sli_fe1f1f1bb0aa55","object":"line_item","amount":1000,"currency":"jpy","description":"1 × Gold plan (at ¥1,000 / month)","discountable":true,"livemode":false,"metadata":{},"period":{"end":1579481842,"start":1576803442},"plan":{"id":"plan_GOMXJzgxs31NbS","object":"plan","active":true,"aggregate_usage":null,"amount":1000,"amount_decimal":"1000","billing_scheme":"per_unit","created":1576803440,"currency":"jpy","interval":"month","interval_count":1,"livemode":false,"metadata":{},"nickname":null,"product":"prod_GOMWB1WfirJ9E5","tiers":null,"tiers_mode":null,"transform_usage":null,"trial_period_days":null,"usage_type":"licensed"},"proration":false,"quantity":1,"subscription":"sub_GOMXwLV5Qyelwu","subscription_item":"si_GOMXiUNouc5U5J","tax_amounts":[{"amount":100,"inclusive":false,"tax_rate":"txr_1FrZnwCmti5jpytUlv7jF2HT"}],"tax_rates":[],"type":"subscription","unique_id":"il_1FrZnyCmti5jpytUt0gmfTrU"}],"has_more":false,"total_count":1,"url":"/v1/invoices/in_1FrZnyCmti5jpytUqUqqcjVX/lines"}, "livemode": false, "metadata": {}, "next_payment_attempt": null, "number": "4506ADA5-0001", "paid": true, "payment_intent": "pi_1FrZnyCmti5jpytUPbn6rlhH", "period_end": 1576803442, "period_start": 1576803442, "post_payment_credit_notes_amount": 0, "pre_payment_credit_notes_amount": 0, "receipt_number": null, "starting_balance": 0, "statement_descriptor": null, "status": "paid", "status_transitions": {"finalized_at":1576803442,"marked_uncollectible_at":null,"paid_at":1576803443,"voided_at":null}, "subscription": "sub_GOMXwLV5Qyelwu", "subtotal": 1000, "tax": 100, "tax_percent": 10.0, "total": 1100, "total_tax_amounts": [ {"amount":100,"inclusive":false,"tax_rate":"txr_1FrZnwCmti5jpytUlv7jF2HT"} ], "webhooks_delivered_at": null }Original by Github issue
- 投稿日:2019-12-21T11:06:58+09:00
ずら〜っと、Railsでデータベースを確認する方法一覧
データベースクライアントの起動
$ rails dbconsoleこうなる
sqlite>テーブル一覧の見方
sqlite> .tables指定したテーブルのカラムの確認方法(スキーマの確認)
.schema テーブル名
- 投稿日:2019-12-21T09:17:54+09:00
WindowsにRails環境を導入してみる
WindowsにRailsを導入しようとしたらめちゃくちゃ手こずったので、記録しておこうと思って書きました
初めに
ここからRubyをダウンロードする、Devkitがついてるやつがいいらしい
インストールが完了したらコマンドプロンプトを開いてgem install rails
を実行
問題なくインストールできたようなので次のステップへ
rails new hoge
を実行するとガーっとファイルが作られていって安心していたら変なのが出るCould not find gem 'sqlite3 (~> 1.4) x64-mingw32' in any of the gem sources listed in your Gemfile. Run `bundle install` to install missing gems.意訳:ふえぇ、SQLite3 v1.4が見つからないよぉ
Railsを始めてsqlite3まわりのエラーで躓いている人たちへを見るとSQLite3でエラーが頻発して阿鼻叫喚としてるとか書いてある、怖いGemfileを編集する
# Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.4'2行目のSQLite3のバージョンを下げてみる
# Use sqlite3 as the database for Active Record gem 'sqlite3', '~> 1.3.6'ディレクトリ移動して
bundle install
hoge>bundle install Bundle complete! 14 Gemfile dependencies, 70 gems now installed. Use `bundle info [gemname]` to see where a bundled gem is installed.勝ったなガハハ、サーバ立ててlocalhost:3000にアクセスしてやろう。
hoge>rails server C:/Ruby26-x64/lib/ruby/gems/2.6.0/gems/activesupport-6.0.2.1/lib/active_support/dependencies.rb:325:in `require': cannot load such file -- sqlite3/sqlite3_native (LoadError)勝ってなかった。
SQLite3くんが悪さをしているように見えるので消してやりましょうhoge>gem uninstall sqlite3 Successfully uninstalled sqlite3-1.3.13-x64-mingw32
gem install sqlite3 --platform ruby
でSQLite3を再インストールhoge>gem install sqlite3 --platform ruby Temporarily enhancing PATH for MSYS/MINGW... Installing required msys2 packages: mingw-w64-x86_64-sqlite3 Building native extensions. This could take a while... Successfully installed sqlite3-1.4.2 Parsing documentation for sqlite3-1.4.2 Installing ri documentation for sqlite3-1.4.2 Done installing documentation for sqlite3 after 1 seconds 1 gem installed通った。
なおサーバは立たない、なんでや。Railsのバージョンを下げる
rails 環境構築~ヘルスケアwebサービスを自分で作る医者の日記~と【Windows】RubyとRuby on Raisの環境構築でSQLite3まわりで苦労したのコメント欄にRailsのバージョンを下げたらいけたと書いてあったので早速
gem i -v 5.2.3 rails
を実行
終わったぽいのでrails new hoge
でプロジェクトを作るとすんなり通った。
これはいけたのでは?
恐る恐るrails server
実行対戦ありがとうございました。
原因
詳しいことは不明ですが、Rails6.0.2.1とSQLite3 v1.4の相性がよくなかったのかなと思います
最終的な環境
Ruby 2.6.5p114
Gem 3.0.3
Rails 5.2.3
SQLite3 1.4.2最後に
Qiitaで記事を書くのは初めてなので、変なところがありましたらご指摘いただけると幸いです。
- 投稿日:2019-12-21T08:22:12+09:00
Rubyを使って予約システムを使いやすくハックしよう
はじめに
こんにちは。GMOアドマーケティングのyoshishinです。
皆さんは普段、よく使うWEBサービスなどありますか?
自分は最近、予約システムをよく使うようになったのですが、毎回同じ操作をしており、効率的ではないと感じています。
多くの人が使いやすいように作られているとは思うのですが、人によって感じ方は様々で、手順が決まっているのであればいつもの設定をボタン一つでできたら便利ですし、中には自分専用の画面がほしかったりするかもしれません。
今回はこのような予約システムを使いやすくするためのハックを考えていきたいと思います。目標
予約システムから必要な情報を取り出し、使いやすいJSON形式に整形して任意のレスポンスを返すことを目標とします。
JSON形式であれば、データを元に画面を作ったり、思い通りに操作することが容易となります。開発環境
まずは開発環境を整えます。
今回メインで使うライブラリはnokogiriです。
HTMLという素材からノコギリで必要なものを切り出していくイメージですね。
Rubyが使える状態であれば、以下のコマンドでインストールできます。gem install nokogiriRailsを使う場合はこのgemが含まれているため、bundle installすることで追加されます。
公式サイト
http://www.nokogiri.org/以下、公式サイトのサンプルです。
require 'nokogiri' require 'open-uri' # HTMLドキュメントを取得し、解析 doc = Nokogiri::HTML(open('https://nokogiri.org/tutorials/installing_nokogiri.html')) puts "### Search for nodes by css" doc.css('nav ul.menu li a', 'article h2').each do |link| puts link.content end puts "### Search for nodes by xpath" doc.xpath('//nav//ul//li/a', '//article//h2').each do |link| puts link.content end puts "### Or mix and match." doc.search('nav ul.menu li a', '//article//h2').each do |link| puts link.content endxpass,cssを使い、レスポンスを確認しながら目的のデータを取得します
解析
次に解析です。
Chromeであれば、画面を右クリックして、検証を選択することで、HTML構造を確認できます。
HTMLの構造から特徴をまとめていきます。普段使っているWEBサイトであれば、どういう構造で、どんなデータが欲しいのかはすぐに想像できると思いますし、何らかの不満を感じているのであれば、こうすれば使いやすくなるという改善案もあると思います。
対象システムでは日にちごとにいくつかの時間があり、時間毎に予約枠を持っているという特徴がありました。
設計
専用画面を作り、登録した設定情報を元に予約できるようになれば便利になると思うので、JSON形式のレスポンスを返し、そこから画面を表示できるようにしていきます。
解析時の特徴から、以下のようなJSON形式にすると使いやすそうなので、データを整形していきます。[{"day"=>"12/23", "times"=>[ {"time"=>"12:00", "stock"=>0}, {"time"=>"12:15", "stock"=>0}, {"time"=>"12:30", "stock"=>0}, {"time"=>"12:45", "stock"=>0},... ]データ取得
理想の形が定まっているのであれば、あとはnokogiriでひたすらデータを切り出し、設計したJSON形式にデータを格納していきます。
作ったデータを呼び出すメソッドを用意すればJSON API化は完了です。対象サイトをスクレイピングして整形したJSONを返し、それをRailsの画面で表示してみました。
まとめ
以上の手順でJSON API化することができました。
ここまでできれば後は使いやすい画面を作るなり、設定に従った自動予約、チャットシステムとの連携など、思い通りに操作することができると思います。
予約システムだけでなく、他の仕組みにも応用できると思うので、毎日多くの時間を費やしているシステムがあれば、使いやすくハックすることも検討してみてはいかがでしょうか?注意点として、実際に使うタイミングで自動予約するのであれば手動でアクセスするのと比べて負荷は変わりませんが、バッチを使った予約の空き枠通知など、定期的にアクセスする実装はサーバーの負荷になってしまうため、控えた方がよいと思います。
以上です。
明日は、M.Nさんによる「TAXELの回遊率を少しだけ最適化した話」です。
引き続き、GMOアドマーケティング Advent Calendar 2019 をお楽しみください!
- 投稿日:2019-12-21T02:04:36+09:00
railsアセットプリコンパイルまとめ
- javascript
- scss
javascript
プロジェクトフォルダのapp/assets/javascript以下のファイルをすべて読み込む。
railsの初期ファイルのapplication.jsは何かというと、assets/javascript以下のファイルをすべて読み込みまとめたものにあたる。勝手にファイルをまとめてくれるので、application.jsに追加でrequireしなくてもよいのである。
しかしgemなどで追加した機能を読み込みたい場合は、application.jsにrequireする必要がある。これはどこで使用するのかというともちろんviewである。だいたいは、layouts/application.html.erbに書かれている。<%= javascript_include_tag 'application', オプション %> のように記述すればよい。'application'の部分は、他のファイルでもよいがPATHを通す必要がある。
scss
railsでbootstrapをgemで追加する場合はcssではなく拡張子がscssのファイルになる。ここでのcssとscssの違いは記述の仕方である。cssは、javascriptと同じように//=requireのよういに記述する必要がある。また//=require_treeがあるのだがこれは、javascriptと同じですべてのファイルをまとめて読み込むというのを意味している。cssにもapplication.cssというファイルがあり、javascriptと使い方は同じである。
それに比べてscssは、@importのようにして機能をインポートする必要がある。そしてcssと違ってファイルを自動的に読み込んでくれないため、一つ一つインポートする必要がある。
例えば、A.scssというファイルとB.scssというファイルがあるとする。そしてjavascriptと同じで、application.scssというファイルがある。scssではA.scssとB.scssの両方を適用したいのならapplication.scssに、@import 'A.scss'; @import 'B.scss';と記述する必要がある。bootstarpのテンプレートをrailsに適用するときの注意
以上の知識より、ダウンロードしたテンプレートファイルのcss部分とjs部分をそれぞれ、assets配下に配置する。
テンプレートのファイルには、bootstrap.min.cssなどのファイルが含まれているが、これはbootstrapの機能を使えるようにしますよというファイルである。なのでrails側ですでにgemでbootstrapを追加している場合は、assets配下に配置してはならない。
jquery.min.jsなどのファイルもあるがこれも同様である。ダウンロードしたテンプレートのファイルが機能を提供するものなのか、それはrails側ですでに追加されているのかを判断してからssets配下に置くとよい
- 投稿日:2019-12-21T01:19:40+09:00
Rails6 マイグレーションファイルの記載ミスでマイグレートしてしまったらrollbackを使おう
目的
- マイグレーションファイルの記載を間違え、マイグレートしてしまった時の対処法をまとめる
筆者がやらかしたこと
- マイグレーションファイルの記載をミスる。
- その状態でマイグレートしてしまう。
- データベースを確認したところ期待した方になっていない。
- 現在のマイグレーションファイルを削除する。
- 再度、マイグレーションファイルを作成し正式な記載にをした。
- マイグレートを実行した。
- データベースが期待したものになっていた。
間違えてマイグレートをしてしまった時はrollebaskをしよう
マイグレート後にDBに異変を感じたらすぐに下記のコマンドを実行してマイグレート前の状態に戻す。
$ rails db:rolleback
そのあとでマイグレーションファイルの記載を確認、修正を行い再度、マイグレートを行う。
- 投稿日:2019-12-21T00:46:32+09:00
RubyをFlightRecoderを使ってブラックボックス分析する
はじめに
性能分析や障害分析で詳細なログ情報が取得たいときは良くあります。
私は普段はRuby製のバッチやWebアプリを本番で動かさないのであまり気にした事が無かったのですが、パッと思い付かなかったのでJFR(JDK Flight Recorder)をRubyに適用出来るかを試してみました。
JFRというブラックボックス分析向けのJVMの仕組みで低遅延のプロファイラを常時本番環境のアプリに適用してCPUやメモリ、GCログはもちろんスレッドダンプやI/O、カスタムイベントを1ファイルに集約保存する仕組みです。
これによって、障害時の原因の特定をスピーディーに実施できます。今回はTruffleRubyを使う事でJFRを有効にしてみました。
TruffleRubyのインストール
まずはインストールです。rbenvとかで入れたくなるのですが、その場合JVM版が入らないのでGraalVMからインストールを行います。
こちらからCommunity版をインストールするか、SDKMANを使ってインストールします。下記はSDKMANのケースです。
# GraalVMのインストール $ sdk install java 19.3.0.r11-grl $ java -version openjdk version "13.0.1-BellSoft" 2019-10-15 OpenJDK Runtime Environment (build 13.0.1-BellSoft+9) OpenJDK 64-Bit Server VM (build 13.0.1-BellSoft+9, mixed mode, sharing)続いてTruffle Rubyのインストールです。ここからの手順はGraalVMのインストール方法に依存せず共通です。
$ gu install ruby $ ruby --jvm --version truffleruby 19.3.0, like ruby 2.6.2, GraalVM CE JVM [x86_64-darwin]インストールはこれで完了です。
検証アプリを作成
動作テスト用のアプリをSinatraで作成します。
$ bundler init $ echo 'gem "sinatra"' >> Gemfile $ bundler install下記のように
app.rb
を書きます。require 'sinatra' get '/' do 'Hello world!' end⏎JFRの取得
アプリを起動させてJFRを取得します。以下のようなJVMオプションをRubyに渡します。
--vm.XX:...
のようにする事でJVMオプションを指定できます。$ ruby --jvm --vm.XX:StartFlightRecording=settings=profile,filename=myapp.jfr app.rb下記のメッセージが出ればJFRが有効になっています。
Started recording 1. No limit specified, using maxsize=250MB as default. Use jcmd 53009 JFR.dump name=1 to copy recording data to file.プロセス終了時にmyapp.jfrという名前で作成されます。
VisualVMで見てみる
JFRはバイナリなのでJDK Mission Controlをインストールして解析したり、
jfrコマンド
でJSONに変換したりして分析します。
GraalVMにはjvisualvmが同梱されているのでこちらを利用することも可能です。VisualVMでJFRを開くと以下のような情報が取得できます。他にもGCログとかヒストグラムとかスレッドロックとか色々見れますが割愛。VisualVMのJFRサポートはまだ絶賛開発中なので、本格的に見るならJMCの方がUIもこなれていてオススメです。
カスタムイベントの追加
Flight Recorderにはカスタムタグと言って任意のイベントを追加する仕組みがあります。これを使うことでログを吐くのと同じ感覚でJFRファイルの中に情報を組み込めます。
以下のように
jdk.jfr.Event
クラスを継承したコードをJavaで書きます。java/src/profile/HttpRequestEvent.javapackage profile; import jdk.jfr.Category; import jdk.jfr.Event; import jdk.jfr.Label; /** * * @author koduki */ @Category({ "Application Profile" }) @Label("HTTP Request") public class HttpRequestEvent extends Event { @Label("Method") public String method; @Label("URL") public String url; }続いてRubyでJavaコードを呼び出します。
require 'sinatra' Java.import 'profile.HttpRequestEvent' get '/' do event = HttpRequestEvent.new event.begin r = 'Hello world!' event.end event.setUrl("/") event.setMethod("get") event.commit r end
Java.import
でJavaのコードをrequireします。JRubyとTruffleRubyで構文が少し違うのでご注意ください。
begin
/end
で囲んだ範囲の処理をduration
として自動で記録します。その後、先ほどJavaのクラスで指定した任意のプロパティに値を入れる事ができます。続いて以下のコマンドで実行します。
$ javac java/src/profile/HttpRequestEvent.java -d java/classes $ ruby --jvm --vm.classpath=./java/classes/ --vm.XX:StartFlightRecording=settings=profile,disk=true,filename=myapp.jfr app.rb新しいオプションとして
--vm.classpath
を足しています。こちらで作ったJavaのカスタムイベントをクラスパスに通します。準備はできたのでabコマンドで適当にリクエストを投げてみます。
$ ab -n 5000 -c 10 "http://localhost:4567/"こちらrubyのプロセスを終了してmyapp.jfrを見てみると以下のようにカスタムイベントが表示されます。
今回は単純にリクエストを入れましたが障害時に便利な情報を入れておくと良いでしょう。ちなみに閾値の設定とかもできます。
ちなみにJMC以外で例えばMetabaseとかで分析したい場合は2019-12-09
JFRをBigQuery/Metabaseのオレオレダッシュボードで可視化するをご参照ください。まとめ
Rubyの本番時の障害分析はおそらくNewRelicなどのAPMを中心に行われてるのでは無いかと思われます。
ただスレッドロックとか細かい情報も見たいことは本番運用してたらありそうなので、JFRはAPMよりも障害分析に特化した機能が多くRubyでも使おうと思えば使えるのは助かります。
今回はビジネスロジックの中に直書きしましたが、Rubyなのでメソッド呼び出しをフックして自動で書き込むとかFW側にJFRのロジックを入れれば、グッと使いやすくなると思います。
TruffleRubyはパフォーマンスが注目されがちですが、こうしたJavaのエコシステムと連携できるのも魅力の一つかと考えています。
ただ、スタックトレースがRuby側まで追いきれないようなので、この辺はやはりRubyネイティブのものがあると良いですね。
それではHappy Hacking!
参考