UUUMエンジニアブログ

UUUMのエンジニアによる技術ブログです

Ruby on Rails Model関連のクラスで定義するscopeとクラスメソッドの違いについて

自己紹介

皆さんこんにちは。ohashi_tと申します。

弊社ではバックエンドにRuby on Railsを採用しているプロジェクトが多数あり、 日々押し寄せる業務要件に応え、アプリケーションがスケールしていく中でも絶えず改善が行われております。

私も日々の業務を通じて様々な要件に対応する機会があり、 その都度Ruby on Railsに対する理解が段々と深まって来ていると 実感を得られています。 今回、その中で特に印象に残っている 「Model関連のクラスで定義するscopeとクラスメソッドの違いについて」 事例を1つご紹介したいと思います。

はじめに

scopeとクラスメソッドの違いについてググるとこれに関する記事が沢山出てきます。 しかし、個人的には誤解のある表現が多いと感じ なかなか想定する動作を実現出来ず、振り回された経験がありました。 この度記事としてアウトプットさせていただき、 少しでも皆様のお役に立てるよう願っております。

今回扱うデータはこちらになります。

irb(main):001:0> Weather.all
  Weather Load (0.4ms)  SELECT `weathers`.* FROM `weathers`
=>
[#<Weather:0x000000010fc8a4a0
  id: 2,
  temperature: 21.4,
  place: "Bankok",
  status: "cloudy",
  observed_at: Sat, 01 Apr 2017 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00>,
 #<Weather:0x000000010fde3978
  id: 3,
  temperature: 20.6,
  place: "Tokyo",
  status: "rainy",
  observed_at: Fri, 01 Jun 2018 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00>,
 #<Weather:0x000000010fde38b0
  id: 4,
  temperature: 34.7,
  place: "Sydney",
  status: "sunny",
  observed_at: Thu, 01 Aug 2019 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00>,
 #<Weather:0x000000010fde36f8
  id: 5,
  temperature: 27.8,
  place: "Newyork",
  status: "windy",
  observed_at: Thu, 01 Oct 2020 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00>,
 #<Weather:0x000000010fde3630
  id: 6,
  temperature: 0.0,
  place: "London",
  status: "snowy",
  observed_at: Wed, 01 Dec 2021 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00>]

多分一番分かりやすいと思われる違い

何もしないクラスメソッド・scopeの比較

早速定義して動作を確認してみましょう。

class Weather < ApplicationRecord
  .....

  scope :do_nothing_by_scope, -> {}

  def self.do_nothing_by_class_method; end
end
scopeを使用した場合
irb(main):001:0> Weather.do_nothing_by_scope
  Weather Load (0.6ms)  SELECT `weathers`.* FROM `weathers`
=>
[#<Weather:0x0000000113a6ff90
  id: 2,
  temperature: 21.4,
  place: "Bankok",
  status: "cloudy",
  observed_at: Sat, 01 Apr 2017 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:50:52.170025000 UTC +00:00>,
 #<Weather:0x0000000113be6f40
  id: 3,
  temperature: 20.6,
  place: "Tokyo",
  status: "rainy",
  observed_at: Fri, 01 Jun 2018 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:37.625832000 UTC +00:00>,
 #<Weather:0x0000000113be6e78
  id: 4,
  temperature: 34.7,
  place: "Sydney",
  status: "sunny",
  observed_at: Thu, 01 Aug 2019 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:46.206855000 UTC +00:00>,
 #<Weather:0x0000000113be6db0
  id: 5,
  temperature: 27.8,
  place: "Newyork",
  status: "windy",
  observed_at: Thu, 01 Oct 2020 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:53:54.038531000 UTC +00:00>,
 #<Weather:0x0000000113be6ce8
  id: 6,
  temperature: 0.0,
  place: "London",
  status: "snowy",
  observed_at: Wed, 01 Dec 2021 00:00:00.000000000 UTC +00:00,
  created_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00,
  updated_at: Fri, 24 Jun 2022 05:54:01.782352000 UTC +00:00>]

どうやら戻り値が偽(false, nil)であればActiveRecord_Relationsのインスタンス群を返すようです。

クラスメソッドを使用した場合
irb(main):002:0> Weather.do_nothing_by_class_method
=> nil

一方クラスメソッドで定義した場合はそのまま戻り値が返ります。

調べてみたところ、どうやらここのinstance_exec(*args, &block) || selfの箇所で||演算子が使用されているため、lambdaで定義された評価結果がnilもしくはfalseの場合にselfを返すようです。

github.com

    def _exec_scope(name, *args, &block) # :nodoc:
      @delegate_to_klass = true
      _scoping(_deprecated_spawn(name)) { instance_exec(*args, &block) || self }
    ensure
      @delegate_to_klass = false
    end

気をつけなければいけないパターン

条件分岐をする場合

弊社のプロジェクトでもよく見るパターンですが、 例えばデフォルト引数でnilを宣言しておき、空でない値が指定された時のみ絞り込みを行うパターンが考えられます。

def self.filter_by_class_method(status = nil)
  where(status: status) if status.present?
end

scope :filter_by_scope, ->(status = nil) { where(status: status) if status.present? }

引数が空でない値が指定されれば同等の出力結果が期待出来ますが、 引数を指定せず呼び出した場合は先程の例のように差異が発生してしまいます。

もしscopeと同等の結果を得たいならば下記のように引数が指定されなかった場合の条件も明示する必要があります。

def filter_by_class_method(status = nil)
  return where(status: status) if status.present?

  all
end

scopeを使用する際のイメージとしては、幾つかメソッドチェーンして抽出条件を絞り込んでいくのがscopeと言う名称と役割がマッチしていて用途が適切なのではないかと思います。

そのためには全ての条件で戻り値がActiveRecord::Relationsを返すように工夫する必要があります。 注意しなければいけないのは全てのActiveRecordに関するクエリがActiveRecord::Relationsを返すわけではなく、 例えばfirsttakeは戻り値にインスタンスや配列を返す(ActiveRecord::Relationsを返さない)ため、 メソッドチェーンさせることが出来ません。

このようなパターンでは役割の違いを明確にするため、 クラスメソッドを使用するのが良いのではないかと思います。

ちなみにscopeでfind_byを使用する場合は更に注意が必要で、 普段であれば該当するレコードが見つかればそのレコードに関するインスタンスを返す、無ければnil..... のはずがnilではなくレシーバーで評価され得るインスタンス群全てが返ってくるので 挙動の違いをしっかりと把握していないと想定外のエラーに見舞われます。

scopeで1件も返したくない場合

ActiveRecordではnoneというメソッドがあり、空のActiveRecord::Relationsを返します。こちらはrubyで例えると空配列に相当するので、 もしこのようなケースではこちらを使用するのが良いと思います。

scopeでのブロックの役割はメソッドと異なる

scopeでのブロックの役割はscopeが適用されたActiveRecord_Relationsのみに追加で名前空間を提供 (そのscopeを介したActiveRecord::Relationの集合体だけに更にメソッドやscopeを追加) することが出来ます。

このようにscopeではブロックの役割が既に決まっておりクラスメソッドで用いていた要領でブロックを用いることが出来ません。

普段使用しているような要領でメソッド内のタイミングでブロックの評価をさせたい場合なども クラスメソッドを使用する方が良いのではないかと思います。

さいごに

「scopeはクラスメソッドに比べて短く宣言出来て素敵~」 みたいにscopeの意図を十分に掴めていないままアウトプットに踏み切った記事が見られましたので 上記で説明したように

「クラスメソッドとscopeでは挙動や用途が異なる」

ということをしっかりと理解された上で使い分けることをおすすめします。

最後にこちらが今回取り上げたscopeに関するソースコードです。ざっと目を通してみましょう。

github.com

def scope(name, body, &block)
  unless body.respond_to?(:call)
    raise ArgumentError, "The scope body needs to be callable."
  end

  if dangerous_class_method?(name)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but Active Record already defined " \
      "a class method with the same name."
  end

  if method_defined_within?(name, Relation)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
      "an instance method with the same name."
  end

  extension = Module.new(&block) if block

  if body.respond_to?(:to_proc)
    singleton_class.define_method(name) do |*args|
      scope = all._exec_scope(*args, &body)
      scope = scope.extending(extension) if extension
      scope
    end
  else
    singleton_class.define_method(name) do |*args|
      scope = body.call(*args) || all
      scope = scope.extending(extension) if extension
      scope
    end
  end
  singleton_class.send(:ruby2_keywords, name)

  generate_relation_method(name)
end

この辺はガード説ですね。同名のメソッドが既に定義されていたり、Procクラスでは呼べるであろうcallを持って無ければここで弾かれます。

  unless body.respond_to?(:call)
    raise ArgumentError, "The scope body needs to be callable."
  end

  if dangerous_class_method?(name)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but Active Record already defined " \
      "a class method with the same name."
  end

  if method_defined_within?(name, Relation)
    raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
      "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
      "an instance method with the same name."
  end

先程も説明したとおり、scope独自の名前空間の作成に使用されます。

extension = Module.new(&block) if block

そしてこのタイミングでメソッドが定義されて、 呼び出された時にdo...end内が評価されるんですね。


if body.respond_to?(:to_proc)
  singleton_class.define_method(name) do |*args|
    scope = all._exec_scope(name, *args, &body)
    scope = scope.extending(extension) if extension
    scope
  end
else
  singleton_class.define_method(name) do |*args|
    scope = body.call(*args) || all
    scope = scope.extending(extension) if extension
    scope
  end
end

最後にリレーション間でもメソッドが呼び出せるになるようです。

  generate_relation_method(name)

以上です。ありがとうございました!

カオスの鑑賞

はじめ

こんにちは。UUUMのシステムユニットの柿木です。
今日はカオスを鑑賞していきたいと思います。
用意するのは、python3の実行環境です。
turtleライブラリを利用してカオスを鑑賞します。
* 今回用意したコードは結構PC唸るので、実行は自己責任でお願いします。

そもそもカオスとは?

wikipediaによると

次の3つの条件を満たす写像f: V → Vは、Vの上でカオス的であるといえる[35]。
初期条件に鋭敏に依存する。
位相的に推移的である。
周期点はVにおいて稠密である。

という定義があります。
つまりカオスということですね。

前提

開発用ログファイル 座標など見たい時に使います。 以下ファイルは全て同じ階層です。

IS_LOG_SHOW = False

def log(s):
    if IS_LOG_SHOW:
        print(s)

ロジスティック写像

参考URL
定義がよく分からないので、さっそく実例を見ていきましょう。
ロジスティック写像は

 f(x) = \alpha X(1-X)

で、定義される関数があるとき

 a_1=f(x), a_2=f(f(x)), a_3=f(f(f(x))) \cdots

という風に定義される数列のことをいいます。
これを実装したのが以下のファイルです。 \alpha=4です。
*3000回以上やると描画の関係か重くなってしまうので実行の際は注意してください。

from log import log
from turtle import *

SPEED = 10
SHAPE = 'circle'
LINE_COLOR = 'red'
TIRTLE_COLOR = 'yellow'
SCALE_X = 150
SCALE_Y = 500
N = 1000

def do():
    shape(SHAPE)
    speed(SPEED)
    color(LINE_COLOR, TIRTLE_COLOR)

    y = 0.6

    for x in range(N):
        if x == 0:
            continue
        y = f(y)
        x = symmetric_x(x, 5)
        log(f'(x, y) = {x},{y}')
        setpos(x * SCALE_X, symmetric_y(y) * SCALE_Y)
    
    done()


def f(x):
    return 4 * x * (1 - x)

def symmetric_x(x, l):
    if int(x /l)%4 == 0:
        return x%l
    elif int(x /l)%4 == 1:
        return l - x%l
    elif int(x /l)%4 == 2:
        return -(x%l)
    else:
        return - (l - x%l)

def symmetric_y(y):
    return y - 0.5

これをmain関数から呼び出すと
(以下同じような呼び出しなので次からは省略)

import logistic

def main():
    logistic.do()

main()

以下のような軌跡を描きます。

logistic

はい、まさにカオスですね。
 f(x) = 4X(1-X)という単純な規則を持ちつつも、動きがランダムで0と1の間でしか動いていません。
とても面白いですね。

ラングドンのアリ

参考URL
これはカオスなのか?って思いましたが、見た目がカオスっぽいので入れました。
ラングドンのアリの考案者であるクリストファー・ラングドンさんはカオス研究していたみたいなので、きっとこれはカオスなのでしょう。
さて本題に入ります。
ラングドンのアリはシンプルな2つの規則により構成されます。

白いマスにアリがいた場合、90°右に方向転換し、そのマスの色を反転させ、1マス前進する。
黒いマスにアリがいた場合、90°左に方向転換し、そのマスの色を反転させ、1マス前進する。  

これを12000回動かしてみましょう。
コードは以下になります。

from log import log
from turtle import *

SPEED = 10
PEN_SIZE = 10
SHAPE = 'turtle'
LANGTONS_LIMIT_X = 1001
LANGTONS_LIMIT_Y = 1001
N = 12000

def do():
    pen(fillcolor='black', pencolor='black')
    pensize(PEN_SIZE)
    speed(SPEED)
    shape(SHAPE)

    langtons_map = init_langtons_map(LANGTONS_LIMIT_X, LANGTONS_LIMIT_Y)
    x, y = 501, 501
    direction = 1

    for i in range(N):
        if langtons_map[x][y]:
            langtons_map[x][y] = False
            left(90)
            pen(pencolor='white')
            forward(10)
            direction = (direction - 1 + 4)%4
            x, y = set_x_y(x, y, direction)   
        else:
            langtons_map[x][y] = True
            right(90)
            pen(pencolor='black')
            forward(10)
            direction = (direction + 1)%4
            x, y = set_x_y(x, y, direction)
            
        log(f'x is {x}, y is {y}, direction is {direction}')
    done()

def init_langtons_map(n, m):
    langtons_map = []
    for i in range(n):
        temp_list = []
        for j in range(m):
            temp_list.append(False)
        langtons_map.append(temp_list)
    return langtons_map

def set_x_y(x, y, direction):
    if direction == 0:
        y += 1
    elif direction == 1:
        x += 1
    elif direction == 2:
        y -= 1
    else:
        x -= 1
    return x, y

実行結果が以下になります。

langtons

本来はマス目の塗りつぶしなので綺麗になるのですが、turtleライブラリはあくまで軌跡を描くものになるのでちょっと塗り残しが発生してしまします。
しかし、これはこれで趣がありますね。
また、上の方でまっすぐ道を作ってるのも興味深いです。

ローレンツ方程式

参考URL
ローレンツ方程式は、ローレンツさんが大気現象のモデルを研究していた時に発見した方程式です。 方程式は以下になります。

 {\displaystyle {\frac {dx}{dt}}=-px+py}
 {\displaystyle {\frac {dy}{dt}}=-xz+rx-y}
 {\displaystyle {\frac {dz}{dt}}=xy-bz}

左辺は時間微分です。 なので、コードは以下のようになります。
 p=10, r=28, b=\frac{8}{3}, h=0.01です。

from log import log
from turtle import *

SPEED = 10
SHAPE = 'circle'
LINE_COLOR = 'red'
TIRTLE_COLOR = 'yellow'
SCALE_X = 10
SCALE_Y = 10
N = 10000

def do():
    speed(SPEED)
    shape(SHAPE)
    color(LINE_COLOR, TIRTLE_COLOR)

    p = 10
    r = 28
    b = 8/3
    h = 0.01
    x = 1
    y = 1
    z = 1

    setpos(x * SCALE_X, y * SCALE_Y)
    for i in range(N):
        x, y, z = f(x, y, z, p, r, b, h)
        log(f'x is {x}, y is {y}')
        setpos(x * SCALE_X, y * SCALE_Y)
    
    done()

def f(x, y, z, p, r, b, h):
    x_next = x - (p * h * x) + (p * h * y)
    y_next = y - (h * x * z) + (r * h * x) - (h * y)
    z_next = z + (h * x * y) - (b * h * z)
    return x_next, y_next, z_next

実行結果は以下のようになります。 xy平面の断面図です。

lorents

今まで動かしてきたのはカクカクとしたものだったので、カーブが描かれるとまた新鮮に感じますね。

レスラー方程式

参考URL
レスラー方程式は、上記のローレンツ方程式を参考に単純化されたものになります。
方程式は以下になります。

 {\displaystyle {\frac {dx}{dt}}=-y-z}
 {\displaystyle {\frac {dy}{dt}}=x+ay}
 {\displaystyle {\frac {dz}{dt}}=b+xz+cz}

ローレンツ方程式と比べると少し簡単になっていますね。
左辺は時間微分となっているので、ローレンツ方程式の時と同様に求めます。
なので、コードは以下のようになります。
 a=0.2, b=0.2, c=5.7, h=0.1です。

from log import log
from turtle import *

SPEED = 10
SHAPE = 'circle'
LINE_COLOR = 'red'
TIRTLE_COLOR = 'yellow'
SCALE_X = 20
SCALE_Y = 20
N = 10000

def do():
    shape(SHAPE)
    speed(10)
    color(LINE_COLOR, TIRTLE_COLOR)

    a = 0.2
    b = 0.2
    c = 5.7
    h = 0.1
    x = 10
    y = 1
    z = 1
    
    for i in range(N):
        x, y, z = f(x, y, z, a, b, c, h)
        setpos(x * SCALE_X, y * SCALE_Y)
    
    done()

def f(x, y, z, a, b, c, h):
    x_next = x - (y * h) - (z * h)
    y_next = y + (x * h) + (a * y * h)
    z_next = z + b + (x * z * h) - (c * z * h)
    return x_next, y_next, z_next 

実行結果は以下のようになります。 xy平面の断面図です。

rossler
ローレンツ方程式よりは単純な動きをしていますね。

最後に

いかがでしたでしょうか?
私の拙い言葉でどれだけカオスの魅力をどれだけ伝えられたか分かりませんが、1ミリでもカオスの魅力が伝わったのなら幸いです。
今回紹介したコードでは回数、描くスピード、色、縮尺など変えれるようにしたのでぜひ遊んでみてください。

2年半ぶりの開発合宿に行ってきました!

こんにちは あるいは こんばんは
UUUMのシステムユニットの渋江です。
5/13(金)〜5/14(土)に開発合宿へ行ってきました! 2019年秋以来、2年半ぶりの開催です。 今回の目的はスキルアップとコミュニケーションです。 リモートワーク中心で会うことが少なくなり、新入社員も増え、オフラインで会ったことのないメンバーもいました。この機会に親睦を深めようと思います!
この記事ではその時の様子をお届けします!

続きを読む

静的解析ツール「RubyCritic」の紹介

おはようございます こんにちは こんばんは。
UUUMのシステムユニットのmatsumotoです。
先日、RubyCriticという静的解析ツールを導入しましたので紹介します。

RubyCriticとは?

RubyCriticはrubyコードを静的解析するツールです。rubygemで提供されています。

ファイル毎の複雑度や重複した記述などを指摘してくれます。

解析結果はブラウザ上で確認できます。

導入に至った経緯

自分が担当しているシステムには既にメジャーな静的解析ツールであるRubocopが導入されており、コーディング規約に準拠してるかのチェックは行えていました。
しかし、より可読性、保守性が高いコードを書くためにもコーディング規約のチェックに加え、ファイル毎の複雑度や重複箇所などもチェックされるようになると良いと思いました。

また、RubyCriticを導入する事によってレビュー前にコーディングの指摘を受ける事ができ、結果としてレビュワーの負担を軽減する事ができると思いました。

あと解析結果を表示するブラウザがとても見やすい点もRubyCriticを導入した理由の一つです。

導入方法

導入方法はとてもシンプルです。

まずはrubycriticのgemをインストールします。

Gemfile

group :development do
  gem "rubycritic", :require => false
end

bundle installを行えば※基本的には導入完了です。


※ 補足

自分がrubycriticを導入しその後実行した時下記エラーが発生しましたので共有します。

RailsアプリにDockerを導入している場合に起こりうる可能性があるエラーです。(自分が担当しているシステムにもDockerが導入されています。)

エラー文

ArgumentError: invalid byte sequence in US-ASCII
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html/line.rb:22:in `delete'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html/line.rb:22:in `render'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html/code_file.rb:42:in `block in render'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html/code_file.rb:39:in `each'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html/code_file.rb:39:in `with_index'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html/code_file.rb:39:in `render'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html_report.rb:37:in `block (2 levels) in create_directories_and_files'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html_report.rb:36:in `open'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html_report.rb:36:in `block in create_directories_and_files'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html_report.rb:34:in `each'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html_report.rb:34:in `create_directories_and_files'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/generators/html_report.rb:21:in `generate_report'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/reporter.rb:9:in `block in generate_report'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/reporter.rb:8:in `each'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/reporter.rb:8:in `generate_report'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/commands/default.rb:29:in `report'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/commands/default.rb:19:in `execute'
  /usr/local/bundle/gems/rubycritic-4.6.1/lib/rubycritic/cli/application.rb:21:in `execute'
  /usr/local/bundle/gems/rubycritic-4.6.1/bin/rubycritic:10:in `<top (required)>'
  /usr/local/bundle/bin/rubycritic:23:in `load'
  /usr/local/bundle/bin/rubycritic:23:in `<top (required)>'
ERROR: 1

調査した事

Dockerfileを確認するとDockerhub上のRubyのimage(version 2.5.7)が使われていました。

そのイメージの詳細を確認するとデフォルトのエンコーディングがUS-ASCIIでした。

$ docker run -it ruby:2.5.7 bin/bash
root@49bf9357e1b3:/# ruby -e 'puts Encoding.default_external'
US-ASCII

なので上記エラーが出たようです。

export RUBYOPT=-EUTF-8すればエンコーディングを変更できます。

$ docker run -it ruby:2.5.0 bin/bash
root@6b9b22a6ce36:/# export RUBYOPT=-EUTF-8
root@6b9b22a6ce36:/# ruby -e 'puts Encoding.default_external'
UTF-8

対処方法

Dockerfileに下記コードを追加

ENV RUBYOPT -EUTF-8

これでエンコーディングを変更できエラーなくrubycriticを実行できます。

因みにMacにrbenv経由で入れたRubyの場合、デフォルトのエンコーディングはUTF-8になっています。

実行方法

$ rubycritic .

で実行できます。

実行後./tmp/rubycritic/直下に解析結果がhtmlで保存され、ブラウザが起動して閲覧することができます。

$ rubycritic app/controllers/api/v1

のように指定のディレクトリだけを対象にして実行することもできます。

その他オプションはREADMEのUsageに記載さされているのでご確認いただけたらと思います。

解析内容について

rubycriticを実行した結果は下記になります。

$ docker-compose run --rm web bundle exec rubycritic app/controllers                                                                                                                                                                                                                              11:47:35
Creating coda-develop_web_run ... done
running flay smells
...........................................................
running flog smells
....................................................................................................................................................................................
running reek smells
....................................................................................................................................................................................
running complexity
....................................................................................................................................................................................
running attributes
....................................................................................................................................................................................
running churn
....................................................................................................................................................................................
running simple_cov
....................................................................................................................................................................................
New critique at file:////coda/tmp/rubycritic/overview.html
Attempted to open file:////coda/tmp/rubycritic/overview.html and failed because Unable to find a browser command. If this is unexpected, Please rerun with environment variable LAUNCHY_DEBUG=true or the '-d' commandline option and file a bug at https://github.com/copiousfreetime/launchy/issues/new
Score: 53.79
open tmp/rubycritic/overview.html

RubyCriticは、以下のruby解析用Gemに依存しています。

  • flay

    • コード同士の類似度が高い部分を検出し、共通化に役立ちます。
  • flog

    • コードの複雑度が高い部分を検出します。
  • reek

    • 臭いコード(smell)を検出します。
    • smellは、reekのドキュメントによると、「コードが読みにくい、または保守しづらい場所を示唆するもの」とのことです。

解析結果の確認

Overview
Code: ソースコードごとの警告の数や複雑度や評価(Rating)が一覧表示されます。
Smells: 臭いコードが存在する箇所が一覧表示されます。
CodeやSmellsで対象コードのリンクを押した際に開かれる画面。
コードの中に警告内容がインラインで表示され、警告名をクリックすると警告解説記事にジャンプします。

reekが検出するsmellについて

rubycriticはreekというコードの"臭う"箇所を検出するgemに依存していますが、そのreekが検出するsmellはデフォルトの設定だととても個性的かつ厳しすぎるくらい指摘されます。

なので使い勝手が悪い、また肌に合わない場合はカスタマイズすると良いかと思います。

リポジトリのトップにyamlファイル(.reekファイルまたはconfig.reekファイル)を置くことで、reekが検出する特定のsmellをカスタマイズして抑制することができます。

詳細はこちらに記載されています。

RubyCriticを導入した結果(感想)

レビューをいただく前に実装で不安な箇所があった際は、今回導入した解析ツールを走らせる事によって改善点の早期発見とともにレビュアーの負担を削減できました。

また解析結果を表示するブラウザのUIもとても分かりやすく、今後リファクタをするタスクに着手する際はとても助けになるかと思います。

JetBrainsのDataGripを勧めてみる。

こんにちは。UUUMのシステムユニットの永井です。
Apex Legends シーズン13が始まりました。
今シーズンはマスター目指します。

はじめに

皆さんはデータベース管理ツールは何を使用していますか?
PHPならMySQL Workbench、PostgreSQLならpgAdmin 4、国産フリーソフトのA5:SQL Mk-2など 様々あると思いますが、私は普段JetBrainsのDataGripというツールを使用しています。
ですので、今回はDataGripの便利な機能を何点か共有できたらと思います。

ざっくりDataGripとは

JetBrainsが出しているデータベースIDEです。2017年生まれくらいです。
とにかく色々なデータベースを管理することができます。

なので、DataGripを使えれば正直どこ行っても新しいツールを覚える必要ないです。

フリーソフトではありませんが、無料30日間体験版もあります。
またざっくり一年間支払いすれば、その製品に対して永久fallbackライセンス(購入時点のバージョンならサブスクしてなくても使える)も得られます。

詳しくは以下を参照してください。

サブスクリプションベースのライセンスモデル対-永久ライセンスモデル

年額払いなら2年目は20%割引、3年目なら40%割引もしてくれます。

便利機能

コードフォーマット

例えばWebアプリケーションでデータの出力内容がおかしくて、
デバックやログに表示された複雑な条件のズラーっと長いSQLを確認する必要がある場合、
コードフォーマットを使えばいい感じに見やすく整えてくれるので捗ります。

公式のGifです。整形前がもっと複雑でもいい感じに整形してくれます。 https://www.jetbrains.com/datagrip/features/img/editor/FormatCode.gif

コード補完機能

JetBrains製のIDEを使った事がある人ならわかると思うんですが、コード補完が強力です。 DataGripでもがっつりコード補完してくれます。

特にありがたいなと思うのは、テーブルに設定されている外部キーを元にJoin句の補完候補を出してくれることです。

結合先のテーブルが多い時は候補も多くあげてきますが、
そんな時は、user_groupsというテーブルを結合させたい場合は、ugと入力すると候補絞ってくれます。

https://www.jetbrains.com/datagrip/features/img/completion/JOIN.png

他にも色々と補完してくれます。 www.jetbrains.com

差分確認機能

任意のスキーマやテーブルの差分を確認することもできます。

DataGripの古いバージョンだと差分ビューアー内で、差分を修正するためのDDLを表示してくれてはいましたが、そのまま実行はできませんでした。
しかし最近差分ビューアーが改良され、ビューアー内で確認→実行できるようになったみたいです。

https://resources.jetbrains.com/help/img/idea/2022.1/db_compareTableStructures.png

ただし開発環境とステージング/本番のテーブル比較して確認したいだけの時に、間違えてExecute押してしまうと惨事なので、
最低限本番環境の接続設定は読み取り専用にしておくといいです。

pleiades.io

ダイアグラム生成機能

全体又は任意のテーブルを選択してダイアグラムを確認できるのもいい点だと思います。
また、生成したダイアグラムは以下の形式でエクスポートできるようになったので、 サードパーティツールとの互換性が確保されたとのことです。

  • yEd の .graphml
  • JGraph の .drawio
  • Graphviz の .dot
  • Graphviz の位置情報付きの .dot
  • Mermaid の .md
  • Plantuml
  • IDEA の .uml

DataGrip上のダイアグラム

drawio形式でエクスポートした結果

ただし、ER図を書いて、それを元にDDLや定義書を作成してくれる機能はない(たぶん)ので
今後のアップデートで追加してくれないかなと思います。
因みにそういった事やりたい人はA5:SQL Mk-2 がオススメです。

どんな人にオススメ?

こんな人にオススメです。

  • JetBrains製のIDEを使った時ある人。→ 使用感同じです。
  • All Products Pack もってるけど使ってない人 → もったいないので使ってください。
  • 色々なDBに接続する必要がある人 → ツール切り替えしなくて済みます。cross-DBMSが使えるので、PostgreSQL データベースから SQL Server へすべてのテーブルを簡単にコピーとかもできるらしいです。
  • 多機能すぎて使いこなせない人 → データベースを管理する上で基本的な操作などをサポートしてくれるので十分利用価値あると思います。

さいごに

今回はDataGripを紹介させていただきました。
他にも便利機能あるので、ぜひ一度使ってみてください!

pleiades.io

また、UUUMではエンジニアを絶賛募集中です!
https://www.wantedly.com/projects/861137

自己研鑽補助ってなに?

UUUMのシステムユニットのしだです。
GWが明け、久々の仕事で何から手をつけるんだっけ? となっている方も多いのではないでしょうか。私もそうです。
2022年はなんと今日から7/18まで祝日がありません。どうにか日々を乗り切りましょう。

はじめに

UUUMのシステムユニットは勉強会・輪読会が盛んで、週に3、4回、もしかしたら毎日なにかしら開催されているのではないかなという状況です。
もちろん全てに全員が参加しているわけではありませんが、チームで開催されたり、個人が宣言して有志で集まって開催されたり、結構自由に行われています。
社内だけではなく、社外のセミナーや勉強会に参加される方も多く、自主的に学ぶことに意欲の高い人が多くいる印象です。


私が以前いた会社では、私のいた部署はもちろん、システム部でも特に勉強会などはなく、恥ずかしながら外部セミナーなどに参加する発想自体がありませんでした。完全に、自身の文化資本にない状態ですね。
本を読むことは頭にあっても、身銭を切るにあたって一番良さそうな本はなんなのか、金額的にどうなのかなど調べていると、もうネットでいいかとなってしまったり。
そういった人間でも、業務に関連する本をなんやかんや毎月買って読んでいたり、社内外の勉強会やセミナーに参加するようになりました。
単純に、業務時間内に勉強会が開催されていることや、学習意欲の高い人たちがたくさんいるから影響されたのも要因ですが、私にとって一番影響があったのが、自己研鑽補助という制度でした。


ということで、自主的に学ぶ文化形成に寄与しているんじゃないかと思われる、自己研鑽補助について紹介します。
ちょうど前回sawada_yさんが『Udemyの動画と書籍については、自己研鑽補助の制度を利用し購入しました』と書いていましたが、その制度がこちらです。

自己研鑽補助とは

エンタメに触れ、情報を集め、体感すること、ビジネススキルに繋がることなど 常に感度を高め、自己研鑽に励む社員を会社がサポートする制度で、 毎月利用上限を1万円とし、ビジネススキル向上やエンタメ情報収集のための書籍購入などを経費申請できるUUUM独自の補助制度です。

全社的なルール

全社的には、下記の用途で利用OKとされています。(※2022年5月時点)

  • サブスクリプション(サブスク系映像、ニュースコンテンツ等)
  • 書籍購入(ビジネス書籍、マンガ、雑誌、新聞)
  • セミナー参加(セミナーへの参加)
  • 資格取得、教材購入(スクール・資格取得、教材の購入等)
  • ビデオ・コミックレンタル
  • 映画鑑賞
  • ステージ・音楽鑑賞(コンサート、芸術鑑賞等)
  • その他(上記項目以外 アミューズメントパーク等)

システムユニットでのルール

システムユニットでは業務内容を考慮し、以下のルールで運用されています。

  • 技術・ビジネス書・小説系の書籍や雑誌可
  • 紙・電子書籍のマンガは「マンガでわかるRuby」など、ノウハウ系のもののみ可
  • 映像系円盤(DVDなど)は、レンタルのみ可
  • 映画(劇場・配信)・ステージ・芸術鑑賞など可

※記載がないものは全社ルール適用(サブスクリプションや資格取得など)

具体的にどう利用しているか

私自身は主に、システム関連の知識を得るための本を買ったり、業務知識に関連する本を買うことに利用しています。
会社からの補助が1万円まであるからと、比較的高額な書籍も含めて買う習慣がつきました。
先にも書いたとおり、自分で買うとなると、あれもこれも買う余裕がないので選りすぐりの1冊を求めて探す時間が発生してしまうんですよね。
迷って探している時間は、気軽に買って読み終わる時間と同じかもしれない。そういった意味でも、かなりメリットの大きい制度だと思っています。
そして、買えば読むので、結果的に業務外の時間でも業務の学習をすることが増えました。

これが私だけなのか、みんなそう思っているのか、そもそもどれくらいの人がこの制度利用しているのかが知りたくなったので、システムユニット内でアンケートを取ってみました。

アンケート結果

有効回答数は18件、システムユニット全体で40人ほど在籍しているので、およそ半数に回答いただきました。
設問は下記の通りです。

  • 自己研鑽補助、毎月使ってますか?
  • 使わないことがある方は理由を教えて下さい
  • 使っている方、使用用途を教えて下さい(複数選択可)
  • 自己研鑽補助があったからやったこと・買ったものがあれば教えて下さい
    • 例:受験料を補助申請できるから〇〇資格を取った、技術系書籍は補助があるから気軽に買うようになった、毎月映画を見る習慣ができた、など
  • 自己研鑽補助を利用して得たもので、自分にとってよかったもの
    • 得たものの具体的な内容(書籍の場合は書籍名、体験・経験の場合は概要など)と、よかった理由を教えてください
自己研鑽補助、毎月使ってますか?

毎月使っている人が12名、基本的に使っているも含めると16名で、全体の88.9%が利用している状況です。
「利用していない」も選択肢として設けていましたが、0でした。
全社では利用率が7割程度らしいので、システムユニットは比較的利用率が高いと言えそうです。

利用しない理由

回答6件のうち、申請忘れや締切によるものが3件、あとは主体的な理由です。
その他でいただいたうち、「前月の本を読み切れていないため」という回答があって誠実な人だなと感心しました。
私は積ん読することへのためらいが一切ないので、読んだかどうかに関わらず欲しい本はとりあえず買ってしまいます。

利用用途

予想通り、ほぼ全員が技術系書籍の購入に利用していました。
全社的にはエンターテインメントに関する補助という側面の強い制度ですが、システムユニットではかなり実務に即した利用が行われているのが見て取れます。
とはいえサブスクリプションや映画などにもかなり利用されているので、業務以外にもいい影響がありそうです。

自己研鑽補助があったからやったこと・買ったもの

ここは自由記入枠で、回答が15件ありました。
項目内に理由を複数記載してくださっていた方もいたので分割した上で、大きく分けて以下の通りの内容です。

  • 技術書を買うようになった(10件)
  • 技術以外も含めた学習(4件)
  • サブスクで音楽や動画に触れる(3件)
  • 資格取得・受験(2件)

技術書を買うようになった、には、以前と比較して何冊も買うようになったも含んでいます。
頂いた中には以下のようなコメントもありました。

自身が持っているスキル領域以外に対して気軽にチャレンジするようになった
どうしても自身の業務に関わることが優先になりがちですが、会社から補助があることで、ちょっと他のことも試してみようという気になれるのは確かにそのとおりです。
オーディオブックを聞くようになり、ランニングが続くようになった
これは副次的な効果ですが、健康にも寄与している例ですね。

自己研鑽補助を利用して得たもので、自分にとってよかったもの

ここも自由記入枠で、回答が12件ありました。
いくつかコメントをピックアップします。

毎月技術書を買おう、読もうという気になれる
技術書を買うハードルが下がった。ちょっと高い分たまに1冊買う程度だったのが、すすめられたものを気軽に買えるようになった。
学習習慣がついた

このあたりは私が感じていたのと同じく、この制度によって習慣に影響があった例ですね。
また、それに加えてこういったコメントもありました。

技術的な成長はもちろん、美術館鑑賞など興味の幅が広がった
英語が以前より流暢に読めるようになり、業務でドキュメントを読むときに役立っています。また、 人から勧めてもらった映画などでコミュニケーションに役立ちました。

こちらは自身の業務スキルのみではなく、人とのかかわり合いや自身のプライベートにも影響があった例ですね。
さらに、具体的に書籍名などを上げてくださった例もありました。

PMBOK 第7版。高いので補助がなかったら絶対自主的に買わなかったと思うので。
「闇の西洋絵画史」山田五郎さん独自の視点で西洋絵画の主だったモチーフを解説してくれます。ちょっと高い

PMBOKは私も補助を利用して購入しました。
現在輪読会でこの本を読んでいますが、かなり高額なので補助がなかったら輪読会で読む本に上がらなかったのではないかなと思います。

「闇の西洋絵画史」、面白そうだなと思ってピックアップしてしまいました。業務とは直接関わりませんが、こういう本も気軽に買えるのがありがたいところです。(とりあえず1巻買いました)

まとめ

アンケートの設問がいまいちでしたね……特に最後2つは似たような内容になってしまったので、意図が明確な質問を考えるべきでした。
そんな反省点はありつつも、回答内容を見るに、身も蓋もない話で恐縮ですが、お金面での補助は学習習慣にかなり影響があると言って良さそうです。
かつ、この制度は用途を業務勉強のみに限っていないところが肝なんじゃないかなと感じました。
技術書を買って勉強する合間に、サブスクで映画を見てリフレッシュしたり、楽しみにしていた小説の新刊を読むとか、そういう余地があることで、業務も頑張れる。
ワークライフバランスという言葉自体はあまり好みませんが、結果的にそこに寄与する制度なんじゃないかなと思いました。

さて、そんなわけで、こういったユニークでありがたい制度もありつつ、勉強会なども盛んなUUUMのシステムユニットではエンジニアを募集しています。
気になる方はぜひ。 www.wantedly.com

【GAS】GoogleDrive内のcsvファイルをスプレッドシートに一括インポートする

こんにちは、UUUMシステムユニットの sawada_y です。 第2回オンラインハッカソンにて、シェルスクリプトを使用して月次作業を自動化したことについて発表させていただきました。その後、別の月次作業においてもGASを使用して自動化することができましたので、そのことについて書きたいと思います。

はじめに

担当業務の中で月次でGoogleDriveにあるcsvファイル(50~60位)をスプレッドシートに手動でアップロードするという、とても面倒な月次作業がありました。そこで、GAS(Google Apps Script)を使用してGoogleDriveにある複数のcsvファイルをスプレッドシートに一括でインポートすることができたので、そのことについて書きたいと思います。※GASの環境構築についてはまとめて下さっている記事が多数あったので、省略します。

自動化する前の作業内容

Google Driveに格納されている複数のcsvファイルをスプレッドシートに手作業で1つ1つインポートする作業を月次で行なっておりました。csvファイルの種類については最近になって増えてきて、その数50~60ファイル程度になっています。ポチポチと1つ1つファイルを選択して[データをインポート]を押すというイメージです。

これは面倒。。。

ということでこの作業を自動化することにしました。

自動化した後の結果イメージ

GoogleDriveの指定フォルダにcsvファイルを格納しておいた状態で

スクリプトを実行すると、複数のcsvファイルがスプレッドシートに一括でインポートされます。

要件

結果をイメージできたところで、具体的に要件を整理します。

  • Google Driveに格納された複数のcsvファイルを該当のスプレッドシートに一括でインポートしたい

  • 1つのファイルの内容が1つのシートに反映されるようにする。(50ファイルあったら、合計50シートになる)

  • それぞれのファイルの内容が、特定のシート名(そのファイルの一部が名前となっている)のシートに反映されるようにする

    • ex)13tokyo_2202.csvなら13tokyoという名前のシートに、11saitama_2202.csvなら11saitamaという名前のシートに反映されるようにする。(細かいですが、前月のスプレッドシートのデータが残っていることを前提にそのデータをクリアにしてから反映するようにします)
  • 特定のシート名(そのファイルの一部が名前となっている)のシートがなければ、特定のシート名(そのファイルの一部が名前となっている)のシートを新たに作成しそこに反映されるようにする。

コード内容

上記の要件を満たした、GASで記述したスクリプトを以下に記載します。 初学者向けにコードの内容を書いたコメントアウトも細かめに記載しております(参考にさせて頂いた記事のコードを読み解くのに時間がかかってしまったので)。 このスクリプトを実行すると、[2.実行イメージ]のようにインポートされます。

// 書き込む対象のアクティブなスプレッドシート(スクリプトにバインドされているシート)を取得
let ss = SpreadsheetApp.getActiveSpreadsheet();

function Import() {
  // getFoldersByIdでフォルダを取得
  let folder = DriveApp.getFolderById("格納フォルダのID");
  
  // ドライブの指定フォルダの全てのファイルのコレクションを取得
  let files = folder.getFiles();
  // ()内の条件式がtrueの間、{}内の処理が実行
    // hasNextで次のファイルが存在していたらtrueを返す。全て処理をしていればfalseを返す(未処理の場合、true。処理が終わればfalseとなる。)
  while (files.hasNext()){
  // nextで未処理の次のファイルを取得
  let file = files.next();
  // ファイル名を取得
  let fileName = file.getName();
  // _で区切って配列にする
  let fileNames = fileName.split('_');
  // 0番目から最後の要素以外をアンダーバーでくっつけたシート名にする。(13tokyo_2202.csvというファイル名なら13tokyoが返ってくる。)
  let sheetName = fileNames.slice(0,fileNames.length - 1).join('_');

  // ファイル名から抽出したシート名のシートがあるか判定
  let sh = ss.getSheetByName(sheetName);
    if(sh == null)
    {
      // なければ、引数の名前の新しいシートを挿入し、取得
      ss.insertSheet(sheetName);
      sh = ss.getSheetByName(sheetName);
    }
    else
    {
      // あればシートの値や数式や書式をクリアする
      sh.clear()
    }

  // getBlobでBlobオブジェクトとして取得(Blobとはデータの内容を操作したり、データを交換する為のオブジェクト)
  // getDataAsStringでブロブをエンコーディング。charset(文字コード)による文字列として取得
  let data = file.getBlob().getDataAsString("UTF-8");
  // 文字列データcsvを区切り文字 delimiterで分割して二次元配列を取得
  let csv = Utilities.parseCsv(data);

  // セルA1からCSVの内容を書き込む
  // getRangeで書き込む範囲を指定(開始セルは1行目の1列目、書き込み行数は配列数、書き込み列数は1次元配列の要素数)
  // setValuesは配列を対象のセル範囲に入力するメソッド
  sh.getRange(1,1,csv.length,csv[0].length).setValues(csv);
  }
}

コードの細かい内容はコメントに書いておりますが、ざっと以下のことを行っています。

  • 書き込むスプレッドシートを取得

  • csvが格納されているフォルダを取得し、そのフォルダ内の全てのファイルを順番に処理していく

  • ファイル名から一部抽出したものをシート名とする。

    • 13tokyo_2202.csvなら13tokyo、11saitama_2202.csvなら11saitamaをシート名として抽出。また、13_tokyo_2202.csvと途中で (ハイフン)がついていても 2202だけ削除し13_tokyoだけ抽出できるようにしている
  • 抽出したシート名のシートが既にあればクリアしてから、csvの内容を書き込む

  • 抽出したシート名のシートがなければ、その名前で新しいシートを作成し、csvの内容を書き込む

最後に

簡単なスクリプトにはなりますが、月次でインポートする作業を手動で行う手間と間違うリスクを少しばかり削減できたので、やって良かったなと思いました。ただ一旦インポートできた!という段階なので、より使用・管理しやすいように今後改良していきたいと思います。

参考にしたもの

(Udemyの動画と書籍については、自己研鑽補助の制度を利用し購入しました。)