ガラシのパルプンテ頼み

地方産限界エンジニアのグローバル独り言

【初心者向け】CSSセレクタとは?様々な要素の指定方法 | CSS入門

CSSのセレクタとは何か?

CSSはWebページを装飾するためのスタイルシート言語です。CSSセレクタは、要素のスタイルを指定するための特定のHTMLタグ、またはクラスを指定する際に用いられます。

まずは本題であるCSSセレクタの説明を行う前に、前提となるHTMLおよびCSSの知識について復習していきましょう。

HTMLの構造

CSSおよびCSSセレクタを理解する前に、まずはHTMLの構造について理解しておく必要があります。

HTMLはタグと呼ばれるそれぞれが意味を持った記号によって構成されています。 タグには開始タグ終了タグが存在し、文章をタグで囲むことにより特定のルールに沿った文章構造を定義することができます。 またこの、HTMLタグで囲われた情報の単位のことを「要素」といいます。

さらに、この要素自身もタグで囲むことも可能です。 この場合、二つの要素は階層構造となり、外側にある要素を親要素、内側にある要素を子要素と呼びます。 子要素の中にさらに要素を配置した場合は、さらに一階層下に孫要素を定義したこととなります。

このようにHTMLは要素の階層を重ねていくことで、親の子、そのまた孫...というように子孫要素を内包することができます。

HTMLタグの属性

前段でHTMLタグはそれぞれが意味を持った記号であると説明しました。HTMLタグはその種類に応じて様々な設定を与えることが出来ます。 このそれぞれのHTMLタグに与えられる設定のことを属性、設定の内容のことを属性値と呼びます。

与えられる属性とその値はタグごとにそれぞれ異なります。 例えば以下はid属性headerという属性値を与えたdiv要素の例です。

<div id="header">
  <h1>こんにちは、世界!</h1>
</div>

また以下はリンクを定義するaタグに、リンク先を指定に用いるhref属性、属性値にリンク先のURLを設定した例です。

<a href="https://www.google.com/">Googleトップページ</a>

このようにHTMLタグごとに与えられる属性とその属性値が異なることがわかっていただけたかと思います。 CSSはこれらの要素、階層、属性、属性値を指定して任意の装飾を行なっていきます。

CSSの基本文法

CSSは「セレクタ」「プロパティ」「値」の3つから構成されます。 CSSファイルではそれぞれを以下のような対応で記述することでスタイリングを行なっていきます。

  • セレクタ = HTMLのどの要素に
  • プロパティ = 何を
  • 値 = どのようにスタイリングする
p {
  color: red;
}

上記の例はそれぞれ以下のような対応でpタグの文字色を赤色に変更しています。

  • セレクタ = pタグの
  • プロパティ = 文字色を
  • 値 = 赤色にする

基本的なCSSセレクタについて

ここからは具体例を挙げながら実際にCSSセレクタによりHTMLの要素を指定してスタイルを当てる方法について見てみようと思います。

例えば、次のHTMLコードがあるとします。

<div class="box">
  <h1 id="title">こんにちは、世界!</h1>
  <p>これはCSSのセレクタについての説明です。</p>
</div>

このコードを使って、いくつかのセレクタを見てみましょう。

1. タグセレクタ

h1 {
  color: red;
}

このセレクタは、h1タグに対してスタイルを適用します。この場合、h1タグ内のテキストの色が赤に変わります。

2. クラスセレクタ

.box {
  background-color: yellow;
}

このセレクタは、class属性がboxである要素に対してスタイルを適用します。この場合、div要素の背景色が黄色に変わります。 また、CSSでクラスセレクタにアクセスする場合は「.(ドット)」に続けてクラス名を記述することで指定が可能です。

3. 子孫セレクタ

.box p {
  font-size: 18px;
}

このセレクタは、.boxクラス内のp要素にスタイルを適用します。この場合、p要素内のテキストのフォントサイズが18pxに変わります。 スタイルが適用される範囲はboxクラス内に存在するp要素のみとなります。

4. IDセレクタ

#title {
  text-align: center;
}

このセレクタは、id属性がtitleである要素に対してスタイルを適用します。この場合、h1要素内のテキストが中央揃えに変わります。 また、CSSでIDセレクタにアクセスする場合は「#(シャープ)」に続けてID名を記述することで指定が可能です。

5. 属性セレクタ

<a href="https://example.com" target="_blank">Link to Example Site</a>
a[target="_blank"] {
  color: blue;
}

このセレクタは、target属性がblankであるa要素に対してスタイルを適用します。この場合、リンクのテキストの色が青に変わります。 ちなみにtarget属性とはリンク先の文書を開くウインドウを指定するための属性です。属性値としてblankを指定することで、クリックしたリンク先のページをブラウザの別タブで展開します。

以上がいくつかのCSSセレクタの例です。 セレクタを使って要素を選択し、スタイルを適用することで、Webページをより魅力的に装飾することができます。

M1Mac + Docker環境でGroverを用いたPDF生成をするまでがまじでしんどかった話 | Rails外伝

概要

実務にて特定のwebページのPDF化およびダウンロード機能を実装することとなり、いくつかあるライブラリの中から今回は「Grover」というGemを採用することとなりました。

導入に差し当たり公式のドキュメントを参照しながらDockerで構築済みのローカル環境にGemをインストールし、ドキュメントの内容に沿って動作を確認していく際、PDFの生成に至るまでにかなりハマってしまったためその備忘録として本記事を残します。

まず具体的な対応手順の前に、前提条件となる環境や今回関連するライブラリ群に関する説明をそれぞれ簡単に行っていきます。

開発環境

macOS BigSur ver 11.6

MacBook Air (M1, 2020)

Docker Desktop on Mac(Mac with Apple chip)ver 4.2.0

Groverとは

後述するNode.js製のブラウザ自動操作ライブラリであるPuppeteerの操作をRubyライクで書けるようにしたGemで Groverはざっくりと以下のようなことを行います。

  • アプリケーションで生成したHTML文字列をPuppeteerに受け渡す
  • Puppeteerに対して、出力するファイル形式やスタイルの設定やメディアタイプ等の指定

github.com

Puppeteerとは

端的に表現するとHeadlessChromeを利用するためのブラウザ自動操作ライブラリで、類似ライブラリとしてはCapybaraやselenium-webdriverなどがあります。

Google製のWebブラウザであるChromeは、画面を持たずコマンドラインやリモートデバッグ機能を通じてWebブラウザを操作できるHeadlessChrome機能を備えており、PuppeteerはこのHeadlessChromeをサーバサイドで実行されるNode.jsのJavaScriptから自動操作できるようにしたライブラリです。

Puppeteerを利用するとユーザーがWebブラウザをマウスやキーボードで操作することなく、プログラムから特定のWebページを読み込んだり、画面キャプチャを取得したり、指定された場所をクリックし、値を入力し結果を取得するといった操作を自動的に行わせることが可能となります。

github.com

Grover, Puppeteer, Headless Chromeそれぞれの役割

それぞれの役割と関係性は以下の通りです。

ライブラリ名 役割
Grover Puppeteerで実行可能なブラウザの自動操作オプションをRubyから実行できるようにする
Puppeteer Groverから渡ってきた命令を元にHeadlessChromeを操作
HeadlessChrome 渡ってきたHTMLを描画してPDF、PNG、JPG等のファイルを生成する

今回のハマりポイント

M1 Mac上のDockerでGoogle Chromeが動作しない

ここまでは前提となるGrover、Puppeteer、HeadlessChromeの役割と関係性を整理してきました。

次に今回自分が沼ったポイントについて。Docker Desktop on Mac(Mac with Apple chip)で作成したローカル環境で、Groverを使うにあたり一番ネックとなった問題。それはPuppeteerのインストール時にバンドルされるGoogle Chromeでした。

具体的なエラーの内容としては以下の通り。Groverの公式ドキュメントに沿ってyarnからPuppeteerのインストールを実行したところ、以下のエラーが発生しPuppeteerの導入が行えない状況に直面しました。

The chromium binary is not available for arm64:
If you are on Ubuntu, you can install with:

 apt-get install chromium-browser

Chromiumのバイナリはarm64では利用できないため、もしUbuntuを使っているならaptでChromiumベースのブラウザをインストールして欲しいとのこと。

エラーの原因についてはあくまで推察となってしまうのですが、実装当時(2022年11月時点)そして本記事執筆時点でもどうやらarm64 x Linux向けのGoogle Chromeが存在しておらず、Puppeteerのインストール時にバンドルされてきた、x_86向けのLinux用Google Chromeのインストールに失敗したことからエラーが発生したのではないかと考えています。

そこで回避策として、Dockerのベースイメージに内包されているLinuxディストリビューションのパッケージマネージャーから直接Chromiumをインストールし、それをPuppeteerから利用する方針で対応することを考えました。

今回利用しているベースイメージに含まれているLinuxOSがDebianであったため、Dockerfileにapt経由でのChromiumのインストールコマンドを記述し、ビルド時にあらかじめChromiumをインストールするように設定します。利用しているLinuxOSの種類を確認したい場合、コンテナにログインした上で、以下のコマンドを実行することで確認が可能です。

# コンテナにログイン
$ docker exec -it <container name> bash

# OSの種類を確認
cat /etc/os-release

また、PuppeteerにはバンドルされたChromiumのインストールのスキップと、パッケージマネージャから別途インストールしてきたChromiumを使用するよう、設定を行う必要があります。 上記エラーに関する内容はGitHubでもIssueが上がっており、対応方法について議論が為されていました。 パッケージマネージャ経由でChromiumをインストールする方法はここから着想を得た形となります。

github.com

【2023-03-09 追記】

以下の記事にて、M1MacのDocker上でChromeが使用できない事象について、より詳細な原因や迂回案について綴られています。

blog.savanna.io

シチュエーションや環境としては本記事と類似しているため、「M1MacのDcokerでChromeが使用できない」というエラーの解決策を求めて本記事にたどり着いた方は、 上記記事を参考にしてみることをおすすめします。

具体的な対応手順

ここからは本題であるM1Mac + Docker環境でGroverを使ってPDFを生成を通すまでの具体的な手順を追っていきます。

1. Groverの追加

gemfileにGroverを追加してbundle installします。

gem 'grover'
$ bundle install

2. DcokerfileにPuppeteerのインストール設定とchromiumのインストールコマンドを追加

M1Mac + Docker(arm64 x Linux)環境下でPuppeteerを使用するため、Dockerfileに以下のコマンドを追加します。

※尚PuppeteerのインストールにはNode.jsのver 14.12.0以上が必要となります。

  # Puppeteerのインストール時にバンドルされているChromiumのダウンロードをスキップする
  ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

  # PuppeteerでパッケージマネージャからインストールしたChromiumを使用するように指定
  ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium

  # ディストリビューションのパッケージマネージャからChromiumをインストール
  RUN apt-get install -y chromium

3 コンテナにログインの元、yarn経由でPuppeteerをインストール

コンテナ情報の確認

$ docker container stats

コンテナにログイン

$ docker exec -it <container name> bash

コンテナログイン後にyarn経由でPuppeteerをインストール npmを使用している場合はnpmのパッケージインストールコマンドに置き換えてください。

# yarn add puppeteer

4 controllerとGroverのconfigファイルにPDF出力コードと共通設定を追加

各種controllerにPDFの出力コードを、configにGroverでのPDF出力に関する共通設定をそれぞれ追加し、PDF出力の疎通確認を実行します。

  • config
# config/initializers/grover.rb

Grover.configure do |config|
  config.options = {
    format: 'A4',
    margin: {
      top: '30px',
      right: '20px',
      left: '20px'
    },
    cache: false,
    display_url: <%= ENV.fetch("GROVER_DISPLAY_URL") {'localhost:3000' } %>
  }
end

display_url = 外部のCSSファイルを読み込む際、相対パスを補完するための自ホストのURLを設定するオプション。スタイルをHTMLにインラインで記述している場合には不要。

  • controller
def show
  # PDFの元となるHTML文字列を生成
  html = ApplicationController.new.render_to_string(template: 'sample/show')
  # Groverのインスタンスに生成したHTML文字列を渡してto_pdfメソッドによりPDF形式に変換
  pdf = Grover.new(html).to_pdf
  # send_dataでPDFファイルをクライアントに返却
  send_data(pdf, filename: 'sample.pdf', type: 'application/pdf', disposition: 'inline')
end

5. Headless Chromeのsandbox機能への対応

上記の手順まででDocker環境にGrover、Puppeteer、Chromeをインストールし、PDFを生成するための地盤は整いました。 しかしこのままPDF生成を実行するとおそらく以下のエラーが発生し、処理が落ちることが予想されます。

ERROR:zygote_host_impl_linux.cc(90)] Running as root without --no-sandbox is not supported

原因としてはPuppeteerから実行されたHeadlessChromeの内部的なタブで、sandboxが有効化されていることから、渡されたHTML文字列をセキュリティ的にブロックしてしまうためです。 解決するためにはPuppeteerからHeadless Chromeを起動する際に、オプションでsandbox機能を無効化した上で起動することを伝える必要があります。

github.com

GroverにはPuppeteerからChromeをsandbox無効状態で起動させるための方法が用意されているので、これに則って設定を行います。 方法としては二つあり、一つ目が環境変数を設定するもの。二つ目がGroverのinitializeファイルに設定するものです。以下のいずれかを行うことでエラーの回避が可能です。

  1. Dockerfileに以下を追加してビルドを実行の上、再度PDFの生成を行なってください。
# Puppeteerをサンドボックス無効状態で使用
ENV GROVER_NO_SANDBOX=true
  1. config/initializers/grover.rbに以下を追加
Grover.configure do |config|
  追加→ config.launch_args: ['--no-sandbox', '--disable-setuid-sandbox']
end

6. 日本語フォントの導入と文字化けへの対応

最後に、出力されたPDFを確認すると日本語部分が表示できていないことが確認できるかと思われます。 これはOSに日本語フォントが導入されていないためPDFにフォントの埋め込みが行えていないためです。別途OSに日本語フォントを導入していきます。

OSに日本語フォントを導入(Dockerfile記載)

# PDF生成に使用する日本語用フォントの追加
RUN apt-get -y install fonts-ipafont fonts-ipaexfont

もし日本語フォントの導入で文字化けが解消しない場合、利用しているtemplateに以下の記述が含まれていない可能性があります。

<meta http-equiv="content-language" content="ja" charset="utf-8">

Groverを選定した理由

最後に今回Groverを採用した理由について。

RailsでのPDF生成において、自分の観測範囲ではPrawnが最も代表的なGemなのではないかと思われます。 今回のGroverを選定した理由としては、PDF帳票という性質上レイアウトの変更を求められる可能性が高かったため、以下の観点からPrawnよりもGroverを利用したいと考えました。

  • PrawnはGem独自の記法、オプションを用いてPDFをレイアウトしていかなければならず、帳票の手直しを行う際に再度Prawnのドキュメントのインプットが必要となり、メンテナンスのコストが嵩む
  • Rubyの言語が読み書きできることが必須であるため、今後新規のメンバーなどがアサインした場合に誰でも・比較的容易にメンテナンスが行えるという観点でHTML/CSSをベースにレイアウトが変更できるGroverの方がメンテナンス性が高いと考えたため

同じ観点でHTMLからPDFを生成できるGemとしてwicked_pdfも老舗のGemとして知られていますが、こちらに関しては単純にGem自体の開発が停滞しており、今後使っていくにあたり不安要素が大きかったため、同じHTML to PDFで生成が可能で更新が活発なGroverを採用することとしました。

実際蓋を開けてみると上述の通り開発環境の構築時点で様々な課題が見つかりかなり大変な思いはしましたが、これに関してはM1Mac + Dockerという特定の環境下での事象かと思われますので、基本的には要件の性質やプロジェクトメンバーの知見などに合わせてPrawnまたはGroverを利用していくという考え方でいいのではないかと思います。

【初心者でも怖くない】結合とは?内部結合と外部結合の違い | データベース入門

はじめに

データベースやSQLについて学んでいくと必ずエンカウントするのが「内部結合」「外部結合」

結合という単語からも、どちらも複数のテーブルをまとめることであることは間違いありませんが、 「内部とか外部とか具体的な違いがわからない」「なんとなくは分かっているけど言語化して説明できるほどではない」 そんな初学者に向けて今回はそもそものテーブルの結合とはなんなのかという部分から掘り下げてみようと思います。

テーブルの結合とは

データベースからなんらかのデータを取得する場合「◯◯テーブルから☓☓データを取り出す」というSELECT文を用いますね。 そしてこのデータ取得は単一のテーブルのみを対象とする場合もあれば、複数のテーブルを対象に取得するケースも少なくありません。

例えば商品と仕入先マスタという二つのテーブルがあるとします。ある商品の仕入先名を知るために

  1. 商品テーブルから仕入先名を知りたい商品データを検索する
  2. 商品データが有する仕入先IDで仕入先マスタテーブルを検索する
  3. 2つの検索結果を足しわせる

という工程を毎回踏んでいては効率が良くありません。

そのため通常の検索の場合、以下のような方法をとります。

  1. 商品テーブルと仕入先マスタテーブルをまとめる
  2. まとめて一つとなったテーブルに対して検索

この「まとめる」作業のことを「結合」と呼び、その結合の種類として内部結合外部結合が存在しています。

内部結合とは

内部結合とは、結合条件に指定している値が両方のテーブルに存在するデータを抽出する結合のことです。

以下は商品テーブルと仕入先マスタテーブルを「商品.仕入先ID = 仕入先マスタ.ID」の条件で内部結合した結果です。

内部結合した結果、結合条件である「仕入先ID」が両方のテーブルに存在している商品ID=1と商品ID=2のデータが抽出されています。 内部結合は、SQLのINNER JOIN句もしくはWHERE句により実現することができます。

■INNER JOINを使った例

SELECT * 
FROM products 
INNER JOIN suppliers 
ON products.supplier_id = suppliers.id;

■WHEREを使った例

SELECT * 
FROM products, suppliers 
WHERE products.supplier_id = supplier.id;

(products = 商品テーブル suppliers = 仕入先マスタテーブル)

外部結合とは

外部結合とは、基準となるテーブルに存在すれば抽出する結合のことです。 どちらのテーブルを基準にするかを指定し、その基準となるテーブルに存在するデータを抽出、基準ではないテーブルからは抽出できるデータのみ取得します。

外部結合には基準にするテーブルに応じて「左外部結合」「右外部結合」「完全外部結合」の三つの種類が存在します。 これらについて順を追って説明します。

左外部結合とは(LEFT OUTER JOIN)

左側のテーブルを基準とするのが左外部結合(LEFT OUTER JOIN)です。 以下は商品テーブルと仕入先マスタテーブルを「商品.仕入先ID = 仕入先マスタ.ID」の条件で左外部結合した結果です。

左外部結合した結果、基準となる商品テーブルに存在する全データを抽出し、仕入先マスタテーブルからは条件に一致したデータを抽出しています。 仕入先IDが商品テーブルにしか存在しない、仕入先ID「50」のレコードについては、仕入先名が空白となり、商品テーブルの項目である商品ID、商品名、仕入先IDしか抽出されていません。

左外部結合は、SQLのLEFT OUTER JOIN句を使用します。

■LEFT OUTER JOINを使った例(OUTERは省略可)

SELECT * 
FROM products
LEFT OUTER JOIN suppliers
ON products.supplier_id = suppliers.id;

右外部結合(RIGHT OUTER JOIN)

右側のテーブルを基準とするのが右外部結合です。 以下は商品テーブルと仕入先マスタテーブルを「商品.仕入先ID = 仕入先マスタ.ID」の条件で右外部結合した結果です。

右外部結合した結果、基準となる仕入先マスタテーブルに存在する全データを抽出し、商品テーブルからは条件に一致したデータを抽出しています。 仕入先IDが仕入先マスタテーブルにしか存在しない仕入先ID「10」のレコードは、商品IDと商品名が空となり、仕入先マスタテーブルの項目である仕入先ID、仕入先名しか抽出されていません。

右外部結合は、SQLのRIGHT OUTER JOIN句を使用します。

■RIGHT OUTER JOINを使った例(OUTERは省略可)

SELECT * 
FROM products
RIGHT OUTER JOIN suppliers
ON products.supplier_id = suppliers.id;

完全外部結合(FULL OUTER JOIN)

完全外部結合は両方のテーブルを基準とし、それぞれに一致しないレコードも抽出結果として含めます。 以下は商品テーブルと仕入先マスタテーブルを「商品.仕入先ID = 仕入先マスタ.ID」の条件で完全外部結合した結果です。

仕入先IDが一致しないデータも含んだ、商品テーブル・仕入先マスタテーブルの両テーブルに存在するデータを抽出しています。

完全外部結合は、SQLのFULL OUTER JOIN句を使用します。

■FULL OUTER JOINを使った例(OUTERは省略可)

SELECT *
FROM products
FULL OUTER JOIN suppliers
ON products.supplier_id = suppliers.id

まとめ

結合種別 特徴
結合 複数のテーブルをまとめて、あたかも一つのテーブルとして扱えるようにすること。
内部結合 結合条件に指定している値が両方のテーブルに存在するデータを抽出する結合のこと
外部結合 結合条件として指定している値が基準となるテーブルに存在すれば抽出する結合のこと
左外部結合 左側のテーブルを基準に行う外部結合のこと
右外部結合 右側のテーブルを基準に行う外部結合のこと
完全外部結合 両側のテーブルを基準に行う外部結合のこと

【初心者でも怖くない】インデックスとは?使用するメリットとデメリット | データベース入門

はじめに

データベースやSQLについて学んでいくと必ずエンカウントする「インデックス」

「データベース内のテーブルのデータを効率的に検索するための機能」だということはなんとなく理解しているものの、 それが具体的にどんなもので、使えると何が嬉しいのかという部分がイメージしにくい初学者の方は少なくないのではないでしょうか。

今回はデータベースにおけるインデックスに関する概要と、具体的な作成方法・削除方法、メリットとデメリットについて簡潔にまとめてみました。

インデックスとは何か?

インデックスは、端的に言えばテーブルの「特定の列の値」と「レコードのID」の対応表のようなものです。

レコードを取得する際、データベースはこのインデックスを使用して検索対象のレコードを効率的に見つけ出すことが可能です。

テーブルの特定の行へのアクセスを、マンションへの宅配便に例えると以下のように表現することができます。

  • インデックス無:マンションの1号室から順に1室ずつ回りながら配達先を探す
  • インデックス有:住民の名前と部屋番号の対応表を確認して目的の配達先に向かう

インデックス無しの状態がインデックス有りの状態と比較して非効率であることは言うまでもありませんが、これはデータベースのテーブルに置き換えても同様です。

例えば数万のレコードを抱えたテーブルから目的のレコードを探し当てるのに、上からひとつひとつ見て回ったのでは非常に効率が悪く、全体の処理にその分時間も要してしまいます。

そのため多くのデータベースではこのインデックスという対応表を検索対象のテーブルとは別領域に設けることによって、検索の速度と効率の向上を図っています。

以下にもう少し具体的な例として、usersテーブルのname列に対してインデックスを追加した場合の例を示します。

インデックス

name ID
Alice 1
Bob 2
Carol 3

usersテーブル

ID name age
1 Alice 23
2 Bob 37
3 Carol 42

name列に対してインデックスが張られたusersテーブルから「name = "Carol"」のレコードを取得する場合、まずインデックスからname="Carol"のIDを特定し、 その取得したIDを使ってusersテーブルからCarolのレコードを特定することができます。

仮にこのusersテーブルが10万行に及ぶユーザーデータを有していたとしても、この2ステップのクエリの実行でレコードを特定することができるのが インデックスを追加する大きなメリットのひとつです。

インデックスの作成方法

作成方法

ここまででインデックスを追加することによるメリットについてはイメージがついたかと思います。 ここからは具体的にインデックスを追加する方法について追っていきます。

インデックスの作成にはCREATE INDEX文を使用します。 以下は基本的なCREATE INDEX文の例です。

  • 例文
CREATE INDEX <スキーマ名>.<インデックス名> ON <インデックスを作成するテーブル名>(<カラム名> , ... )
  • 商品テーブルの商品コードに対してインデックスを作成する場合
CREATE INDEX INDEX_PRODUCTS_ON_CODE ON PRODUCTS(CODE)

スキーマ: データベースの構造を表現する設計図のこと。CREATE INDEX文でスキーマを指定しなかった場合、インデックスは親テーブルと同じスキーマに作成されます。

削除方法

次にインデックスの削除について。 インデックスの削除にはDROP INDEX文を使用します。 以下は基本的なDROP INDEX文の例です。

  • 例文
DROP INDEX <スキーマ名>.<インデックス名>
  • 商品テーブルの商品コードに関するインデックスを削除する場合
DROP INDEX INDEX_PRODUCTS_ON_CODE

インデックス追加による弊害

オーバーヘッドの追加

インデックスの追加によるメリットについては冒頭で述べた通りですが、一見万能に思えるインデックスにも追加に伴う弊害があります。

インデックスは検索速度、効率を向上させる一方で、ストレージと更新時間に対するオーバーヘッドの追加に相当します。 オーバーヘッドとは、レコードの追加・更新・削除と並べ替えの際に、インデックスにかかるパフォーマンスコストのことを指します。

冒頭でも述べた通りテーブルとは別領域にインデックス用のデータを蓄積するため、データベースに追加のストレージスペースが必要となります。 また、インデックスが張られたカラムの追加・更新・削除と並べ替えに応じて、そのカラムに対するインデックスの更新も行われるため、更新処理速度が低下してしまうデメリットがあります。

従ってインデックスの対象とするべきカラムかどうか検討する際には、そのテーブルのレコードの件数やカーディナリティといった指標を元に慎重に吟味していくべきでしょう。

まとめ

インデックスとは何か?

インデックスはデータベース内のテーブルのレコードを効率的に検索するための機能。

インデックスの作成方法・削除方法

インデックスの追加

CREATE INDEX <スキーマ名>.<インデックス名> ON <インデックスを作成するテーブル名>(<カラム名> , ... )

インデックスの削除

DROP INDEX <スキーマ名>.<インデックス名>

メリットとデメリット

メリット

書籍の目次のように、取得したいデータへアクセスする速度と効率を向上してくれる。

デメリット

インデックスが追加されたカラムを更新する際に、インデックスそのものの更新も行うため処理速度が低下してしまう。

結局textareaから入力された文字列ってどう改行したらいいの? | Rails入門

はじめに

form_withなどで生成したテキストエリアのフィールドから入力した文章をerbで表示したときに改行が反映されなくて困ったという場面には、多くのRails初学者が直面してきたのではないかと思います。 「Rails 改行」などのワードで検索を試みるとhtml_safe, raw, sanitize, simple_formatなど様々なヘルパーに関する記事がヒットして「結局どれが最適解なの、、」と思った方も少なくないのではないでしょうか。僕もその一人でした。

今回はこの戦争を終わらせるべく、それぞれのヘルパーに関する特徴、場面ごとの使い分けについてまとめていきたいと思います。 想定する読者、解決できる問題は以下の通りです。

  • テキストエリアから入力した文字列をviewで表示させると改行が反映されない
  • 一部の改行は反映されたけど入力した見た目通りに改行ができない
  • とりあえず検索結果に表示されたヘルパーで囲うことで改行はできたけどやり方として合っているのか自信がない

前提知識

特殊文字のエスケープ処理

そもそも何故テキストエリアから入力した文字列を表示すると改行などのスタイルがうまく当たらないのか、まずそれぞれのヘルパーの特徴ついてまとめて行く前に前提知識について整理してみたいと思います。

Railsはデフォルトで<%= %>で囲われた文字列に特殊文字が含まれている場合、それらを無害な文字列に変換する機能を持っています。

ここでいう特殊文字とは具体的には「<」「>」「&」「"」 のような文字列のことで、これらの文字列を「&lt;」「&gt;」「&amp;」「&quot;」 などの無害な文字列に変換するということをしてくれています。

例えば<p>や今回議題として取り上げている改行タグである<br> などのHTMLタグにもこれらの文字列が含まれていることがわかりますね。

この特殊文字を無害な文字列に変換すること工程をエスケープと呼びます。

<  -->  &lt;
>  -->  &gt;
&  -->  &amp;
"  -->  &quot;

<!-- 例えば改行タグは以下のように変換されて出力されている -->
<br> --> &lt;br&gt;

では何故、Railsはこのような文字列の変換を行なっているのか?についてですが、先ほどから幾度も無害な文字列に変換すると言っているように、上述の特殊文字はプログラムの世界ではそれぞれ意味のある挙動をする記号となっており、この対策が為されていないとテキストエリアから悪意のあるコードを流し込まれて、それが表示されるタイミングで実行されてしまうことにより、アプリケーションに意図しない悪影響を与える可能性があるためです。

XSS(クロスサイトスクリプティング)

例えばRailsにデフォルトでエスケープ処理が施されていなかった場合に、テキストエリアから以下のようなコードを入力されたとします。

<script>
  alert('hoge');
</script>

このコードは<script> タグで囲われた中にあるJavaScriptのコードを実行するものです。エスケープ処理が行われていない場合、DBに保存されたこの文字列がviewに表示されるタイミングで実際に実行されて、ブラウザには実際にJavaScriptのアラートが表示されることとなります。

今回のように単純に文字列を表示するだけのアラートならいざ知らず、これに外部サイトにリダイレクトさせるコードを仕込んだりすることも勿論可能です。このようにアプリケーションの入力フォームから悪意のあるコードを流し込み、実行させることで、不当に情報を抜き取ったりデータを破壊したりする攻撃手法のことをXSS(クロスサイトスクリプティング)と呼びます。

このようなセキュリティの脆弱性を塞ぐために、Railsではviewに文字列を出力する際にデフォルトで特殊文字をエスケープする機能が備わっています。

また、エスケープと似た用途で使われる言葉にサニタイズというものがありますが、それぞれ以下のような意味を指しています。

  • アプリケーションにとって有害な文字列を「無害化」する行為全般がサニタイズ
  • 特別な意味を有する文字列を別のものに変換する処理そのものがエスケープ

例えば空港の荷物検査を例に挙げると荷物検査そのものがサニタイズ 、荷物検査のうち問題のある荷物を取り除く工程がエスケープに当たります。

ヘルパーの具体的な使い分けについて

html_safe

さて、前置きが壮大となりましたがここからはこれらの前提を考慮した上で、意図したスタイルをview上で反映させるための具体的なヘルパーとそれぞれの特徴をまとめていきます。

まずはhtml_safeヘルパーについてですが、要点をまとめると以下のような特徴が挙げられます。

  • レシーバーとして与えられた文字列を信頼できる安全なもとしてマークし、エスケープ処理を解除する
  • 改行タグが有効となりview側で改行を含めた表示が可能となるが、改行タグ以外のタグについても否応なく有効化されてしまう
  • ユーザーからの入力など信頼性の低い文字列に対して呼び出した場合、XSS脆弱性の危険性が伴う

html_safeはレシーバーとして与えられた文字列を安全であるとマーキングすることで、含まれる特殊文字に対するエスケープ処理を否応なく解除させるヘルパーです。これにより勿論改行タグである<br> もview上で有効となり、意図した見た目での表示は可能となりますが、改行タグに関わらず全てのタグに対してエスケープが効かなくなってしまいます。

api.rubyonrails.org

sanitize

次にsanitize ヘルパーについて。このヘルパーは第一引数として与えた文字列に対して、第二引数でtags: 配列として与えたHTMLタグのみをホワイトリスト形式で許可し、許可したタグ以外を文字列から削除します。

尚tagsに何のタグも与えなかった場合はデフォルトの設定が適用され、scriptなどの危険性の高い一部のタグが削除されますが、殆どのタグがそのまま削除されずに残る形となります。

# 第二引数なし
sanitize("<h2>タイトル</h2>")
=> "<h2>タイトル</h2>"

sanitize("<script>alert('hoge')</script>")
=> "alert('hoge')"

# 第二引数あり
sanitize("<h2>タイトル</h2>", tags: %w(strong em a))
=> "タイトル"

尚sanitizeのデフォルト設定で許可されているタグの一覧は以下の通りです。

ActionView::Base.sanitized_allowed_tags 
=> #<Set: {
    "strong",
    "em",
    "b",
    "i",
    "p",
    "code",
    "pre",
    "tt",
    "samp",
    "kbd",
    "var",
    "sub",
    "sup",
    "dfn",
    "cite",
    "big",
    "small",
    "address",
    "hr",
    "br",
    "div",
    "span",
    "dd",
    "abbr",
    "acronym",
    "a",
    "img",
    "blockquote",
    "del",
    "ins"
}>

api.rubyonrails.org

simple_format

simple_formatは引数として与えた文字列を以下の条件で加工します。

  • 文字列を<p>で括る
  • 改行コード(/n)は <br> に変換
  • 連続した改行は、</p><p> を付与
  • sanitizeオプションをtrueにすることで文字列に対してsanitizeメソッドを実行することで、上述のデフォルトで許可されているタグ以外を文字列から除去

github.com

引数として与えたHTMLにおける危険な部分をよしなにエスケープしつつ、改行を含んだスタイルもいい感じに表示してくれます。しかし懸念点として以下の2点のような内容も挙げられます。

  • 3つ以上の連続した改行が表現できない
  • それぞれの段落区切りにpタグで囲われるため、pタグに付与されるCSSにより文末に意図しないmarginが付与されるなど、スタイリングの面で融通が効かないシーンもある

例えば以下のような文字列を与えられた際に「こんばんは」と「おやすみ」の間の3行の改行は1行分の改行として表示されてしまいます。

text = <<EOL
おはよう
こんにちは

こんばんは

おやすみなさい
EOL

simple_format(text)
=> 
<p>
    おはよう<br>
    こんにちは
</p>
<p>こんばんは/p>
<p>おやすみなさい</p>

api.rubyonrails.org

safe_join

とはいえ複数行の改行を要する場面というのも無きにしも非ず。

そこで、複数行の改行を含む文字列をセキュアに表示するコードを以下に示します。

html = "おはよう\nこんにちは\n\nこんばんは\n\n\nおやすみなさい"
safe_join(html.split("\n"), tag(:br))

=> おはよう<br>
こんにちは<br>
<br>
こんばんは<br>
<br>
<br>
おやすみなさい

safe_joinメソッドの処理の流れは以下の通りです。

  1. 引数で渡された配列の各要素に対してエスケープ処理を実行

     # splitにより返却される配列の中身
     html.split("\n")
     => ["おはよう", "こんにちは", "", "こんばんは", "", "", "おやすみなさい"]
    
  2. 第二引数で渡されたタグでエスケープ後の各要素をjoin

  3. 任意のタグでjoinされた全体の文字列を返却する

これにより渡ってきた文字列全文を検証した上で、フォーム側で入力した改行の数をそのまま考慮した表示をview上で行うことが可能となります。

api.rubyonrails.org

まとめ

エスケープ処理

ユーザーからの入力値をそのまま表示してしまうとXSSの危険性が伴うため、Railsではviewへの表示の際にデフォルトで文字列に対してエスケープ処理を行なっている。これによりHTMLタグに含まれる記号が無害化されて、副作用としてタグが機能しなくなるため改行等がスタイルとして反映できない。

html_safe

ユーザーから入力された信頼性の低い文字列に対して、スタイリングの用途でこのヘルパーをそのまま当てるのは危険といえます。

sanitize

表示するHTMLに含まれるタグの数が1つ2つ程度なら問題ありませんが、WYSIWYGエディタなどのスタイルの自由度が高いエディタをユーザーの入力フォームとして採用した場合にはメンテナンスが一気に辛くなります。

例えばユーザーから入力されうるスタイルで表示するために10以上のHTMLタグを配列として渡す必要が出てきたり、エディタ側の改修によってsanitizeに認可するタグも変更する必要があるなどメンテナンス性が非常に悪いです。

  • 表示するHTMLに一定の規則が見込める
  • フォーム側の改修頻度がさほど高くない

など入力フォーム側の仕様と相談する必要性はありそうです。

simple_format

前述の通り複数の改行を含んだ文字列の表示などやpタグに対して付与されるスタイルにより思い通りの見た目で表示できない場合もありますが、レイアウトの面は勿論のこと、sanitizeオプションをtrueにすることでセキュリティ面おいても諸々をいい感じに表示・エスケープしてくれるので、改行を反映させたいというケースにおいて大抵はこのヘルパーの利用で事足りるのではないかと思われます。

safe_join

  • 複数行の改行をview側で正確に表示したい
  • pタグに付与されるmarginが表示の要件に対して不都合

などsimple_formatより更に厳格に入力時の見た目を再現する必要がある場合には、この方法も手札として持っておくといいでしょう。

結論

結論として、テキストエリアから入力された文字列の表示時に、改行を反映させたいという目的に対するヘルパーの使用優先度としては以下のようになるかと思います。

simple_format ≥ safe_join > sanitize

【5分くらいでわかる】Moduleの使い方 | Ruby入門

Module

Rubyにはクラス以外にもメソッドや定数を提供する手段として、モジュールという仕組みが存在します。 モジュールでは以下のようなことを実現することができます。

  • クラスと同じように定数やメソッドをまとめる
  • クラスに組み込んで多重継承を実現する
  • クラス名、定数名の衝突を防ぐための名前空間を提供する

ClassとModuleの違い

クラスはオブジェクトを生成する元となるプログラムです。クラスの特徴としては以下のようなものが挙げられます。

  • インスタンスを生成できる
  • 継承を用いて親クラスからメソッドや定数を呼び出すことができる

これに対してモジュールはインスタンスを生成することはできません。継承することも出来ません。 しかしモジュールにはクラスにおける継承と似た仕組みとして、ミックスインというものが存在します。 モジュールをクラスに組み込み(ミックスイン)することで、モジュールに定義してある定数やメソッドを組み込み先のクラスから呼び出すことが出来るようになります。

ModuleをClassにinclude/extendする

Rubyのクラスでは単一継承が採用されており、一つのクラスは一つの親クラスしか持てません。 また、継承の原則はis-aの関係であること。つまり”AはBである”が成り立たないクラス同士では継承の使用は避けるべきです。 しかし現実の開発において、複数クラスにまたがって共通の機能が必要となるシーンは多々存在します。

ここで活躍するのがモジュールです。 前述の通り、モジュールはクラスに組み込むことでモジュールに定義したメソッドを呼び出すことができます。 モジュールを使うことで、本来単一継承しか行えないクラスに対して多重継承を実現することが可能となります。 モジュールの組み込み方法にはincludeとextendの2つのパターンが存在します。

include

includeでは、対象のクラスに組み込んだモジュールのメソッドがインスタンスメソッドとして組み込まれます。 Class.newで作成したインスタンスに対して呼び出すことが可能です。 以下は引数として与えた文字列をログとして出力する汎用的なメソッドを持たせたLoggableモジュールを、 商品クラスにincludeして使用する一例です。

module Loggable
  def log(text)
    puts"[LOG] #{text}"
  end
end

class Product
  include Loggable
end

product = Product.new
product.log('hello world')
#=> "[LOG] hello world"

# 伊藤 淳一. プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plus) (Japanese Edition) (Kindle の位置No.6899-6900). Kindle 版. より一部抜粋

extend

extendでは、対象のクラスに組み込んだモジュールのメソッドがクラスメソッドとして組み込まれます。 インスタンスからの呼び出すは不可で、クラスに対して直接呼び出すことが可能です。 以下は上述の例と同様のモジュールをextendでミックスインした場合の例となります。 メソッドのレシーバーがインスタンスではなくクラスになっている点に注目です。

module Loggable
  def log(text)
    puts"[LOG] #{text}"
  end
end

class Product
  extend Loggable
end

Product.log('hello world')
#=> "[LOG] hello world"

# 伊藤 淳一. プロを目指す人のためのRuby入門 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plus) (Japanese Edition) (Kindle の位置No.6931). Kindle 版.  より一部抜粋

名前空間を提供して命名の衝突を避ける

例えば一つのアプリケーション内で、ユーザーの属性が管理ユーザーと一般ユーザーに分かれているものは多々あるかと思います。 この権限が異なるユーザーをそれぞれ別々のクラスで管理しており、それらを何らかの理由で同時に使用しなければならない場合に、 モジュールを名前空間として用いることで名前の衝突を避けることが可能です。

 # 管理用ユーザー
class User
   extend Loggable
end


# 一般ユーザー
class User
   extend Loggable
end

# どちらに対する呼び出しか判別できない
User.log('admin')
User.log('standard')
module Admin
  class User
     extend Loggable
  end
end


# 一般ユーザー
class User
   extend Loggable
end

# 名前空間としてモジュールで囲うことにより衝突を回避
Admin::User.log('admin')
#=> "[LOG] admin"

User.log('standard')
#=> "[LOG] standard"

まとめ

まとめるとモジュールの用途としては主に以下のようなシーンが想定されます。

  • 継承は使用できないが複数のクラスで横断的に使用したい共通のメソッドや定数を定義したい場合
  • 多くのクラスから利用される汎用的な共通メソッドを定義したい場合
  • 同名で属性の異なるクラスに名前空間を提供して名前の衝突を防ぎたい場合

【5分くらいでわかる】flashとflash.nowの違い | Rails入門

先に結論から

  • flashはアクションが動いた時に表示され、その場限りで破棄される
  • flash.nowはアクションの有無に関わらず表示され、その場限りで破棄される

flashメッセージとは

画面遷移時にユーザーに向けて発信される簡易的なメッセージのこと。 ログインの成功時やデータの登録時など、ユーザーに対して逐次状況を伝える手段として主に用いられます。 実体としてはサーバー上のセッション情報の一部として保持されて、リクエストの度にクリアされるという性質を持っています。 簡単に言うと表示されたら破棄されるその場限りのメッセージです。

flashとflash.now

具体的なコードを書きながらflashメッセージについてまとめてみます。

想定するシチュエーションとしては以下の通りです。

  • 投稿モデルに対して一件の投稿を登録し、登録が完了したら「登録しました」というflashメッセージを画面に表示し、投稿一覧画面に遷移してくる

  • 何らかのバリデーションエラーにより登録が失敗したら、「登録に失敗しました」というflashメッセージを画面に表示し、新規投稿画面を呼び出す

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def create
    @post = Post.new(create_post_params)
    if @post.save
      redirect_to posts_url, flash: {success: '登録しました' }
    else
      flash.now[:danger] = '登録に失敗しました'
      render :new
    end
  end

  private

    def create_post_params
      params.require(:post).permit(
        :name,
        :status
      )
  end
end

上記コードを見て分かる通り、登録成功時には flash: {success: '登録しました' } 登録失敗時には flash.now[:danger] = '登録に失敗しました' という二つの書き方をしていることがわかります。 それではこの二つの書き方にはどのような違いがあるのでしょうか?

redirect_toとrenderの違いについて

二つの違いについて説明する前にまず、redirect_toとrenderの違いについて簡単に説明を挟んでおこうと思います。

redirect_to

引数として次にアクセスしてほしいURLの文字列を渡して、リダイレクト(HTMLの代わりにURLを返却するHTTPレスポンス)を発生させるメソッド。 リダイレクトを受け取った場合ブラウザは、自動的に返却されたURLに対してGETでリクエストをしなおす。 この場では端的に、渡したURLに対してGETリクエストを行い、その先のアクションを実行するものと理解するのがいいかと思います。

render

指定したテンプレート(viewファイル = HTML)をブラウザに渡してレンダリングを実行するメソッド。 これもこの場では端的に指定したテンプレートを呼び出すものと理解するのがいいかと思います。 純粋なテンプレートの呼び出しを行うのみなのでアクションの実行を介しません。

flashとflash.nowの違いについて

flashとflash.nowでは、メッセージを表示するきっかけが異なります。 flashメッセージと前段でおさらいしたredirect_to, renderに焦点を当てながら、改めてコードを分解していきます。

# app/controllers/posts_controller.rb

class PostsController < ApplicationController
  def create
    @post = Post.new(create_post_params)
    if @post.save
      redirect_to posts_url, flash: {success: '登録しました' }
    else
      flash.now[:danger] = '登録に失敗しました'
      render :new
    end
  end

  private

    def create_post_params
      params.require(:post).permit(
        :name,
        :status
      )
  end
end
投稿の登録が成功した場合
  1. redirect_to posts_urlにより投稿一覧画面に遷移。(アクションの実行)遷移したページでflashメッセージが表示される(メッセージ破棄)
  2. 異なるページに遷移する(アクションの実行)
  3. 1の時点でflashに格納されたメッセージが破棄されているため、遷移したページでメッセージは表示されない
投稿の登録が失敗した場合
  1. render :newにより新規投稿画面がレンダリングされる。新規投稿画面でflashメッセージが表示される(メッセージ破棄)
  2. 異なるページに遷移する(アクションの実行)
  3. 格納されたメッセージが破棄されているため、遷移先のページではメッセージは表示されない
投稿失敗時にflash.nowではなく、flashでメッセージを表示しようとした場合
  1. render :newにより新規投稿画面がレンダリングされる。新規投稿画面でflashメッセージが表示される
  2. 異なるページに遷移する(アクションの実行)
  3. 遷移したページでも再度メッセージが表示される(メッセージ破棄)
  4. 異なるページに遷移する(アクションの実行)
  5. 3のタイミングで格納されたメッセージが破棄されているため、遷移先ではメッセージは表示されない

このようにflashとflash.nowでは、アクション(リクエスト)を跨ぐか跨がないかによりメッセージが表示・破棄されるタイミングに違いがあります。

まとめ

  • flashはアクションが動いた時に表示され、その場限りで破棄される
  • flash.nowはアクションの有無に関わらず表示され、その場限りで破棄される

参考記事

railsguides.jp

ichigick.com

pikawaka.com