Azuki'sLog

都内で働くエンジニアのブログ!RubyとかPHPとか

v-forでkey属性を指定する意味

vueのコードを見ていて v-for などで key 属性が何かを忘れて調べるをすること3回目。
覚えるための記事ということで。。自分用のメモ

key属性の仕様

まず仕様はこちら。

v3.ja.vuejs.org

キーがない場合、Vue は要素の移動を最小限にするアルゴリズムを使い、できる限り同じ種類の要素をその場でパッチや再利用しようとします。 キーがある場合、キーの変更順序に基づいて要素の順番を変更して、存在しなくなったキーを持つ要素は常に削除や破棄されます。

key属性なしで動かしてみる

See the Pen key属性無しv-for by azukineko (@azukineko) on CodePen.

この色のテキストボックスに色を入力する

f:id:nyantech0207:20210827002216p:plain:w400

その後に3番のレモンを削除すると、項目がズレてしまう

f:id:nyantech0207:20210827002342p:plain:w400

これはkey属性を指定しない場合にできる限り同じ種類の要素を再利用しようとする仕様のため。

key属性ありで動かしてみる

See the Pen by azukineko (@azukineko) on CodePen.

こちらで同じ動作をさせるとkey属性を指定したtr要素ごと削除などが適用される。

Railsでページネーションを自作する

Railsでページネーションをgemを使わないで実装した事について書きます。
「ページネーションを使える様にしましょう」という事は良くあり、、というかWEB開発では必ずある。
今まではRailsでは kaminari とか will_paginate を使ってきて、便利だし個人的には使えば良いと思っている。

今回は特殊な事情から自作する事になった。

何故自作したか

  1. アプリの構成が特殊で、kaminariを使おうとしたけど何故かkaminariが正常にロードされなかった。
  2. 1. の問題を深掘りしたい気持ちもあったものの、作ってしまった方が早いとなった。
  3. カスタマイズしやすいし、アプリの影響範囲もわかるので自作の良い点も多い。

ページネーションの処理をmodelによせたかった

初めはコントローラ側にページネーションの処理を書いていたのですが、
コントローラにこんな感じのコードが複数発生してきて、共通化できる様にしたかった。

@page = params[:page].to_i < 1 ? 1 : params[:page].to_i
@offset = PER_PAGE * (@page - 1)
relation = Huga.where(coditions)
@hugas = relation.offset(@offset).limit(PER_PAGE).order(id: :desc)
@total_count = relation.count

modelに移した

modelに移す上で、kaminariのコード を見てみると、ActiveRecord::QueryMethods#extending というメソッドが使われている。
ActiveRecord::Relation にメソッドを追加したり拡張ができるみたいで便利!
ドキュメントの例もページネーションだし、これ使ってみようという気持ちになる。
という事で実装してみた所、以下の感じになった。簡単・・・。

module Hoge
  module Concerns
    module Paginate
      extend ActiveSupport::Concern

      included do
        scope :page, ->(page, per_page) do
          all.extending(Hoge::Concerns::Paginate::Extends).paginate(page, per_page)
        end
      end

      module Extends
        def paginate(page, per_page)
          @current_page = page.to_i < 1 ? 1 : page.to_i
          offset = per_page * (@current_page - 1)
          offset(offset).limit(per_page)
        end

        def current_page
          @current_page
        end

        def limit_value
          self.values[:limit]
        end

        def offset_value
          self.values[:offset]
        end

        def total_count
          except(:limit, :offset).count
        end
      end
    end
  end
end

使い方

class Huga < ApplicationRecord
  include Hoge::Concerns::Paginate
end

hugas = Huga.page(3, 30) # 1ページ30件の3ページ目を取得する
hugas.current_page # 3
hugas.limit_value # 30
hugas.offset_value # 60
hugas.total_count # 全件の件数

RSpecによるRailsテスト入門を読んだ

「テスト経験が少なくて・・・」みたいな話をした時に、
色々な方から「everyday rails読むといいよ!」と勧めて頂いた。
3人の方から勧められたので、最早自分が読んでいないことに焦りを感じたレベル。

なので「everyday Rails RSpecによるRailsテスト入門」を読んだ話。

本の概要

書籍名
 Everyday Rails - RSpecによるRailsテスト入門
URL
 https://leanpub.com/everydayrailsrspec-jp

本から学びたかったこと

RSpecはともかく触れてこなかったので、1から知りたかった。

  • 開発におけるテストの進め方
  • RSpecのよく使う機能を知りたい
  • RSpecで出来ることと出来ないこと

読んでみてよかったこと

  • 本を順番通りに読み勧めて行くだけで開発の流れが一通り掴める作りになっていた
  • マニュアル的な要素は少なく、よく使う機能に関して使い方や理由がしっかり書かれている
  • RSpecだけでは苦労する点などは実現する為のgemの紹介、使い方の説明がある
  • RSpec3.6 ✖️ Rails 5.1 と読んだ時点で新しいバージョン対応であり、不安がなかった
  • サンプルコードがしっかりとあり(githubに公開)、解りにくい所は試すことができる
  • 書き手の言葉が強制感がなくとても優しく・共感が持てる

次からのTry

  • リポジトリで勉強の記録を残す
    自分で試したコードや印象に残ったコードなどは後から見れる様にしたら良かったと思った。
    自分のgithubにプッシュしておけば良いので、次から技術書を読むときは一回リポジトリを作ってみる。

次何読もうかな

  • 一旦は家に届いてまだ読んでいなかったWeb+DBを読む

WEB+DB PRESS
https://gihyo.jp/magazine/wdpress

2018年年越し前の日記

久しぶりに実家の群馬に帰ってきて年越しすることになった。

実家に帰るとVBAスマホ周りのお仕事が結構溜まっていたりする。
PCとタブレット使いこなして仕事を依頼してくるうちの親は結構凄いのだろう・・・。

今年は本当に多くのことがあった!
良かったことと悪かったことを3つぐらいずつ書こう!と思ったのだけど、
悪かったことが離婚だったり・・・お酒なしには辛い事が多く、まとまらなかったので
「書けることから」ってことで良かったことだけ書く!

良かったこと

良い転職先に巡り会えたこと

9月から新しい職場でお世話になっている。
転職直後にCTOとの1on1で転職した理由をお話した際に、
「君は一発で素晴らしい会社を選んだよ」と言われて泣きそうになった。

約4ヶ月新しい環境に身を置いて、思う今の職場の素晴らしいことは、
* 皆が「こういう事がやりたいから、こういう努力をしている」を共有しあっている事
* 失敗してもトライした人に責任を押し付けることなく、「ナイストライ!」と褒める風土
* 技術力が高く、打ち合わせ等があると知らない事が必ず出てくる

良い環境に身をおくからにはその環境を構成する1人である為に悩む事も本当に多い・・・。
が、来年のこの記事を書く頃には胸を張ってこの会社の一員と言える様に頑張っていきたい。

前の職場の方と良い関係で離職できたこと

去年の今頃、周りの人たちも30歳をこえて仕事・家庭と悩みが増える。
一方で時間が取れずに人間関係が薄くなりやすく、来年は人間関係は大事にしたいと思っていた。

そんな中で恐らく恨まれる事なく前の職場を辞められ、
前職の方とも時々飲みに行ったりして、、
仕事が別になってもそんな関係でいさせて貰えたのは個人的に本当に良かった。

EvoJapanというゲームのイベントに参加した

格闘ゲームが好きで、今年の上旬はKingOfFighters14というゲームをやっていた。
そんな格闘ゲーム好きには夢の様な世界大会であるEvoJapanというイベントが1月に開催され、それに参加した。
世界中から様々なゲームのプレイヤーが東京に集結する一大イベントとなった。
結果は65位とふるわない結果となったものの、素晴らしいイベントに参加できた事が自分に取ってとても嬉しい事だった。

転職前後は優先順位からゲームを避けていたのですが、
計画的に時間作って来年はゲームも真剣にやっていきたい。

PlantUMLでER図を書いてみる

プロジェクトでPlantUMLで図を書いている人が居るんです。
これが自分が過去感じて来た課題を解決してくれる物に見えたので自分でも書いてみる。

解決してくれそうと思っている課題

  • ER図やシーケンス図のバージョン管理
  • 他の仕組みとの連携
    • ER図からRailsのmigrationファイルを自動生成とか、その逆とか

Macで環境を作る

brew install graphviz
# brew cask install java
brew install plantuml

エラーになって 以下のメッセージが出たので、自分は上の brew cask install java を挟む事になった。

% brew install plantuml
Updating Homebrew...
plantuml: Java is required to install this formula.
JavaRequirement unsatisfied!
You can install with Homebrew Cask:
 brew cask install java
You can download from:
https://www.oracle.com/technetwork/java/javase/downloads/index.html
Error: An unsatisfied requirement failed this build.

とりあえず作ってみる

1. ディレクトリ作る

mkdir work/test_plant_uml

2. ソースを書いてみる

vi work/test_plant_uml/recipes.uml
--------
@startuml

package "cock" as cock {

  entity "recipes" {
    + id [PK]
    ==
    name:string
  }

  entity "recipe_ingredients" {
    + id [PK]
    ==
    #recipe_id:references
    #ingredient_id:references
    quantity:integer
  }

  entity "ingredients" {
    + id [PK]
    ==
    name:string
  }
}

recipes --{ recipe_ingredients
ingredients --{ recipe_ingredients

@enduml

3. 図を生成してみる plantuml コマンドに引数でファイルを渡すとpngを生成してくれる。

% plantuml work/test_plant_uml/recipes.uml
% ls work/test_plant_uml/        
recipes.png  recipes.uml

4. 出来た図
それっぽいのがとりあえずできた!

f:id:nyantech0207:20181224215457p:plain
recipes.png

デザインも変えられる

デザインも変えられる。 (参考: http://plantuml.com/skinparam

例えば以下のコードをumlファイルに追加して、図を生成し直す。

skinparam roundcorner 20
skinparam class {
  BackgroundColor PaleGreen
  ArrowColor SeaGreen
  BorderColor SpringGreen
}
skinparam stereotypeCBackgroundColor YellowGreen

f:id:nyantech0207:20181224215526p:plain
recipes_green.png

素敵!・・・とはこれはならなそうだけど、いい感じにできたら良さそう。

ActiveRecord::RecordNotUniqueが起きるケースのRSpecを書いた

Ruby経験は多いけど、テストコードの経験が少ない・・・そんなRuby技術者です。
最近テストコード(RSpecなど)を大事にするプロジェクトに携わらせて頂く事になりました。
経験が少ない自分には涙が出るほどありがたいお話なのですが、、わからないことが多い!
テスト書くのに時間が掛かりコミットしていけないなんて事に・・・なりましたorz
これはすぐ解決できる問題ではない認識で、少しずつ学んでいくしかないとは思うのですが、
少しでも早く改善していく為にハマッたポイントは振り返る。
なのでRSpecについて書きます!

書くこと

RSpecreceivereturn_toand_raise によるモックで ActiveRecord::RecordNotUnique
の例外が起きるパターンのテストコードを書く。

解決したかった課題

ボタン連打により発生する ActiveRecord::RecordNotUniqueRSpecでまず再現し、
再現した後に改善していきたい。

前提

例として扱うアプリ

  1. レシピに材料と個数を指定し、材料を登録する機能を想定
  2. レシピに材料が未登録の場合は中間テーブルを作成する
  3. レシピにその材料が既に登録されている場合は中間テーブルの個数を更新する
  4. ボタン連打などでほぼ同時に2リクエストが来た時、2. の動作が2回動き、
    DB側でDuplicate Entryエラーとなる。
    ActiveRecord としてはActiveRecord::RecordNotUnique の例外をraiseする。

コードにすると以下のイメージ Recipe#add_ingredient! が今回のテスト対象

class Recipe < ApplicationRecord

  has_many :recipe_ingredients

  def add_ingredient!(ingredient, quantity)
    recipe_ingredient = self.recipe_ingredients.find_or_initialize_by(ingredient: ingredient)
    recipe_ingredient.increment(:quantity, quantity)
    recipe_ingredient.save!
  end
end

class Ingredient < ApplicationRecord
end

class RecipeIngredient < ApplicationRecord
  belongs_to :recipe
  belongs_to :ingredient

  validates :ingredient_id, presence: true, uniqueness: { scope: :recipe_id }
end

どう改善したいか?

ActiveRecord::RecordNotUniqueがraiseされた場合は、リトライしたい。

自分のハマったポイントと書いたRSpec

  • リトライ処理をさせた時に1回目と2回目でメソッドに異なる動作をさせたい
    • and_return に引数を複数指定すると呼び出しごとに返り値を変える事ができる
      • 引数を使い切った後は最後の引数を返す動作となる
# find_or_initialize_byに固定で1回目は新規レコードを返させ、2回目は登録済みのオブジェクトを返させる
new_recipe_ingredient = recipe_ingredients.new(ingredient_id:ingredient.id)
allow(recipe_ingredients).to receive(:find_or_initialize_by).and_return(new_recipe_ingredient, recipe_ingredient)
  • ActiveRecord::RecordNotUnique が起きる状況を再現させる事が出来ない
    • 状況揃えて起こさせる事は難しいのでモックに置き換えて無理やり例外を起こさせる
# 1回目の新規レコードのみ固定でActiveRecord::RecordNotUniqueをraiseする様にする
allow(new_recipe_ingredient).to receive(:save!).and_raise(ActiveRecord::RecordNotUnique)

書いたテストコード全体

require 'rails_helper'

RSpec.describe Recipe, type: :model do

  describe '#add_ingredient!' do
    # recipesに目玉焼きレコードを登録
    let(:recipe) { FactoryBot.create(:recipe) }
    # ingredientsに卵レコードを登録
    let(:ingredient) { FactoryBot.create(:ingredient) }

    context '同じ材料が同時に登録されたとき' do
      # 目玉焼きの材料に卵を1個登録しておく
      let(:recipe_ingredient) { 
        FactoryBot.create(:recipe_ingredient, recipe: recipe, ingredient: ingredient, quantity: 1)
      }

      before do
        # 目玉焼きの材料のCollectionProxyに固定のオブジェクトを返させる用に変更
        recipe_ingredients = recipe.recipe_ingredients
    allow(recipe).to receive(:recipe_ingredients).and_return(recipe_ingredients)

    # find_or_initialize_byに固定で1回目は新規レコードを返させ、2回目は登録済みのオブジェクトを返させる
    new_recipe_ingredient = recipe_ingredients.new(ingredient_id:ingredient.id)
    allow(recipe_ingredients).to receive(:find_or_initialize_by).and_return(new_recipe_ingredient, recipe_ingredient)

    # save!時にvaidationによりActiveRecord::RecordInvalidに弾かれるので、
    # 1回目の新規レコードのみ固定でActiveRecord::RecordNotUniqueをraiseする様にする
    allow(new_recipe_ingredient).to receive(:save!).and_raise(ActiveRecord::RecordNotUnique)
      end

      it 'リトライされて指定された個数分の材料が追加登録されること' do
        expect {
          recipe.add_ingredient!(ingredient, 1)
    }.to change {recipe.recipe_ingredients.find_by(ingredient_id: ingredient.id).quantity}.by(1)
      end
    end
  end
end

結果

やったー!テストに失敗した。

% bundle exec rspec spec/models/recipe_spec.rb                                                                             (git)-[master]
F

Failures:

  1) Recipe#add_ingredient! 同じ材料が同時に登録されたとき リトライされて指定された個数分の材料が追加登録されること
     Failure/Error: recipe_ingredient.save!
     
     ActiveRecord::RecordNotUnique:
       ActiveRecord::RecordNotUnique
     # ./app/models/recipe.rb:18:in `add_ingredient!'
     # ./spec/models/recipe_spec.rb:43:in `block (5 levels) in <top (required)>'
     # ./spec/models/recipe_spec.rb:42:in `block (4 levels) in <top (required)>'

Finished in 0.19459 seconds (files took 5.26 seconds to load)
1 example, 1 failure

Failed examples:

rspec ./spec/models/recipe_spec.rb:41 # Recipe#add_ingredient! 同じ材料が同時に登録されたとき リトライされて指定された個数分の材料が追加登録されること

ブログ書けるようになるぞ

実は「継続的にブログを書くぞ!」という目標を3ヶ月間掲げていた。
しかし3ヶ月間で書いた記事は1つ・・・これはなんとかしなければ・・・
という事で自分なりの方針決めをやろう。

何故ブログ書くんだっけ?

  • 自分の技術者の価値を証明出来るようにしておきたい
  • 外向けに発信する事で組織の価値を高められる技術者でありたい

ブログを書く為に必要なことはなんだろう?

とりあえず1つのブログ記事を書く為に必要なことをリストアップ

  • ブログの登録
  • 記事を書き方(マークダウンなど)の知識
  • 題材
  • 書く為の時間

あまり思いつかなかった・・・

それぞれ出来ているだろうか?

ブログの登録
記事を書き方(マークダウンなど)の知識

ブログは登録済み!
1つは記事を書けているのでマークダウンもOK

題材

ここで3ヶ月間の反省として思ったのは以下。

  • 題材に対して不足している知識の調査や検証に時間が掛かって結局書かなかったりした
  • 書く事と書かない事をしっかり決めていないと広がりすぎてまとまらず、書かなかったりした

書く為の時間

  • 時間がない事はない。例えば日曜日の夜2時間やるとか決めればやれる。

当面の間の対策

  • 習慣になるまでは内容は気にせず簡単な題材でとにかく書く
  • 題材を決めの時間と書く時間を分け、題材決めをしっかりやる
  • 題材決めと書く時間を一旦カレンダー管理でしっかりやる