コード日進月歩

しんくうの技術的な小話、メモ、つれづれ、など

Factory Botで数百を超えるレコードを作る場合はbuild_listとinsert_allと組み合わせると軽快に生成できる

すごく助かったのでメモがてら

環境

$ rails -v
Rails 7.1.2
factory_bot (6.4.4)
  activesupport (>= 5.0.0)
factory_bot_rails (6.4.2)
  factory_bot (~> 6.4)
  railties (>= 5.0.0)

やり方について

今回の事例

下記のように1000レコードを作成するようなテストを書く場合を想定する。

RSpec.describe SoccerMember, type: :model do
  context "1000人のメンバーが作成されたとき" do
    let!(:team) { create(:soccer_team) }
    before do
      ## この部分で SoccerMember を1000レコード分作る ##
    end
    it "正しい順番でならんでいること" do
      ## 略 ##
    end
  end
end

素直にcreate_listを使う場合と課題点

factoryBotにはcreate_listという機能があるので、素直に使うと以下のようになる。

RSpec.describe SoccerMember, type: :model do
  context "1000人のメンバーが作成されたとき" do
    let!(:team) { create(:soccer_team) }
    before do
      create_list(:soccer_member, 1000, soccer_team: team)
    end
    it "正しい順番でならんでいること" do
      ## 略 ##
    end
  end
end

ただし、この場合内部的には SoccerMember.create が1000回繰り返されてしまうため、SQLのINSERTが1000回実行することになるのでパフォーマンスとしてはよろしくはない。

大体案としての build_list + insert_all

やりたいことの内訳としては

  1. FactoryBotに定義したカラムの内容を生成してもらう
  2. それをRDB上にレコードとしてつくる

というような内訳になるので、1と2の作業を分離する。

1. カラムの内容生成

1の作業としてbuild_listでオブジェクトをつくる。

active_record_list = build_list(:soccer_member, 1000,
                                soccer_team: team,
                                created_at: Time.zone.now,
                                updated_at: Time.zone.now)

2. RDB上にレコードを作成する

2の作業として、insert_all で実行を行う。insert_allはActiveRecordのオブジェクトではなく、attributesのhashの配列がほしいので変換してあげる。

attributes_array = active_record_list.each_with_object([]) do |soccer_member , result_array|
  attribute_hash = soccer_member.attributes
  result_array.push(attribute_hash)
end

SoccerMember.insert_all(attributes_array)

もっと簡潔に書くと以下のようにも書ける

SoccerMember.insert_all(active_record_list.map(&:attributes))

注意点として、ActiveRecord独自のattributesを増やしているとそれも .attributes で取り出すときに含まれてしまうので注意。その場合はhashから取り除く必要がある。

実行時間の比較

比較として下記のように実行してみる。

RSpec.describe SoccerMember, type: :model do
  context "1000人のメンバーが作成されたとき" do
    let!(:team) { create(:soccer_team) }
    before do
      start_time = Time.zone.now.to_f

      create_list(:soccer_member, 1000, soccer_team: team)

      end_time = Time.zone.now.to_f
      elapsed_time = (end_time - start_time) * 1000
      puts "create_listのみ          生成時間: #{elapsed_time}ミリ秒"
    end
    it "正しい順番でならんでいること" do
      ## 略 ##
    end
  end

  context "1000人のメンバーが作成されたとき" do
    let!(:team) { create(:soccer_team) }
    before do
      start_time = Time.zone.now.to_f

      now_time = Time.zone.now
      # NOTE: 明示的にcreated_atとupdated_atを入れてあげないとnilになってしまってエラーになるので入れる
      active_record_list = build_list(:soccer_member, 1000,
                                      soccer_team: team,
                                      created_at: now_time,
                                      updated_at: now_time
      )
      SoccerMember.insert_all(active_record_list.map(&:attributes))

      end_time = Time.zone.now.to_f
      elapsed_time = (end_time - start_time) * 1000
      puts "build_list + insert_all 生成時間: #{elapsed_time}ミリ秒"
    end
    it "正しい順番でならんでいること" do
      ## 略 ##
    end
  end
end

そうなると結果は以下のようになり、だいぶ短縮された。

create_listのみ         生成時間: 2795.0141429901123ミリ秒
build_list + insert_all 生成時間: 140.3510570526123ミリ秒

参考リンク

CSSの詳細度(Specificity)に関してざっくりまとめる

いつも仕様を忘れてしまうので自身のメモとしてざっくりまとめる

仕様について

Selectors Level 4 - 17. Calculating a selector’s specificity

詳細度と他の要素の関係

詳細度以前に順番を決めるルールが存在するが、それは別の記事で記載したのでそちらを参考のこと。

2024年1月時点のCSS記述の採用順番についてざっくり理解する - コード日進月歩

ルールについて

以下の内容で決まる

詳細度のA,B,Cのカウントで勝る内容(A,B,Cに関しては後述) 1. Aの数を比べて数値が高い方 2. Aが同じ場合、Bの数値が高い方 3. AもBも同じ場合、Cの数値が高い方 4. AもBもCも同じ場合、記述がファイルのあとにある方

詳細度には3つのカウント数値をもとに決める。

カウンタ名称 何をカウントするか
A (ID列) 「IDセレクタ(ex.#example)」のカウント
B (CLASS列) 「クラスセレクタ(ex..example)」「属性セレクタ(ex.[name="example"])」「擬似クラス(ex.:hover)(一部除く※1)」のカウント
C (TYPE列) 「要素型セレクタ(ex.div)」「疑似要素(ex. ::before)」

※1 :where() は記載してもカウントは増えず,:not().:has(),:is() は引数の要素でもっとも高い詳細度のカウントを+1する。

このABCは (A, B, C)A-B-Cという表現をする。

<div>
  <p name="example" id="first">
    sample text
  </p>
</div>

というHTMLがあった場合に、以下のCSSはすべて上記のDOMには適用されるが、color に関しては詳細度が一番高いものが適用されるので、文字色は緑になる。

div p  {  /* 詳細度は(0,0,2) */
  color: red;
}

div p#first {  /* 詳細度は(1,0,2) そのため他3つよりもこれが優先 */
  color: green;
}

p#first{ /* 詳細度は(1,0,1) */
  color: orange;
}

div p[name="example"] { /* 詳細度は(0,1,2) */
  color: yellow;
}

参考リンク

2024年1月時点のCSS記述の採用順番についてざっくり理解する

import! がどうやって適応されていくのかの原理を理解する。

出典元

2024年時点だとLevel4の資料がベースになるのでそれを今回をベースに記載する。

CSS Cascading and Inheritance Level 4

どこの記述が利用されるかを決める "Cascade Sorting Order"

CSSのどの記述が採用されるかはLevel4では大きく4つのパートに分けており、1のルールから順番に適用されていく。

  1. Origin and Importance
  2. Context
  3. Specificity
  4. Order of Appearance

Origin and Importance(Originに関してとimportかどうか)

最初に適用順番が決まるのがこの Origin and Importance のルール。このルールを把握するためにまず、 OriginImportance という概念に関して理解する必要がある。

Origin

Originはおおまかに言うとCSSが記述されている箇所の話であり、3つに区分けされている。

  • Author
    • HTMLやCSSファイルに書かれた領域のStyleの記述。普段から読み書きしているのはこの領域。
  • UserAgent
    • ブラウザが元から備える領域のStyle記述。リセットCSSなどで無効化される記述の部分などがそれにあたる。
  • User
    • ブラウザを利用するユーザが設定する領域のStyle記述。ブラウザ側によって様々だが、例としてブラウザが提供する拡張機能などに記載のあるStyleなどが挙げられる。

Importance

もう一つの概念として Importance がある。これは !important がついている記述かどうかの観点。ついていないものを normal と呼称し、ついているものを important という。

決定順番

優先順 優先されるもの
1 Transactionに関する記述
2 UserAgent の領域で important な記述
3 User の領域で important な記述
4 Author の領域で important な記述
5 Animationに関する記述
6 Author の領域で normal な記述
7 User の領域で normal な記述
8 UserAgent の領域で normal な記述

この優先順の上で、同列の存在に関しては次以降の優先順位決定ロジックが適用されていく。

Context

こちらはカプセル化された内側要素(たとえばShadow DOM)と比較した場合にどちらの記述を優先するかという話を定義している。

前項であげた importantnormal かで決まるルールとなっており…

  • important の場合は内側の記述が優先される
  • normal の場合は外側の記述が優先される。

分かりづらい話ではあるが、Shadow DOM採用時の例で整理すると、通常のDOM(Light DOM)が外側、Shadow DOMが内側となり、Shadow DOM側に !important があった場合は、外側であるLight DOMも影響を受ける、以下のサイトでは実例のコードも乗っているので参考のこと。

Cascading and Inheritance Level 4ではContextによるルールの優先付けが定義されており、normalな(!importantではない)ルールではShadow DOMの中よりも外が優先される一方で、!importantはルールではShadow DOMの外よりも中が優先されると定義されている。 - Shadow DOMによりスタイルが閉じ込められる仕組み

Specificity(詳細度)

この詳細度は普段から利用されるものなので、本説明では割愛する。

Order of Appearance(styleの登場順番)

styleの記述が重複している場合は、後に登場する記述が優先(後勝ち)される

挙動のまとめ

下記の順番で決まる。

  1. Origin and Importance
  2. Context
  3. Specificity
  4. Order of Appearance

1の中で優先度が同じルールのものは2のルールへ 2の比較をしてもでも同列のものは3の詳細度の高い順で 3の詳細度で最優先値のものが2つ並んだ場合は4のルールである登場順の後ろ

上記のような考え方で決まる。

補足 Level5とLevel6について

2024年2月時点ではLevel5とLevel6のルールも存在する

Level5の順番について

執筆時点でのpusblishedな情報だと以下の通り

  1. Origin and Importance
  2. Context
  3. [Level5での追加要素] Element-Attached Styles
  4. [Level5での追加要素] Layers
  5. Specificity
  6. Order of Appearance

2つ要素が追加されている。

  • Element-Attached Styles は インラインstyleであればそちらを優先するというもの。header要素に <style> を宣言したり、<p style="margin: 10px"> のような記述はCSSの詳細度判定以前に優先されるルール。この実装に関しては現時点でも多く行われている。
  • Layers@layerという新しい記述方法で優先されるものを切り分ける方法。詳しいことはここでは割愛する。Layerは2023年4月ごろには主要なブラウザには対応している。またトランスパイルも提供されている。

Level6の順番について

こちらに関しては2024年時点では絶賛議論中だが、2024年2月時点最新のEditors Draftによるとだと以下のようになりそう。

  1. Origin and Importance
  2. Context
  3. The Style Attribute(Element-Attached Stylesと内容は同じ)
  4. Layers
  5. Specificity
  6. [Level6での追加要素] Scoping Proximity
  7. Order of Appearance

  8. Scoping Proximity@scope を使うことで影響の及ぼす範囲を限定できるようにするもの。こちらも詳しいことは割愛する。当初Specificityより先に判断するか後に判断するかが議論されていたが、最終的にはSpecificityより後の順番になりそうな状況。

参考リンク

オブジェクト指向CSS(OOCSS)とは何なのかをざっくりまとめる

提唱元のスライド

OOCSSが提唱された2009年頃のCSS事情

OOCSSが提唱された2009年頃は、再利用性を鑑みたCSSは少なく、ページ単位でCSSを置くことが多い時代でした。またSassなども本格的に流行する前だったため、動的に何かを生成するというようなアプローチも確立されていない頃でした。

当時を知る手がかりとして2009年当時の書籍WebDesigningでのCSS特集のサンプルコードサイトがあります。

Web Designing: 2009年11月号

構造として、登場するDOM要素ごとにCSSが記述されており、 「header.sitewide .util li」というような作りになっていたので、DOM要素とclassの組み合わせで見栄えを決めるということが主流だったことが読み取れます。

OOCSSが解決したかった課題

2009年頃のCSSはパーツごとにCSSを書かなければならない状況であり、資料などを読み解くと以下の問題があったようでした。

  • CSSが表示するDOM要素それぞれにフィットするように個別に作られているので再利用性がない
  • CSSが肥大化しているためWebサイトの表示スピードが遅くなる

そのため、この課題に対して解決する考え方としてOOCSSが提唱したと考えられます。ではOOCSSはどのようなアプローチでこの問題に取り掛かったのでしょうか?

OOCSSの考え方

OOCSSはCSSとそこに関連するHTMLやJSを「CSS Object」と捉えて、Webページ毎につくるものではなく、抽象化して再利用可能なパーツとして考える考え方です。そのため具体的な記述ルールなどがあるわけではなく、CSSやHTMLを設計するための考え方となっています。

再利用性を念頭に置くことで、重複する記述の削除と無駄なコードの削減を狙ったのではないか、と考えられます。そしてこの再利用性を実現するために大きく2つの原則を提唱しています。

OOCSSを構成する2つの原則

原則1. ストラクチャ(構造)とスキンの分離

Webページについて、構成物パーツを形成する「ストラクチャ」と、ストラクチャに対しての見栄えを制御する「スキン」に分けてつくるという考え方です。

例えば、Webページ全体で使う "送信ボタン" , "キャンセルボタン" , "いいねボタン" があったときに、「ボタンストラクチャ」と「送信スキン」「キャンセルスキン」「いいねスキン」というような形式で分離をするというようなアプローチです。

原則2. コンテナとコンテンツを分離する

ここで言う「コンテンツ」は文章や画像などのWebページの構成パーツのことで、「コンテナ」はそれを束ねる要素のことを指しています。その2つを分離するのは、依存関係をなくすということを指しています。

例としてよくあるのはページの先頭の"ページヘッダ"がコンテナとして存在し、フッタ内に配置される"ボタンアイコン" がコンテンツとして存在するときの例です。この"ボタンアイコン"が"ページヘッダ"の特定の場所に配置することを前提としたCSS記述にすると、もし”ページフッタ"で使いたくなったときに見た目の部分は同じ"ボタンアイコン"に関する記述をページフッタ専用で書き直す必要が出てきてしまいます。そのようなことを回避するためにコンテナとコンテンツを分離するべき、というアプローチです。

どう「オブジェクト志向」を取り込んだのか

2009年当時の話でもふれたとおり、CSSの作りが作るページ単位で書き下ろしをし、またサイト内で共通化するということもしづらかった状態が当時あったことかと推察されます。

ではオブジェクト指向のどのあたりを取り込んだのがOOCSSなのでしょうか。提唱者のGitHubを見ると以下のような記述があります。

Basically, a CSS “object” is a repeating visual pattern, that can be abstracted into an independent snippet of HTML, CSS, and possibly JavaScript. That object can then be reused throughout a site. - Home · stubbornella/oocss Wiki

和訳すると以下のようになります。

基本的に、CSSの "Object"とは、繰り返される視覚的なパターンのことで、HTML・CSS・場合によってはJavaScriptスニペットとして独立した抽象化をすることができる。このObjectは、サイト全体で再利用することができます。

また、上記の CSS Objectの構成要素として以下のような内容も上げています。

A CSS object consists of four things:

  1. HTML, which can be one or more nodes of the DOM,
  2. CSS declarations about the style of those nodes all of which begin with the class name of the wrapper node
  3. Components like background images and sprites required for display, and
  4. JavaScript behaviors, listeners, or methods associated with the object.

This can be confusing because each CSS class is not necessarily an object in its own right, but can be a property of a wrapper class. - FAQ · stubbornella/oocss Wiki

和訳すると以下

CSS Objectは以下の4つから構成される:

  1. HTML、DOMの1つまたは複数のノード。
  2. CSS宣言、これらのノードのスタイルに関するものでラッパー・ノードのクラス名で始まるもの。
  3. コンポーネント、例えば表示に必要な背景画像やスプライトなど。
  4. オブジェクトに関連するJavaScript、ビヘイビア・リスナー・メソッドなど。

CSSクラスは、それ自体がオブジェクトであるとは限らず、ラッパークラスのプロパティであることもあるので、これは混乱するかもしれません。

このような例示をしているので、OOCSSで提唱したかったものはDOMそのものをふくめたコンポーネントそのもののことを指しているように読み取れます。

スライドでもWebページの構成をレゴで組み立てる話になぞらえて説明をしているので、オブジェクト指向の考え方の構造的な面でのメリットである「部品化」や「再利用性」というところをCSSをライティングする人々の世界に取り込みたかったのかなと考えられます。

参考リンク

RFC8058ワンクリック購読解除の仕様に関してざっくりまとめる

headerを設定すると何が起きるのか整理する

出典となるRFC

先にまとめ

RFC2369で、メーラーに関してそのメールの購読解除の案内をするヘッダとして List-Unsubscribe ヘッダの仕様がある。このヘッダはメールアドレスを指定しておくとその宛先にメールを送る、URLを指定するとそのページに遷移するというような挙動をサポートするヘッダ。 RFC8058でList-Unsubscribeと合わせて、List-Unsubscribe-Post を利用すると、指定したURIにPostリクエストを送信する仕様が定義されている。

ヘッダについて

List-Unsubscribeヘッダ

RFC2369にて定義されている設定。ユーザが対象のメールの購読を解除するためのアクションを設定する。値として例示されているのは大きくmailtoの形式と、URLの形式。

▼mailtoの形式

List-Unsubscribe: <mailto:list@example.com?subject=unsubscribe>

▼URLの形式

List-Unsubscribe: <http://example.com/list.cgi?cmd=unsub&lst=list>

なお複数の形式も設定することもできる

List-Unsubscribe: <http://example.com/list.cgi?cmd=unsub&lst=list>,<mailto:list@example.com?subject=unsubscribe>

List-Unsubscribe-Postヘッダ

RFC8058に定義されている設定。List-Unsubscribe とセットで利用するヘッダで、セットで送ることでList-UnsubscribeにPostリクエストでワンクリック登録解除が実現できることを示す。なお、フォーマットは以下の通り。

List-Unsubscribe-Post: List-Unsubscribe=One-Click

なお、List-Unsubscribe=One-Click 以外の書き方は現時点では記述が存在しないので書き方はこの一択。

挙動について

RFC8058の挙動に関して意図通りにサポートしていることが明確なのはGmailだが、他のメーラーに関しては対応状況はまちまちの様子である。なかでもOutlookに関しては List-Unsubscribe にURLしかない場合には意味がないものとみなすことがあるとのこと(詳しくは参考リンク参照のこと)

参考リンク

has_manyでorderを設定したものを無視したいときはreorderを使う

設定しているときに無視したいという特殊なケースの対策。基本は打ち消しをしないことを前提に書いたほうがよいが書かざるを得ないときにどうするかのためのメモ。

環境

$ bin/rails -v
Rails 7.1.2

前提

has_manyなどの子の関連に関して、デフォルトの並び順を設定することができる。詳しくは以下の記事を参照のこと。

RailsのActiveRecordのhas_manyにはorderで順番の指定できる - コード日進月歩

例えば以下のようなModelがあったとする

# == Schema Information
#
# Table name: soccer_teams
#
#  id         :bigint           not null, primary key
#  name       :string(255)
#  created_at :datetime         not null
#  updated_at :datetime         not null
#
class SoccerTeam < ApplicationRecord
  has_many :soccer_member, -> { order(:name) }
end
# == Schema Information
#
# Table name: soccer_members
#
#  id             :bigint           not null, primary key
#  name           :string(255)
#  soccer_team_id :bigint           not null
#  created_at     :datetime         not null
#  updated_at     :datetime         not null
#
class SoccerMember < ApplicationRecord
  belongs_to :soccer_team
end

普通に SoccerTeam がhas_manyで持っている SoccerMember を取得しようとすると以下のようになる。

SoccerTeam.first.soccer_member.to_sql
# "SELECT `soccer_members`.* FROM `soccer_members` WHERE `soccer_members`.`soccer_team_id` = 5 ORDER BY `soccer_members`.`name` ASC"

例えばこれをid順にしたいと思ったときに下記のようにしても、nameのorderが優先されてしまう

SoccerTeam.first.soccer_member.order(:id).to_sql
# "SELECT `soccer_members`.* FROM `soccer_members` WHERE `soccer_members`.`soccer_team_id` = 5 ORDER BY `soccer_members`.`name` ASC, `soccer_members`.`id` ASC"

これを回避するためには reorder を利用する。

SoccerTeam.first.soccer_member.reorder(:id).to_sql
"SELECT `soccer_members`.* FROM `soccer_members` WHERE `soccer_members`.`soccer_team_id` = 5 ORDER BY `soccer_members`.`id` ASC"

関連リンク

Railsプロダクトを考古学するときに見ると良いポイントを雑に書き出す

最初から自分が関わっていないプロダクトコードを探索する記事を書いたのですが、そこからRails特有のTipsを書き留めておきます。

考古学するときに見るといいポイント

routesを常に見れる状態にする

Webサービスをやっているので、大概の要望は「◯◯というページの内容を変えたい」など、どこかのページをどうにかしたいという話から始まります。そのため、そのWebページがどのように処理されているかをというところが肝要という話になります。毎度 bin/rails routes して探すのもアリですし、私の場合は bin/rails routes した結果を手元に残しておいて検索をかけて使います。( annotate のgemを入れいてるのであれば、 annotate --routes でroutesにもコメントを吐き出してくれるのでそちらを閲覧するのもアリです。)

メソッドが増えるタイプのgemがあればそれを頭に入れる

「このメソッドどこに定義があるんだ…?」と思ったらgemが生やしている、というケースはたまにあります。そういうgemがどれだけあるかを頭に入れておくだけで考古学がはかどることがあります。自分がよく知るものだと以下です。

Viewの作り方の質感を整理する

ModelやControllerは少し見ると方向性がつかみやすいのですが、Viewは作り方の方向性が色々あるので、どういうコンセプトなのか見ていくと良いと思っています。確認ポイントは以下。

  • JavaScript関連はどのような使い方をしているか
    • webpackerなのかwebpackなのか、VueやReactを使っているのか、古いものであればturbolinkをつかっているのかいないのか、など。
  • テンプレートエンジンは何を使っているか
    • erbじゃなくてslimやhamlを使っているケースもあれば、混ざっていたりとか
  • インスタンス変数の取り扱い方
    • partialにもインスタンス変数取り扱ったり、ぼっち演算子でカバーするのかifでカバーするのか、などインスタンス変数起点でそのプロダクトの王道がありそうであれば把握します。

挙動がよくわからなかったらテストコードを書く

ロジック面で難解な処理があれば、テストコードを書いちゃうのが一番効率がいいので書くのが早いです。View側も可能であればControllerまで処理を移植してControllerSpecを書くと挙動理解が早いです。もしページに来るべきパラメータがわからない、などであれば一旦Railsデバッグログからパラメータを拾い上げるなどやるとショートカットできます。

考古学するときのポイント

一番はどうやってコードが伸びてきたのか、というところに思いを馳せると良いと思っています。「このコードを書いている人は既存のコードベースを参考にして書いているのかな?」とか「この人はどちらかというと責務の比重をフロントエンドに置こうとしているのかな?」とPullRequestやAuthorの名前を見ながら考えると、どのような成長の仕方をしたか、当時どういう思いで書いていたかというのがなんとなく見えてきます。

余談ですが、自分の価値観に沿わないコードを見ると憤りの気持ちが募ることもありますが、コードを考古学する本質は「なんでこうなったか」を把握して、現状のあるべき論と照らし合わせたときに誤った方向にいかないようにすることなので、憤ったときはその憤りの根源を整理して後の改善案と結び付けられるように整理できるとよいと思います。

関連リンク