axiosのエラーかを判別する(axios.isAxiosError)

axiosでREST APIにアクセスするコードを含む複数のコードをtry-catchで囲んでいるときに、axiosのエラーをログなどに出力する際、JSON.stringifyしていたのですが、axios以外が原因でエラーになっている場合、JSON.stringifyでは文字列を得られなかったので対処方法を検討していました。

axios.isAxiosError

最初objectかどうかで判定しようとしていましたがうまく判定できませんでした。axiosではaxiosのエラーかどうかを判別するためのaxios.isAxiosErrorが提供されているため、これを使う方が良さそうです。

async function test() {
  try {
    const response = axios.get("http://httpstat.us/200");
  } catch (error) {
    if (axios.isAxiosError(error)) {
      console.log(JSON.stringify(error, null, 2));
    }
  }

動作確認

この動作を確認するために以下のようなプログラムで確認しました。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js"></script>
    <script>
      async function test() {
        try {
          const response = await axios.get("http://httpstat.us/200");
          const a = [];
          console.log(a[1].b);
        } catch (error) {
          if (axios.isAxiosError(error)) {
            console.log("Axios Error:", JSON.stringify(error, null, 2));
          } else {
            console.log("Error:", String(error));
          }
        }
      }

      test();
    </script>
  </body>
</html>

このプログラムでは、httpstat.usという指定したHTTPステータスのレスポンスを返してくれるサービスで任意のHTTPステータスを返すようにしています。HTTPステータスが200の場合は例外が発生しないため、後続の処理でundefinedなオブジェクトのプロパティにアクセスしているため例外が発生します。

上記のHTMLにブラウザからアクセスすると、コンソールログに

Error: TypeError: Cannot read properties of undefined (reading 'b')

と表示されます。

一方、

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script src="https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js"></script>
    <script>
      async function test() {
        try {
          const response = await axios.get("http://httpstat.us/404");
          const a = [];
          console.log(a[1].b);
        } catch (error) {
          if (axios.isAxiosError(error)) {
            console.log("Axios Error:", JSON.stringify(error, null, 2));
          } else {
            console.log("Error:", String(error));
          }
        }
      }

      test();
    </script>
  </body>
</html>

のようにURLの200部分を404に変えてアクセスすると、

Axios Error: {
  "message": "Request failed with status code 404",
  "name": "AxiosError",
  "stack": "AxiosError: Request failed with status code 404\n    at Qe (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:31804)\n    at XMLHttpRequest.y (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:36641)\n    at e.<anonymous> (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:48621)\n    at p (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:3448)\n    at Generator.<anonymous> (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:4779)\n    at Generator.throw (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:3858)\n    at p (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:9996)\n    at u (https://cdn.jsdelivr.net/npm/axios@1.7.8/dist/axios.min.js:1:10235)",
  "config": {
    "transitional": {
      "silentJSONParsing": true,
      "forcedJSONParsing": true,
      "clarifyTimeoutError": false
    },
    "adapter": [
      "xhr",
      "http",
      "fetch"
    ],
    "transformRequest": [
      null
    ],
    "transformResponse": [
      null
    ],
    "timeout": 0,
    "xsrfCookieName": "XSRF-TOKEN",
    "xsrfHeaderName": "X-XSRF-TOKEN",
    "maxContentLength": -1,
    "maxBodyLength": -1,
    "env": {},
    "headers": {
      "Accept": "application/json, text/plain, */*"
    },
    "method": "get",
    "url": "http://httpstat.us/404"
  },
  "code": "ERR_BAD_REQUEST",
  "status": 404
}

と表示され、axios.isAxiosErroraxiosのエラーかどうかを判別できていることが確認できます。

まとめ

例外が発生したときにエラーページに遷移するという動作をするアプリケーションがあり、コンソールにログを出力しても記録に残らないため、サーバーにログが送るという対処を行っていました。実際に試してみると、axiosのエラーだと、エラーの内容がサーバーのログで確認できましたが、それ以外のエラーの場合は{}になって何も確認できないという事象があり、これを確認できるようにするために、このような検証をしてみました。

try-catchで例外をcatchしていることまでははっきりしていても、axiosのエラーかそれ以外のエラーかを判定する場合は型ではなく、axios.isAxiosErrorで判定するのが確実だということが確認できました。

Emmetを活用してコーディングのパフォーマンスを向上させよう

EmmetはHTMLやCSSを省略記法で記述できるようにプラグイン・拡張機能です。

最近Youtube動画で、Beginner/JuniorエンジニアとPro/Seniorエンジニアのコーディングの仕方についた動画がよく流れてきたので、使い方をまとめてみました。

Emmetの使い方

Emmetの省略記法からHTMLのタグやCSSのプロパティを作成するにはENTER/RETURNまたはTABを使用します。どちらが使用するかはエディタによって異なっており、Visual Studio CodeはENTER/RETURNTABのどちらでも変換できますが、WebStormでは[TAB]のみで変換します。

例えば、<div></div>タグを作成するには

div[ENTER/RETURN or TAB]

のように入力します。

HTMLのタグの作成

主にEmmetが使用されるのはHTMLのタグを記述する場合です。少ないタイピング量で素早くタグを作成できます。

よく使われる省略記法について見ていきます。

空タグを作成

単に空タグを作成したい場合はタグ名を入力後、[ENTER/RETURN or TAB]を入力します。

h2[ENTER/RETURN or TAB]
->
<h2></h2>

空タグを作成するとカーソルは<h2></h2>の間にあるので、十字キーやマウスでカーソル位置を変えることなくタグ内に入力することができるようになります。

id属性付きの空タグを作成

実際にコーディングしていると、単純に空タグを作成するということはid属性やclass属性がついたタグを作成することがほとんどだと思います。id属性がついた空タグを作成するには、タグ名に続いて#を入力してid属性の名称を入力し、[ENTER/RETURN or TAB]を入力します。

h2#title[ENTER/RETURN or TAB]
->
<h2 id="title"></h2>

ここではHTMLファイル内で一意になるtitleというid属性をもったh2タグを作成しました。

クラス属性付きの空タグを作成

class属性をついた空タグを作成するにはタグ名に続いて.を入力してclass属性の名称を入力し、[ENTER/RETURN or TAB]を入力します。

div.sidebar[ENTER/RETURN or TAB]
->
<div class="sidebar"></div>

2つ以上のクラスを定義したい場合は、.class名を繰り返します。

div.sidebar.active[ENTER/RETURN or TAB]
->
<div class="sidebar active"></div>

BootstrapやTailwind CSSなどのCSSフレームワークを使っている場合はタグにいくつものクラスを定義することになるため、Emmetを使った省略記法でのコーディングは生産性に大きな影響を与えます。

div.mb-3.row[ENTER/RETURN or TAB]
->
<div class="mb-3 row"></div>

子要素をまとめて作成

例えば、ulタグの中にliタグがあるような構造をまとめて作成するには、>を使用します。

ul>li[ENTER/RETURN or TAB]
->
<ul>
  <li></li>
</ul>

作成後のカーソル位置は<li></li>の間にあります。

要素を複数まとめて作成

同じタグを複数個まとめて作成したい場合は*に続いて繰り返し回数を入力することでまとめて作成できます。

前述の例で、子要素のliタグをまとめて3個作成したい場合は

ul>li*3[ENTER/RETURN or TAB]
->
<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>

のようにします。作成後のカーソル位置は一番最初のliタグの<li></li>の間にあり、[TAB]で次のliタグの<li></li>の間に移動できるので、

ul>li*3[ENTER/RETURN or TAB]
->
<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>

abc[TAB]
->
<ul>
  <li>abc</li>
  <li></li>
  <li></li>
</ul>

def[TAB]
->
<ul>
  <li>abc</li>
  <li>def</li>
  <li></li>
</ul>

ghi[TAB]
->
<ul>
  <li>abc</li>
  <li>def</li>
  <li>ghi</li>
</ul>

のように十字キーやマウスを使うことなく効率的にコーディングすることができます。

テキスト付きの要素を作成

前述ではh2タグを空タグで作成して、その後にテキストを入力していました。Emmetではテキスト付きの要素を作成することもできます。

h2#title.text-5xl.font-semibold{タイトル}
->
<h2 id="title" class="text-5xl font-semibold">タイトル</h2>

少し注意点すべき点があるとすれば、Copilotなどが有効になっていると、{を入力したあと、CSSのプロパティを候補として提示することがあり、何も考えずに[TAB]を押すと変換できない場合がある点です。初めての場合になりやすいので注意しましょう。

ダミーテキストを作成

デザインを確認するために実際のコンテンツの代わりにダミーテキストを挿入したいということがあります。

このような場合にはloremを入力して[ENTER/RETURN or TAB]を入力すると、

lorem[ENTER/RETURN or TAB]
Lorem ipsum dolor sit amet consectetur adipisicing elit. Architecto nisi facere, velit facilis quo rerum quae eveniet animi alias, voluptate rem, tempora placeat ut. Voluptates saepe earum nesciunt! Repudiandae, earum.

のようにダミーテキストが作成されます。

独自の属性付きの要素を作成

ピュアなHTMLのタグしか生成できないというわけではなく、独自の属性をつけることもできます。

例えば、Vue.jsのv-ifディレクティブをもったdivタグを作成するとします。v-ifディレクティブが真になる条件はisShowtrueになる場合とします。

div[v-if="isShow"]
->
<div v-if="isShow"></div>

今回はVueのディレクティブの例を示しましたが、aria-*属性などにも使用できるため、利用シーンは多いと思います。

CSSプロパティの作成

あまり知られていないかもしれませんが、CSSのプロパティを省略記法で作成することもできます。

.title {

}

というセレクタがあるとき、

.title {
  mb10[ENTER/RETURN or TAB]
}

のように省略記法をでプロパティを書くと、

.title {
  margin-bottom: 10px;
}

のようにプロパティが作成されます。

HTMLの場合と異なり、省略記法を覚えていないと使えませんが、ある程度法則性があり、Tailwind CSSのクラス名と同じようなルールのものもあるので、よく使うものから覚えていくのがよいと思います。

Emmetチートシート

Emmetの使い方をまとめたチートシートが公開されています。

HTMLとCSSの両方をサポートしていますので、是非ご活用ください。

EmmetとCopilotの使い分け

少ないタイピング量でコードを生成するといえば、CopilotやTabnineなどの生成AIがあります。

生成量という意味では生成AIに軍配が上がりますが、必ずしも意図した内容が生成されるとは限りません。同じような内容を何度も生成する場合、定石のような内容を生成する場合、どう書けばいいかわからない場合などは生成AIを使用し、何を出力したいのかがはっきりしている場合は生成AIが提示する候補を[ESC]でキャンセルしてEmmetの省略記法を使うのがよいと思います。

どちらか一方だけを使うのではなく、それぞれの長所と短所を理解し、適したツールを選択していくことを大切です。

コーディング時の意識をアップデートする

HTMLのコーディング時にEmmetを活用する場合、コーディング時の意識をアップデートするようにしていくことが必要かもしれません。

Emmetを使わないときは、タグの構造だけに着目してタグを作成し、その後にデザインを整えるためにクラスをどこにつけようか、といった流れで書くことができます。

例えば、3つの列を持つテーブルを作成する場合、

<table>
  <thead>
    <th>
      <td></td>
      <td></td>
      <td></td>
    </th>
  </thead>
  <tbody>
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
</table>

のようにタグの構造を作成してから、テーブル全体をデザインするためにdata-tableクラスをtableタグにつけて、ヘッダー部分をデザインするためにtheadtable-headerをつけて、ボディ部分をデザインするためにtbodytable-bodyクラスをつけて、といったように後からつけていくことができます。

<table class="data-table">
  <thead class="table-header">
    <th>
      <td></td>
      <td></td>
      <td></td>
    </th>
  </thead>
  <tbody class="table-body">
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
</table>

一方、Emmetを活用しようとすると、タグの構造を考えるときにそのタグにどんな意味を持たせるかということを考えながらやった方が効率的に生成できます。

// データテーブルを意味するtableを用意して、
table.data-table
↓
// その中にヘッダーを意味するtheadを用意して、
table.data-table>thead.table-header
↓
// ヘッダータイトルはまだ決めていないけど、3列用意しよう
table.data-table>thead.table-header>tr>td*3
↓
// さらにボディ部分を意味するtbodyを用意して、
table.data-table>thead.table-header>tr>td*3^tbody.table-body
↓
// 同じように3列用意しよう
table.data-table>thead.table-header>tr>td*3^^tbody.table-body>tr>td*3[ENTER/RETURN or TAB]
->
<table class="data-table">
  <thead class="table-header">
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </thead>
  <tbody class="table-body">
    <tr>
      <td></td>
      <td></td>
      <td></td>
    </tr>
  </tbody>
</table>

思考の順序と省略記法の書く順序が一致すると、もっとも自然にもっとも効率的にコーディングすることができます。これは慣れが必要となるので、最初は声に出しながら何度もトレーニングするのがよいでしょう。

まとめ

Emmetを活用することで効率的にHTMLやCSSを効率的にコーディングできるようになります。ただし、効率性を高めるにはCopilotとの使い分けをうまくやったり、コーディング時の意識をアップデートする必要があります。

Emmet以外にも効率的にコーディングを効率化するためのプラグイン・拡張機能やツールがあります。少しずつ活用できる幅を広げてコーディングを爆速にしていくのがよいと思います。

参考文献

Visual Studio Codeでタグをまるごと選択する

Visual Studio Codeで開始タグから終了タグまでをまるごと選択したいことが多々あります。

これまでは一度対象のタグを閉じて範囲選択をしていたのですが、タグの範囲を見やすくする拡張機能と相性がよくなかったので困っていました。

Emmetショートカットを使う

Emmetショートカットを使用することで、追加の拡張機能なしで実現できます。

コマンドパレットからEmmet: Balance (outward)を見つけ、歯車ボタンを選択します。

任意のショートカットを設定します。

開始タグで設定したショートカットを使用すると、

このように範囲選択できます。

あまり使うことはないかもしれませんが、Emmet: Balance (inward)を使用することで、

選択しているタグの内側を選択することができます。

[関係データベース]スーパーキー、候補キー、主キー、代替キー

スーパーキーと候補キーの違いがよくわからなかったので、関係データベースにおけるキーについてまとめてみました。

用語について

関係データベースの用語で説明しますが、データベースでどの用語に対応するかについてまとめておきます。

  • 関係(リレーション) → 表(テーブル)
  • タプル → 行(レコード)
  • 属性 → 列(カラム)
  • 空値 → NULL値

各キーの概念の階層化

これから説明するスーパークラス、候補キー、主キー、代替キーは、より広い概念(上位)とより狭い概念(下位)といった階層構造の関係があります。

これらを図示すると以下のようになります。

スーパーキー
└── 候補キー
    ├── 主キー
    └── 代替キー

上位の概念から下位の概念の順に説明していきます。

各キーについて説明する際の具体的な例として、関係”社員”

社員(社員ID, 社員番号, 氏名, 部署, メールアドレス)

を使用します。

スーパーキー(Super Key)

今回説明するキーの中ではもっとも広い概念です。関係のタプルを一意に識別できるすべての属性や属性の組み合わせがスーパーキーです。後述の候補キーとは異なり、余分な属性が含まれてもよいという特徴があり、テーブル内で一意性を保証できる組み合わせであればすべてスーパーキーになります。

関係”社員”の場合、

  • {社員ID}
  • {社員番号}
  • {メールアドレス}
  • {氏名, メールアドレス}
  • {社員番号, 部署}
  • ・・・

などがスーパーキーになります。

候補キー(Candidate Key)

スーパーキーの中でも、最小限の属性で一意性を保証できるキーが候補キーです。余分な属性がないため、スーパーキーよりも狭い概念になります。後述の主キー/プライマリーキーとの大きな違いは空値を許すという点です。

関係”社員”の場合、

  • {社員ID}
  • {社員番号}
  • {メールアドレス}

が候補キーになります。たとえば、{社員ID, 氏名}はスーパーキーですが、氏名がなくても一意性を保証できるため、候補キーにはなりません。

主キー/プライマリーキー(Primary Key)

主キーは、候補キーの中から選んだ1つの候補キーです。前述の候補キーとの違いとして、空値(NULL値)を許しません。

関係”社員”の場合、

  • {社員ID}

が一意かつ空値(NULL値)を許さないため、主キーに適しています。

代替キー/代用キー(Alternate Key)

候補キーのうち、主キーとして選ばれなかったキーです。代替キーは主キーとして選ばれなかった候補キーであるため、空値を許します。

候補キーのうち、空値を許さないキーが1つしかなければ、そのキーが主キーになり、それ以外が代替キーになりますが、主キーとして選択可能なキーが複数ある場合はどれを主キーにするかは設計上の判断次第になるため、必ずこれが主キーになるといったルールがあるわけではありません。

関係”社員”の場合、

  • {社員番号}
  • {メールアドレス}

が代替キーになります。

ナチュラルキーとサロゲートキー

キーの分類には、前述の階層構造と直交する「ナチュラルキー」「サロゲートキー」という考え方があります。

ナチュラルキー/自然キー(Natural Key)

ナチュラルキーとは、データそのものに基づいて一意性を保証するキーです。すなわち、すでに現実世界で意味を持っているデータをプライマリーキーとして使用します。

関係”社員”の場合、

  • {社員番号}
  • {メールアドレス}

がナチュラルキーになります。今回の例にはありませんが、SSN(社会保障番号)、マイナンバー(個人番号)なども社員を一意に特定できるナチュラルキーになります。

サロゲートキー/代理キー(Surrogate Key)

一方、サロゲートキーとは、データベース内で独自に生成され、現実世界のデータとは直接の関係がない一意なキーのことです。主に整数値(自動増分のID)、GUID(グローバル一意識別子)、UUID(ユニバーサル一意識別子)などが使用されます。

関係”社員”の場合、

  • {社員ID}

がサロゲートキーになります。これについては少しわかりづらいので順を追って説明します。

現実世界の社員を分析した結果作成した関係”社員”が

社員(社員番号, 氏名, 部署, メールアドレス)

だったとします。当然ですが、企業においては社員番号で社員を一意に識別することができます。概念データモデルでは{社員番号}を主キーにすることで特に問題なさそうですが、次のケースを考えてみます。

まず、関係”社員”に

社員(社員番号, 氏名, 部署, メールアドレス, 入社年度)

のように属性入社年度を追加します。

次に定年退職後嘱託社員としてシステムに再登録することことを考えてみます。このとき、社員番号は変えずに入社年度は元々の入社年度の情報を残しつつ、嘱託社員として入社したときの入社年度を記録したいとしましょう。具体的には

('S001', '山田太郎', '総務部', 'taro@example.com', 1964)

という社員がいる場合、この社員が2024年に嘱託社員として再雇用された場合、

('S001', '山田太郎', '総務部', 'taro@example.com', 1964)
('S001', '山田太郎', '総務部', 'taro@example.com', 2024)

のようにデータを登録したい場合、社員番号は関係”社員”でタプルを一意に特定するキーとは使用できなくなります。(普通は嘱託社員として再雇用した場合、社員番号は変えることが多いと思いますが、ここでは変えない運用を想定しています)

この場合、

社員(社員番号, 連番, 氏名, 部署, メールアドレス)

のように、属性連番を追加して{社員番号, 連番}で一意になるようにすることもできますが、属性社員IDというタプルを一意に特定する属性を追加する方法もあります。この社員IDはタプルを一意に特定する以外の意味を持たないため、サロゲートキー(ナチュラルキーの代理で使われるキー)とよばれます。

まずはナチュラルキーから検討する

ここではデータベースの用語で説明します。

プロジェクトの方針によってはすべてのテーブルにサロゲートキーを設定する場合もありますが、ナチュラルキーを使いつつ、どうしてもナチュラルキーが使えない場合のみサロゲートキーを使用するという方針もあります。具体的には以下のような場合です。

  • ナチュラルキーではレコードを一意に特定できない
  • レコードインサート時にナチュラルキーが設定されないケースがある
  • 要件や処理の都合でナチュラルキーが一意にならない/一意にならない瞬間がある

主キーにすることで、NOT NULL制約と一意性制約が付与されるため、同じ値が存在しないことを保証できますが、前述のように社員番号ではなく社員IDを主キーとして使用すると、社員番号が重複してしまう可能性があるという問題が起こるため、1件だけ取得したつもりが2件以上返ってきてしまうといった問題につながります。対策として一意性制約を付与するという方法がありますが、意外と忘れがちだったります。

こういったことから、基本的にはナチュラルキーを主キーにできないかを検討し、どうしても難しい場合はサロゲートキーを使用するのがよいでしょう。

外部キー(Foreign Key)

今回の内容とは関係ありませんが、外部キーというキーもあります。

外部キーは、ある関係の候補キーの属性または属性の組を参照するキーです。外部キーは、関係間の関係性を定義し、データの整合性を保証する役割を果たします。

  • 関係間の関係性をあらわす
    ある関係が他の関係のデータを参照することによって、関係間にリレーションを形成します。
  • 一貫性の保証
    参照元の関係(親)に存在しないデータを参照することができなくなります。

まとめ

関係データベースでは、

  • スーパーキー
  • 候補キー
  • 主キー
  • 代替キー

というキーがあり、それらは階層化された関係を持ちます。また、これらのキーと直交する概念として、

  • ナチュラルキー
  • サロゲートキー

というものもあります。

これらの用語の違いを理解することで、キーの設計を円滑に進められるでしょう。

[PostgreSQL]外部キー制約を含むテーブルをDROPするとデッドロックが発生する

PostgreSQLでDROP TABLEを行うときにデッドロックが発生するという事象がありました。私自身が遭遇したわけではないですが、Liquibaseで使用していないテーブルをDROP TABLEしようとしたときに、デッドロックが発生したようです。

そんなわけはないと思いましたが、どうやら発生するケースがあるようです。

以下の記事を参考に調査しました。この記事ではFlywayを使っていますが、Liquibaseでも同様の事象が発生しますし、こういったマイグレーションツールを使っていなくてもBEGINEND内でDROP TABLEするケースでは発生し得る事象になります。

外部キー制約を外すときに参照先のテーブルに対してAccessExclusiveLockを獲得している

問題の根元は外部キー制約を外すときに参照先のテーブルに対してAccessExclusiveLockを獲得している点あるようでした。これを確認するために、以下の2つのテーブルを作成します。

親テーブルであり、外部キー制約における参照先のテーブルにあたるcompaniesテーブルを作成します。

CREATE TABLE companies (
    id SERIAL NOT NULL,
    name VARCHAR,
    PRIMARY KEY (id)
);

次に子テーブルであり、外部キー制約を設定するusersテーブルを作成します。

CREATE TABLE users (
    id SERIAL NOT NULL,
    name VARCHAR,
    company_id INTEGER NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (company_id) REFERENCES companies (id)
);

このDDLによって作成される外部キーの名前を確認します。

SELECT
    table_name, constraint_name
FROM
    information_schema.table_constraints
WHERE
    table_schema = 'public'
    AND
    constraint_type = 'FOREIGN KEY';

このSQLを実行すると、以下のような結果が得られます。

table_nameconstraint_name
usersusers_company_id_fkey

外部キーの名前はあとで外部キー制約をDROP CONSTRAINTするときに使用します。

外部キー制約を外すときにAccessExclusiveLockを獲得していることを確認する

では、外部キー制約を外すときにAccessExclusiveLockを獲得していることを確認しましょう。

確認のために、トランザクション内で実行するようにします。これは、今回遭遇したケースではトランザクション内でDDLを実行していたため、同じようにするためにトランザクションを作成しています。

BEGIN;
SELECT locktype, mode FROM pg_locks WHERE pid = pg_backend_pid() AND relation = 'companies'::regclass;

トランザクションを作成した時点では、親テーブルのcompaniesテーブルに対してロックは獲得されていません。

locktypemode

次に外部キー制約をDROP CONSTRAINTしてからもう一度ロックが獲得されているかを確認してみます。

ALTER TABLE users DROP CONSTRAINT users_company_id_fkey;
SELECT locktype, mode FROM pg_locks WHERE pid = pg_backend_pid() AND relation = 'companies'::regclass;

今度はAccessExclusiveLockが獲得されていることを確認できました。

locktypemode
relationAccessExclusiveLock

公式マニュアルによると、AccessExclusiveLockFOR UPDATEがないSELECT文もブロックするため、オンライン稼働中に外部キーを削除するとデッドロックが発生する場合があります。

例えば、外部キーを2つ持つ以下のテーブルを考えてみます。

CREATE TABLE companies (
    id SERIAL NOT NULL,
    name VARCHAR,
    PRIMARY KEY (id)
);

CREATE TABLE organizations (
    id SERIAL NOT NULL,
    name VARCHAR,
    company_id INTEGER,
    PRIMARY KEY (id),
    FOREIGN KEY (company_id) REFERENCES companies (id)
);

CREATE TABLE users (
    id SERIAL NOT NULL,
    name VARCHAR,
    company_id INTEGER NOT NULL,
    organization_id INTEGER  NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (company_id) REFERENCES companies (id),
    FOREIGN KEY (organization_id) REFERENCES organizations (id)
);

CREATE TABLE products (
    id SERIAL NOT NULL,
    name VARCHAR,
    company_id INTEGER NOT NULL,
    PRIMARY KEY (id),
    FOREIGN KEY (company_id) REFERENCES companies (id)
);

このとき、2つのトランザクションを考えます。

  • トランザクション①
    productsテーブルをDROP TABLEするトランザクションです。一応、この例ではproductsテーブルはまだ使われていないテーブルということで、オンライン稼働中にDROP TABLEしても影響はないだろうという考えのもとでオンライン稼働中に実行しています。
  • トランザクション②
    usersテーブルにcompaniesテーブルとorganizationsテーブルをJOINして検索するトランザクションです。これはオンラインで頻繁に実行されるSQLです。

次のような流れで実行されるとデッドロックが発生します。

  1. トランザクション①でcompaniesテーブルへの外部キーが削除され、companiesテーブルへのAccessExclusiveLockが獲得されます。
  2. トランザクション②でusersテーブルとorganizationsテーブルに対するAccessShareLockが獲得されます。
  3. トランザクション①でorganizationsテーブルへの外部キーを削除しようとしますが、トランザクション②がすでにorganiztionsテーブルに対するAccessShareLockを獲得しているため、トランザクション①はAccessExclusiveLockが獲得できずに待ちになります。
  4. トランザクション②はcompaniesテーブルに対するAccessShareLockを獲得使用としますが、トランザクション①がAccessExclusiveLockを獲得しているため、AccessShareLockが獲得できずに待ちになります。(ここでデッドロックが発生)

発生パターンは他にも考えられますが、使われていないテーブルであっても外部キー制約を持つテーブルの削除を行うだけでデッドロックが発生するという点が最も注意すべき点だと思います。

ではどうすればよいか

参考にした記事とも重複しますが、以下のような対策を行うのがよいでしょう。

  1. 外部キー制約をもったテーブルをDROP TABLEする際には、複数のリソースを扱わないようにトランザクションを分ける(Liquibaseの場合は外部キー制約を解除するDDL(1外部キー制約1changelog)とDROP TABLEをするDDLのchangelogをわける)
  2. DDLとDMLを1つのトランザクション内で実行しない(Liquibaseならchangelogを分ける)
  3. 大量更新(登録・更新・削除)を行うテーブルを参照している場合、その処理が行われているタイミングを避けて実行する

外部キー制約を使っているプロジェクトの場合、使っていないテーブルだからといってテーブル定義を綺麗にするために安易にDROP TABLECREATE TABLEによる再作成を選択しないように慎重に検討すべきです。

どうしてもDROP TABLEをしたい場合は1DDL1トランザクションになるように分けて、先に外部キー制約を解除し、それからテーブルをDROP TABLEするようにした方がよいでしょう。

最後に

最後にデッドロックが発生した場合にどのように対応していくかについて記載しておきます。

以下のSQLを実行すると、ロックが発生しているトランザクション一覧が表示されます。

SELECT
    pg_stat_activity.pid,
    pg_stat_activity.usename,
    pg_stat_activity.query,
    pg_class.relname AS table_name,
    pg_locks.locktype,
    pg_locks.mode,
    pg_locks.granted,
    pg_stat_activity.state,
    pg_stat_activity.application_name
FROM
    pg_stat_activity
JOIN
    pg_locks
    ON pg_stat_activity.pid = pg_locks.pid
LEFT JOIN
    pg_class
    ON pg_class.oid = pg_locks.relation
WHERE
    pg_locks.locktype = 'relation'
    AND
    pg_class.relname NOT LIKE 'pg_%'
ORDER BY
    pg_stat_activity.pid;

application_nameなどをもとに、どのpidが問題になっているかを特定し、

SELECT pg_cancel_backend(<killしたいpid>);

で実行できます。例えば、pidが62なら

SELECT pg_cancel_backend(62);

のように書きます。

これでもkillできない場合は、

SELECT pg_terminate_backend(<killしたいpid>)

でkillします。

[CSS]疑似クラス:first-child、:last-child、:nth-childを活用して特定の要素にスタイルを適用しよう

ウェブサイト制作をしていると、繰り返し登場する任意の要素にスタイルを適用したい場面が多々あります。特に、リストやテーブルの特定の項目に異なるデザインを施す際に便利なのがCSSの疑似クラスです。この記事では、:first-child:last-child:nth-childの疑似クラスを活用し、特定の要素にスタイルを適用する方法を学びます。

疑似クラスとは

疑似クラスは、CSSでHTML要素に追加のクラスを付けなくても特定の条件に基づいてスタイルを適用するためのものです。これにより、HTMLを変更せずにスタイル制御が可能になります。例えば、リスト項目の最初や最後の要素に特別なスタイルを適用することができます。

疑似クラスを使うとコードが簡潔になり、HTMLとCSSの分離が保たれるため、メンテナンス性が向上します。

:first-child疑似クラス

:first-child疑似クラスは、親要素の中で最初の子要素を選択するための擬似クラスです。

.container :first-child {
  ...
}

このように書くことで、containerクラスの子要素のうち、最初の子要素を選択することができます。

:last-child疑似クラス

:last-child疑似クラスは、親要素の中で最後の子要素を選択するための擬似クラスです。

.container :last-child {
   ...
}

このように書くことで、containerクラスの子要素のうち、最後の子要素を選択することができます。

:nth-child疑似クラス

:nth-child疑似クラスは、親要素の中でn番目の子要素を選択するための擬似クラスです。nの部分には、数字やキーワード、または数式が入ります。

.container :nth-child(2) {
  ...
}

このように書くことで、containerクラスの子要素のうち、2番目の子要素を選択することができます。

:nth-last-child疑似クラス

:nth-child疑似クラスは、:nth-childの逆順で選択するための疑似クラスです。

.container :nth-last-child(2) {
  ...
}

このように書くことで、containerクラスの子要素のうち、最後から2番目の子要素を選択することができます。

:not疑似クラス

:first-child疑似クラス、:last-child疑似クラス、:nth-child疑似クラス、:nth-last-child疑似クラスの選択を否定したい場合、:not疑似クラスを使用します。

.container :not(:first-child) {
  ...
}

このように書くことで、containerクラスの子要素のうち、最初の要素以外を選択することができます。

要素の適用に対する注意点

疑似クラスの適用範囲について、気をつけておいた方がよい点が2つあります。

親要素と子要素の関係性

例えば、次のようなHTMLを考えてみます。

<div class="container">
  <div class="item">1-1</item>
  <div class="item">1-2</item>
  <div class="item">1-3</item>
</div>
<div class="container">
  <div class="item">2-1</item>
  <div class="item">2-2</item>
  <div class="item">2-3</item>
</div>
<div class="container">
  <div class="item">3-1</item>
  <div class="item">3-2</item>
  <div class="item">3-3</item>
</div>

このHTMLに対して、

.item:nth-child(odd) {
  color: red;
}

を適用すると、どの文字が赤字になるでしょうか?

実際にやってみると一目瞭然ですが、

のように親要素.containerごとに子要素.itemの奇数番目の要素が赤字になることを確認することができます。同じ子要素の指定だとしても親要素を超えて処理されないという点に注意が必要なため、誤解や誤認を避けるために

.item:nth-child(odd) {
  color: red;
}

のように子要素だけ定義するのではなく、

.container .item:nth-child(odd) {
  color: red;
}

のように、親要素 子要素:疑似クラスのように必ず親要素をつけて定義する方がよいと思います。

以降の説明でも、親要素をつけて定義する形式でスタイルを定義するようにしています。

複数種類の子要素が混在している場合

テーブルのように単一の子要素のみで構成される場合は問題ありませんが、複数種類の子要素が混在している状況では期待したとおりにスタイルが適用されない場合があります。

例えば、次のようなHTMLを考えてみます。

<div class="container">
  <h2>タイトル</h2>
  <p class="phrase">1行目</p>
  <p class="phrase">2行目</p>
  <p class="phrase">3行目</p>
  <p class="phrase">4行目</p>
  <p class="phrase">5行目</p>
</div>

phaseクラスが定義されたp要素以外にh2要素があります。

このHTMLに対して次のスタイルを適用します。

.container .phrase:nth-child(2n) {
  color: red;
}

このスタイルでは、phaseクラスが適用された要素の偶数行目の文字を赤字にします。

実際にやってみると、期待したとおりの結果になりませんでした。疑似クラスを使用したとき、その要素が何個目であるかは指定したクラス(今回はphraseクラス)の中で何個目かではなく、親要素から見たときに何個目の要素かで決まります。

すなわち、タイトルが1個目、1行目が2個目、2行目が3個目、・・・という風に数えられ、その中で指定したクラス(今回はphraseクラス)にのみスタイルが適用されます。

このことを確認するために、:first-child疑似クラスを適用してみて色が変わらないこと、:last-child疑似クラスを適用してみて色が変わることを確認しましょう。

まずは、:first-child疑似クラスを確認します。

.pattern .phrase:first-child {
  color: red;
}

:first-child疑似クラスで指しているのはタイトルで、phaseクラスが適用されていないため、色は変わりませんでした。

次に、:last-child疑似クラスを確認します。

.pattern .phrase:last-child {
  color: red;
}

:last-child疑似クラスで指しているのは4行目で、phaseクラスが適用されているため、色は変わりました。

1つの親要素の中に複数種類の子要素が混在する場合、何個目かは要素の種類に関係なく親要素から見て何個目かで数える点に留意してください。

パターン別

今回はすべて、以下のHTMLに対して適用するパターンで説明します。

<!DOCTYPE html>
<html lang="en">
  <body>
    <div class="container">
      <div class="item"></div>
      <div class="item"></div>
      <div class="item"></div>
      <div class="item"></div>
      <div class="item"></div>
    </div>
  </body>
</html>

スタイルが適用されている要素はアクアで色づけし、スタイルが適用されていない要素はグレーのままになるようにします。

最初の要素にのみ適用

最初の要素にのみ適用したい場合は、:first-child疑似クラスを使用します。

.container .item:first-child {
  background-color: aqua;
}

最後の要素にのみ適用

最後の要素にのみスタイルを適用したい場合は、:last-child疑似クラスを使用します。

.container .item:last-child {
  background-color: aqua;
}

最初と最後の要素に適用

:first-child疑似クラスと:last-child疑似クラスを組み合わせることで、最初と最後のようにスタイルを適用することができます。

.container .item:first-child,
.container .item:last-child {
  background-color: aqua;
}

最初の要素以外に適用

最初の要素以外にスタイルを適用したい場合、:first-child疑似クラスとそれを否定する:not疑似クラスを組み合わせることで実現できます。

.container .item:not(:first-child) {
  background-color: aqua;
}

最後の要素以外に適用

最後の要素以外にスタイルを適用したい場合は、:last-child疑似クラスとそれを否定する:not疑似クラスを組み合わせることで実現できます。

.container .item:not(:last-child) {
  background-color: aqua;
}

最初と最後の要素以外に適用

最初と最後の要素以外にスタイルを適用したい場合、:first-child疑似クラス、:last-child疑似クラス、:not疑似クラスを組み合わせて実現します。こちらは最初の要素以外 かつ 最後の要素以外というAND条件になりますので、OR条件の場合とは書き方が異なります。

.container .item:not(:first-child):not(:last-child) {
  background-color: aqua;
}

偶数個目の要素に適用

テーブルの行をストライプにするときによく使われるのが偶数個目または奇数個目の要素にスタイルを適用する方法です。これには:nth-child疑似クラスを使用します。この書き方には2つあるのでそれぞれ説明します。

まずはnを使って書く方法です。nを使って偶数をあらわすためには2nと書きます。

.container .item:nth-child(2n) {
  background-color: aqua;
}

もう少し簡単に書く方法があります。偶数をあらわすevenを使います。

.container .item:nth-child(even) {
  background-color: aqua;
}

奇数個目の要素に適用

奇数個目の要素に適用する方法も2つあります。

まずはnを使って書く方法です。nを使って奇数をあらわすには2n+1と書きます。

.container .item:nth-child(2n+1) {
  background-color: aqua;
}

偶数のときと同じように簡単に書く方法があります。奇数をあらわすoddを使います。

.container .item:nth-child(odd) {
  background-color: aqua;
}

特定の要素に適用

:nth-child疑似クラスを使うことで特定の要素にスタイルを適用することができます。

一例として、3個目の要素にのみ適用する場合を考えてみます。:nth-child疑似クラスに3を指定します。

.container .item:nth-child(3) {
  background-color: aqua;
}

ただし、:nth-child疑似クラスは万能ではありません。書けないケースや書き方に工夫が必要なケースもあります。

以下のようなことは:nth-child疑似クラスで実現可能です。

  1. 特定の順序の要素を選択
    • :nth-child(2)は、親の中で2番目に位置する子要素を選択します
  2. 特定のパターンに基づく要素を選択
    • :nth-child(odd)または:nth-child(2n+1)は、奇数番目の要素を選択します
    • :nth-child(even)または:nth-child(2n)は、偶数番目の要素を選択します
    • :nth-child(3n)は、3番目、6番目、9番目など、3の倍数の位置にある要素を選択します
  3. 複雑なパターンで要素を選択
    • :nth-child(3n+1)は、1番目、4番目、7番目など、3つおきに要素を選択します
    • :nth-child(-n+3)は、最初の3つの要素(1番目、2番目、3番目)を選択します
  4. 他の疑似クラスとの組み合わせ
    • :nth-child(2):hoverのように、特定の順序にある要素がホバーされたときのスタイルを指定できます
  5. 特定のタグに対して使用
    • p:nth-child(3)のように、特定のタグ(例えばp要素)が兄弟要素の中で指定された順序に位置する場合にスタイルを適用できます

一方で以下のようなことは:nth-child疑似クラスではで実現できません。

  1. 特定のクラスや属性を持つ要素を選択
    • 前述の注意点でも挙げたとおり、:nth-child疑似クラスは要素の順序に基づいているため、クラス名や属性を無視します。例えば、p.special:nth-child(2)のように、特定のクラスを持つ2番目の要素を選択することはできません
  2. 逆順の選択
    • :nth-child疑似クラスは要素の逆順での選択はできません。例えば、末尾から2番目の要素を選択することはできません。ただし、:nth-last-child疑似クラスを使用することで実現可能です
  3. 特定の要素タイプに対してだけの選択
    • :nth-child疑似クラスは指定された順序にあるすべての子要素を対象とするため、特定の要素タイプだけを選択することはできません。例えば、特定の順序にある div要素だけを選択することはできません。div:nth-child(2)は、2番目の子要素がdiv要素である場合にのみ適用されます
  4. an+bの形式であらわせない要素を選択
    • :nth-child疑似要素では、2などの特定値での指定、evenまたはoddan+bの形式(abは省略可)のいずれかの方法でしか指定できません。そのため、可変個数の要素のちょうど真ん中の要素に対してスタイルを適用するということはできません

an+bの形式であらわせない形式であっても工夫によって表現可能な場合があります。例えば、1番目と4番目の要素に対してスタイルを適用したい場合、

.container .item:nth-child(1,4) {
  background-color: aqua;
}

とは書けませんが、

.container .item:nth-child(1),
.container .item:nth-child(4) {
  background-color: aqua;
}

と書くことで実現できます。

まとめ

:first-child疑似クラス、:last-child疑似クラス、:nth-child疑似クラス(または:nth-last-child疑似クラス)および:not疑似クラスを使用することで、一部制約はあるものの様々要素を指定することができるようになります。

テーブルをストライプ模様にする以外にも、最初と最後の要素以外にマージンを設けたり、最初の要素だけデザインを変えたりなど、工夫によって様々なことができるようになります。

指定方法には少し注意すべき点がありますが、活用することでデザインの幅はさらに広がることでしょう。

マージ済みのローカルブランチを一括削除する

プログラミングをしていて、作業ごとにブランチを作成しているので、ローカルブランチが溜まりがちです。

ChatGPTに毎回コマンドを聞くのも申し訳ないので、操作をまとめておきます。

マージ済みのブランチを確認する

この操作の肝になるのが、マージ済みのブランチを一覧表示することです。

$ git branch --merged

このコマンドではマージ済みのブランチを一覧表示できます。このコマンドでは現在のブランチも含まれており、現在のブランチを示す*がついています。

また、mainブランチも含まれている点に注意が必要です。

現在のブランチ、mainブランチを除く

現在のブランチを削除できないのでこれを除外します。また、mainブランチは削除されると困るのでこれも除外します。grep -vで除外できるのでこれを使って除外します。  

$ git branch --merged | grep -v "\*" | grep -v "main"

もし、mainブランチではなくmasterブランチであれば、

$ git branch --merged | grep -v "\*" | grep -v "master"

としてください。

マージ済みのブランチを削除する

マージ済みのブランチを削除するには、git branch -dを使用します。-Dを使用すると未マージのブランチを削除できてしまうので、-dを使う点に十分注意してください。

$ git branch -d <削除するブランチ名>

前述のマージ済みブランチのリストをgit branch -dに渡すにはxargsコマンドを使用します。

$ git branch --merged | grep -v "\*" | grep -v "main" | xargs -n 1 git branch -d

マージ済みのブランチは

$ git branch --merged
  bug/bad-request-when-exception-occurs
  bug/remove-pysqlite3-from-pyproject
  bug/update-readme-md
  doc/add-codacy-badge
  doc/update-doc-and-readme
  doc/update-document
  enhance/enhancing-the-way-router-is-set-up

のようにブランチ名だけがリストされ、このブランチ名をgit branch -dコマンドに渡せばいいので、-n 1(渡す標準入力は1つ)を指定しています。

まとめ

頻繁に使用するコマンドは忘れませんが、時々しか使わないコマンドは忘れがちです。ChatGPTなどに訪ねればすぐに答えを教えてくれるので、それほど困ることはありませんが、以下のようにコマンド化しておけばすぐに使えます。(コマンドを忘れるという可能性はありますが)

# Git Branch Clear
gbc() {
  git branch --merged | grep -v "\*" | grep -v "main" | xargs -n 1 git branch -d
}

ローカルブランチは容量を圧迫するような類いのものではありませんが、あまり溜まりすぎると探しにくくなったりするの、個人的には綺麗にしておきたいと思っています。

プルリクのマージ後に削除すればいいのですが、どうしても忘れてしまうので、思い立ったときに一括削除できるようにしておくくらいがちょうどいいと思います。

REST API特化のWebアプリケーションフレームワーク「Pykour」のご紹介

本日、v0.1.4をリリースし、それなりに動作するようになったので、私が開発中のWebアプリケーションフレームワーク「Pykour」についてご紹介します。

Pykourとは

REST APIを作成することに特化したPython向けWebアプリケーションフレームワークです。FlaskやFastAPIを参考に作成しているので、現時点ではREST APIしか作れないFastAPIのようなフレームワークになっています。

v0.1.xでは機能追加や機能変更を行うベータ版とし、v0.2.0から正式版としてリリースする計画としています。

Pykourの使い方

まずは基本的な使い方について説明します。

インストール

Pykourのインストールは他のフレームワークと同様にpipコマンドでインストールできます。

$ pip install pykour

pykourパッケージをインストールすると、pykourコマンドが使用可能になります。

$ pykour -v
Pykour v0.1.4

main.pyという名前で以下のプログラムを作成します。

from pykour import Pykour

app = Pykour()

@app.get("/")
def home():
  return {"message": "Hello, World!"}

最も簡単なプログラムは、FastAPIとほとんど変わらない書き方になります。このプログラムを実行するには、pykour devコマンドを使います。

$ pykour dev main:app

pykour devコマンドではuvicornを使用し、pykour runコマンドではgunicorn+uvicorn-workerを使用して、ASGIサーバーを起動しています。

デフォルトではhttp://127.0.0.1:8000でサーバーが起動しますので、

$ curl http://127.0.0.1:8000/
{"message": "Hello, World!"}%

のように応答が返ってきます。

v0.1.4ではどこまでできるのか

v0.1.4では、以下のようなことができるようになっています。

ルーティングの設定

ルーティングの設定は、前述のようにPykourインスタンスに直接設定する方法と、

from pykour import Pykour

app = Pykour()

@app.get("/")
def home():
  return {"message": "Hello, World!"}

Routerインスタンスを作成して、それをPykourインスタンスに設定する方法もあります。

from pykour import Router

router = Router(prefix="/users")

@router.get("/")
def get_users():
  return {"message": "get users"}
from pykour import Pykour
from .routes import router

app = Pykour()
app.add_router(router)

スキーマの使用

リクエストボディはデフォルトでは辞書型にマッピングされますが、スキーマを作成して対応させることもできます。

from pykour.schema import BaseSchema

class UserSchema(BaseSchema):
  name: str
  age: int
from pykour import Pykour
from .schemas import UserSchema

app = Pykour()

@app.post("/")
def post_user(user: UserSchema):
  return {"name": user.name}

設定ファイルの使用

YAML限定ですが、設定ファイルを使用することもできます。

例えば、

foo:
  bar: bas

という設定ファイルをconfig.yamlというファイル名で作成した場合、Pykourインスタンス作成時に設定ファイルを指定することができます。

from pykour import Pykour

app = Pykour("config.yaml")

@app.get("/")
def home(config: Config):
  return {"message": config.get("foo.bar")}

設定ファイルはConfigクラスで扱うことができ、ルート関数の引数に指定しておくと、自動的に設定して呼び出してくれます。

データベースアクセス

まだ、sqlite3しか対応していませんが、データベースアクセスも可能です。

設定ファイルでデータベースの設定を行います。

pykour:
  database:
    type: sqlite
    url: pykour.db

設定ファイルを指定することでデータベースにアクセスできるようになります。

from pykour import Pykour
from pykour.db import Connection

app = Pykour("config.yaml")

@app.get("/")
def home(conn: Connection):
  return {"users": conn.select("SELECT * FROM users")}

今後の計画

まだまだ足りない機能や作りたい機能は結構ありますが、どう実装するべきかを考え中です。

  • ORMの実装
  • 複数のデータベースへの対応(まずはMySQLやPostgreSQL)
  • キャッシュ機能(処理を行わずにキャッシュしてあるレスポンスを返す)の実装

最近のFastAPIでは、スキーマとモデルを一体化していますが、リクエストのデータ構造とテーブルのデータ構造の違いを考えると分けた方がよいのではないかと思っています。ORMは確かによいのですが、JOINやカラムの編集などが出てくると途端に難易度が上がったりするので、どういったインターフェースにするか悩んでいます。

また、コード自体の改善やテストケースの改善など安定性の向上についても継続的に進めていきたいと思います。一応、機能を制限することで高いパフォーマンスを発揮することを目標としているので、ベンチマークをとる環境も構築していきたいです。

v0.1.5では複数のデータベースをサポートしつつ、コードの改善を進めていく予定です。

ON句に書くのは結合条件であって絞り込み条件ではないことに注意しよう

令和3年秋データベーススペシャリスト試験 午前Ⅱ問8にJOINのに関する問題が出題されていました。

問題

出題されているテーブルと登録されているデータは以下のとおりです。

CREATE TABLE 社員取得資格 (
    社員コード VARCHAR(4) NOT NULL,
    資格 VARCHAR(2)
);

INSERT INTO 社員取得資格 VALUES ('S001', 'FE');
INSERT INTO 社員取得資格 VALUES ('S001', 'AP');
INSERT INTO 社員取得資格 VALUES ('S001', 'DB');
INSERT INTO 社員取得資格 VALUES ('S002', 'FE');
INSERT INTO 社員取得資格 VALUES ('S002', 'SM');
INSERT INTO 社員取得資格 VALUES ('S003', 'FE');
INSERT INTO 社員取得資格 VALUES ('S004', 'AP');
INSERT INTO 社員取得資格 VALUES ('S005', NULL);

INSERTした結果は以下のようになります。問題に記載されている範囲ではPKを持たないテーブルになります。

社員コード資格
S001FE
S001AP
S001DB
S002FE
S002SM
S003FE
S004AP
S005NULL

出題されている問題は、以下の結果が得られるようにするために、

社員コード資格1資格2
S001FEAP
S002FENULL
S003FENULL

[ a ]に入る字句を答える問題になります。

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    LEFT OUTER JOIN 社員取得資格 C2
[ a ]

この問題の正解は、

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    LEFT OUTER JOIN 社員取得資格 C2
    ON C1.社員コード = C2.社員コード
      AND C1.資格 = 'FE' AND C2.資格 = 'AP'
WHERE
    C1.資格 = 'FE'

になりますが、不正解の選択肢と共通している字句は

    ON C1.社員コード = C2.社員コード
      AND C1.資格 = 'FE' AND C2.資格 = 'AP'

で、自己結合のために使用するC1.社員コード = C2.社員コード以外にC1.資格 = 'FE' AND C2.資格 = 'AP'という条件があるようです。

ON句の条件が結果にどのように影響するのか

ON句に結合条件以外を指定するとどのように振る舞うのかを確認してみたいと思います。

自己結合すると何が得られるのか

まずは、社員コードで自己結合すると何が得られるかを確認してみましょう。

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    LEFT OUTER JOIN 社員取得資格 C2
    ON C1.社員コード = C2.社員コード

このSQLを実行すると、以下のような結果が得られます。

社員コード資格1資格2
S001FEDB
S001FEAP
S001FEFE
S001APDB
S001APAP
S001APFE
S001DBDB
S001DBAP
S001DBFE
S002FESM
S002FEFE
S002SMSM
S002SMFE
S003FEFE
S004APAP
S005NULLNULL

当然ですが、同じ社員コードのレコード同士のすべての掛け合わせが表示されます。この中には自分自身のレコードも含まれています。

ON句にC1.資格 = ‘FE’を指定するとどうなるのか

ON句には結合条件を指定するのですが、ここにC1.資格 = 'FE'を指定するとどうなるのでしょうか?

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    LEFT OUTER JOIN 社員取得資格 C2
    ON C1.社員コード = C2.社員コード
      AND C1.資格 = 'FE'

C1のうち資格 = 'FE'のみ結合できるようにする条件となるため、FE以外の資格の行は社員コードが一致しても結合されなくなります。

社員コード資格1資格2
S001FEDB
S001FEAP
S001FEFE
S001APNULL
S001DBNULL
S002FESM
S002FEFE
S002SMNULL
S003FEFE
S004APNULL
S005NULLNULL

資格1がFEの行については資格2にすべての組み合わせが設定されていますが、資格1がFE以外の行については資格2がNULLになっていることがわかります。

ONにC2.資格 = ‘AP’を指定するとどうなるのか

一旦、C1.資格 = 'FE'の条件を外して、C2.資格 = 'AP'の条件を指定するとどうなるでしょうか?

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    LEFT OUTER JOIN 社員取得資格 C2
    ON C1.社員コード = C2.社員コード
      AND C2.資格 = 'AP'

C2のうち、資格='AP'のみ結合できるようにする条件となるため、AP以外の資格の行は社員コードが一致しても結合されなくなります。結合は左外部結合(LEFT OUTER JOIN)で行われているため、先ほどと異なり、結合相手のC2の行がない資格1の行は1行だけ残り、資格2はNULLになります。

社員コード資格1資格2
S001FEAP
S001APAP
S001DBAP
S002FENULL
S002SMNULL
S003FENULL
S004APAP
S005NULLNULL

APを資格として保有している社員のみ資格1と資格2(AP固定)が表示され、APを資格として保有していない社員は資格1のみ表示されます。

ON句にC1.資格 = ‘FE’ AND C2.資格 = ‘AP’の両方を指定するとどうなるのか

これまでの内容を踏まえると、あくまでも結合対象となる条件を指定しているだけですので、社員コードが一致するC1.資格 = 'FE'の行とC2.資格 = 'AP'が結合されます。このとき、左外部結合(LEFT OUTER JOIN)で結合しているため、C1の行は最低1行は残ります。

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    LEFT OUTER JOIN 社員取得資格 C2
    ON C1.社員コード = C2.社員コード
      AND C1.資格 = 'FE'
      AND C2.資格 = 'AP'

C1の資格がFEでない行はC2が結合されず、C2APでなければC1FEでも結合されないため、資格1がFEでない行は資格2はNULLになり、資格2がAPになっている行は資格1がFEになります。

社員コード資格1資格2
S001FEAP
S001APNULL
S001DBNULL
S002FENULL
S002SMNULL
S003FENULL
S004APNULL
S005NULLNULL

WHERE句でC1.資格 = ‘FE’の行だけフィルタする必要がある 

ここまで見てみると資格1にはFE以外の行があることがわかります。これは左外部結合(LEFT OUTER JOIN)で結合したことでC1の行が消えないためです。得られるべき結果の資格1はFEのみになっているため、FE以外を除外するようにフィルタする必要があります。

これを実現するにはC1.資格 = 'FE'の条件をWHERE句に追加することで実現します。

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    LEFT OUTER JOIN 社員取得資格 C2
    ON C1.社員コード = C2.社員コード
      AND C1.資格 = 'FE'
      AND C2.資格 = 'AP'
WHERE
    C1.資格 = 'FE'
社員コード資格1資格2
S001FEAP
S002FENULL
S003FENULL

代替案の検討

INNER JOINに変更すればWHERE句は不要か

左外部結合(LEFT OUTER JOIN)によって余計な行が残るという話をしてきましたが、内部結合(INNER JOIN)に変更したらどうなるでしょうか?

WHERE句を含まないINNER JOINで自己結合したSQLを作成して実行してみます。

SELECT
    C1.社員コード, C1.資格 AS 資格1, C2.資格 AS 資格2
FROM
    社員取得資格 C1
    INNER JOIN 社員取得資格 C2
    ON C1.社員コード = C2.社員コード
      AND C1.資格 = 'FE'
      AND C2.資格 = 'AP'

この条件ではFEAPの両方の資格を保有している社員の行のみが表示されるようになります。

社員コード資格1資格2
S001FEAP

結合しないで同じ結果を得ることは可能

左外部結合を使った場合は、FEの資格を保有している社員を表示し、APの資格も保有していたら表示する、という条件をあらわしています。

実務でこのようなSQLがあるかといわれるとそれほど多くないと思いますが、私が書くなら結合せずに書くと思います。なぜなら、ON句内に結合条件以外の条件を書くことは、可読性が高くなく、誤った解釈をするリスクがあるためで、ON句には2つのテーブル結合するためのキー情報のみ書きたいと考えているからです。絞り込み条件に見える条件は極力WHERE句に書くようにすることで、SQLの初心者であっても読み間違えないようにすることが重要だと考えています。

では、どうするかというと結合せずにCASE句を使って資格2の条件を記載します。

SELECT
    C1.社員コード,
    C1.資格 AS 資格1,
    CASE
        WHEN EXISTS (SELECT 1 FROM 社員取得資格 C2 WHERE C1.社員コード = C2.社員コード AND C2.資格 = 'AP') THEN 'AP'
        ELSE NULL
    END AS 資格2
FROM
    社員取得資格 C1
WHERE
    C1.資格 = 'FE'

このSQLでは必須となるFEを保有している社員を単純に抽出し、資格2には同社員がAPを保有していれば'AP'と出力し、保有していなければNULLを出力するようにしています。

あくまでもFEを保有している社員を抽出することが主眼とし、+αの情報としてAPを保有しているかを抽出するようにSQLを書いています。

まとめ

ON句にキーの結合条件以外の条件が出てくるとドキッとしますが、ひとつひとつ丁寧に確認していくことでそれが結合する条件であることがわかってきます。

試験対策としてこのような動作は実機で確認して理解しておくことは重要ですが、実務においては誤解されないかを十分に確認した上で使った方がよいと思います。ON句で書くこととWHERE句で書くことは等価な場合もありますが、等価でない場合もありますので、意図した結果を得られるのかを十分に検証してください。

テキストの改行処理は多くの場合デフォルトでいいかもしれない

ここ最近、テキストが折り返されずにはみ出るとかテキストが折り返されるがエリアからはみ出るといった不具合に関わることが多かったので、少し調べてみました。

結論

比較検証を色々してもよかったのですが、結論がかなりシンプルなので結論だけ書いていきます。

基本的にはoverflow-wrapword-breakは不要

多くのケースではoverflow-wrapword-breakによる改行処理は不要だと思います。親要素で幅を制限し、データ量に応じて高さが変わるようになっているのであれば、自然に改行してくれます。

対象が文章であっても禁則処理まで求められるケースは多くない印象です。

テストデータやダミーデータには注意が必要

abc...」のような英字の羅列は1つの単語と見なして途中では折り返されません。テスト時やダミーデータを登録する場合などにこのような文字列を設定しがちですが、想定外の文字列を設定しているのであればバグではないため、設定する文字列を見直しましょう。

コードやIDには注意

コードやIDが固定長であれば折り返さずに表示できる幅を確保するようにデザインすることを検討してください。コードやIDの視認性を考えると折り返さない方が読みやすくなります。

一方で、コードやIDが可変長であったり、複数のコードやIDが同じエリア内に混在するような場合、一部のコードやIDが想定した幅を超えることがあります。このような場合、折り返しを実現する方法として、word-break: break-allの使用を検討してください。break-allを指定した場合、単語の途中でも折り返しますが、コードやIDであれば機械的に端で折り返す方が適していると思います。

複数のシステムと関連していたり、歴史的な理由でコードなのに可変ということは結構あります。現行システムのデータなどを分析して複数の桁のデータが存在していることが確認できた場合は、何桁までを折り返さずに表示できるようにするとよいのか確認した方がよいと思います。

利用者の特性によっては一部の項目にword-break: break-allの使用を検討する

利用者によっては一部の項目にword-break: break-allの使用を検討しなければならないかもしれません。業種、企業風土、利用者のWebリテラシーなど様々な要因によって決まるため一概には言えませんが、主に備考欄や特記事項欄のようなフリー入力欄ではテストデータやダミーデータで入力されがちな自然に折り返すことができないテキストを入力する場合がありますので、word-break: break-allの使用可否を検討することをおすすめします。

テキストの省略には注意が必要

すべての文字がエリア内に収まらない場合、CSSを使って「...」で省略することができます。ただし、省略することで区別不可能になる場合があるため、使いどころには注意が必要です。

例えば、「東京都中央区第一支社」「東京都中央区第二支社」という文字列がある場合に、これを幅の問題で「東京都中央区第…」のように省略しなくてはならないケースを考えてみます。この場合、省略されていると「第一支社」なのか「第二支社」なのか区別がつきません。テキストにマウスカーソルを当てるとすべてのテキストがツールチップで表示する場合もありますが、想定内の操作をしているだけで、マウスカーソルを当てるという余計な操作を余儀なくされるのは、利用者にとっては有益なことではないと思います。

省略する場合、JavaScriptなどで「東京都…第一支社」のように省略する方法も検討した方がよいかもしれません。

まとめ

テキストが折り返されずにエリアを貫通する問題はほとんど問題にならないと考えてよさそうですが、テスト時やダミーデータ設定時には正しくデータを想定して設定することが重要です。

また、一部の状況においてはword-break: break-allの使用を検討する必要がありますが、とりあえず設定するのではなく、十分検討した上で設定するようにするとよいでしょう。

BunとBiomeで開発環境を構築する

JavaScriptではなくTypeScriptで開発することが多く、そのためのセットアップの手間がそれなりに必要となります。BunというNode.jsランタイムがデフォルトでTypeScriptに対応しているので、環境構築してみました。

ついでにBiomeを使ってLinter、Formatterをセットしていきます。

Bunのインストール

macOSにBunをインストールをインストール方法はいくつかありますが、本記事ではHomebrewでインストールしていきます。

$ brew install oven-sh/bun/bun

それ以外のインストール方法については公式の記事を参照してください。

Biomeのインストール

macOSにBiomeをインストールするにはHomebrewを使います。

$ brew install biome

biomeコマンドが使用可能になるため、プロジェクト個別にBiomeパッケージをインストールしません。

プロジェクトの作成

Bunでプロジェクトを作成する方法にはbun initbun createの2つがあります。テンプレートを元にプロジェクトを作成する場合はbun createを使用し、空のプロジェクトを作成する場合はbun initを使用します。

今回は空のプロジェクトを作成するので、bun initを使用します。

$ mkdir bun-demo
$ cd bun-demo
$ bun init
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (bun-demo): 
entry point (index.ts): src/index.ts

Done! A package.json file was saved in the current directory.
 + index.ts
 + .gitignore
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.ts

質問は2つで、パッケージ名(package name)とエントリーポイント(entry point)です。特に変更する必要がなければそのままエンターキーで進めます。

今回はテストケースも作成するので、エントリーポイントはsrc/index.tsにします。

Biomeをセットアップする

Biomeを使用可能にするためのセットアップを行います。Visual Studio Codeで開発する場合、Biomeプラグインを使うのが簡単です。

Preferences>Settingsで設定を開き、Workspaceを選択後に右上のOpen Settings (JSON)をクリックすると、プロジェクトルートに.vscode/settings.jsonが作成されます。

このファイルに以下のように記述します。

{
    "[typescript]": {
        "editor.defaultFormatter": "biomejs.biome",
        "editor.formatOnSave": true,
    },
    "editor.codeActionsOnSave": {
        "source.organizeImports.biome": "explicit"
    }
}

環境によっては一部の設定が不要となる場合がありますが、

        "editor.defaultFormatter": "biomejs.biome",

で、デフォルトのフォーマッターをBiome拡張機能に変更し、

        "editor.formatOnSave": true,

で、変更を保存するときにフォーマットするようにします。

これらの設定はTypeScriptのファイルだけに適用したいため、

    "[typescript]": {
        "editor.defaultFormatter": "biomejs.biome",
        "editor.formatOnSave": true,
    },

のように[typescript]内に書くようにしています。

また、変更を保存するときにインポートを再編成するため、

    "editor.codeActionsOnSave": {
        "source.organizeImports.biome": "explicit"
    }

の設定を行っています。

プログラムを作成して実行する

実際にプログラムを作成して実行してみます。

この後テストしやすいようにmain関数からロジックを分けておきます。

export function greet(name: string): string {
	return `Hello, ${name}!`;
}

greet関数をテストしておきます。Bunが提供するテスティングフレームワークを使ってテストします。

import { expect, test } from "bun:test";
import { greet } from "../src/greet";

test("should return hello world", () => {
	expect(greet("world")).toBe("Hello, world!");
});

テストはbun testコマンドで実行可能です。

$ bun test
bun test v1.1.7 (b0b7db5c)

test/greet.spec.ts:
✓ should return hello world [0.41ms]

 1 pass
 0 fail
 1 expect() calls
Ran 1 tests across 1 files. [57.00ms]

MochaやJestと遜色ない感じで使えます。モック機能もあるのでサードパーティ製パッケージを使う必要はなさそうです。

作成したgreet関数を使ってmain関数を書きます。

import { greet } from "./greet";

function main(args: string[] = process.argv.slice(2)): void {
	console.log(greet("world"));
}

main();

プログラムの実行はbun runコマンドを使用します。

$ bun run src/index.ts
Hello, world!

実行するエントリーポイントを指定して実行します。

Lintの実行

フォーマットは変更の保存時に自動で行われるため、Lintを使う必要はほぼないと思いますが、一応Lintコマンドも確認しておきます。

LintはBiomeを使用します。

$ biome lint src/**/*.ts
Checked 2 files in 4ms. No fixes needed.

Formatterでフォーマットしているので修正が必要な箇所はありませんでした。

まとめ

Bunを使えば、コマンドのインストールを除けばかなり簡単にプロジェクトをセットアップできます。Biomeもセットアップが簡単なので

速度をウリにしている部分もありますが、もう少し大規模なアプリケーションを使わないとなんとも言えないと思いますし、ネットワークなどの方が大きなボトルネックになるので、Node.jsやDenoなどとそれほど際はないかもしれません。

今回作成したプロジェクトは以下になります。

Linuxコマンドの代替・上位互換コマンドを使ってみよう – z, eza, fd, rg, bat, tldr

Linuxコマンドの代替コマンド、上位互換コマンドを紹介する動画を見ていて、知らないコマンドばかりだったので、実際に使ってみました。

コマンド自体はすべてmacOSでインストールして試しています。公式サイトのURLを付けていますので、その他のプラットフォームの場合は公式サイトご確認ください。

z – cdの代替コマンド

zcdの代替コマンドです。cdコマンドと比べて移動に関する操作が強化されています。

インストール方法

macOSへインストールするにはHomebrewを使用します。

brew install zoxide

インストール後、~/.zshrcに以下を追記し、シェルを再起動します。

eval "$(zoxide init zsh)"

使用方法

コマンドが違うだけで基本的にはcdコマンドと同じように使用できます。ただ、前にいたディレクトリへの復帰の方法がとても強力です。

$ z foo
$ mkdir -p bar/baz
$ mkdir -p abc/def
# foo/bar/bazディレクトリに移動する
$ z bar/baz
# 直前にいたfooディレクトリに戻る
$ z -
# 移動したことのあるbazディレクトリに直接移動する
$ z baz
$ z -
$ z abc/def
# 移動したことがあるなら直前にいたディレクトリでなくても移動可能
$ z baz
$ z def

2つ以上のディレクトリを行き来するときとかは重宝しそうです。

eza – lsの代替コマンド

ezaはexaからフォークしたコマンドで、現在はこちらがメンテナンスされているようです。

ezalsの代替コマンドです。lsコマンドよりも高い可読性の表示が簡単にできるのが特徴です。

インストール方法

macOSへインストールするにはHomebrewを使用します。

brew install eza

使用方法

eza

eza -l

色つきや下線がついた見やすい表示になります。

fd – findの代替コマンド

fdfindの代替コマンドです。特定の文字列を含むファイル名やディレクトリ名を簡単に探すことができます。

インストール方法

macOSへインストールするにはHomebrewを使用します。

brew install fd

使用方法

複雑なオプションを使わずにファイルやディレクトリを検索できます。

# fooを含むファイル名、ディレクトリ名を現在のディレクトリから再帰的に検索する
$ fd foo
# ファイルのみを検索したいとき
$ fd --type f foo
# ディレクトリのみ検索したい場合
$ fd --type d foo
# fooを含むファイル名、ディレクトリ名をディレクトリbarを基点に再帰的に検索する
$ fd foo bar

findコマンドはオプションを色々覚えないといけないですし、環境やバージョンによってオプションが異なることがあるため、毎回調べる手間を大幅に削減できるのがうれしいですね。

rg – grepの代替コマンド

rggrepの代替コマンドです。ファイルの中に文字列をGrepすることに特化したコマンドで、サクラエディタのGrep機能をコマンド化したような使い心地です。

インストール方法

macOSへインストールするにはHomebrewを使用します。

brew install ripgrep

使用方法

オプションおよびコマンドの組み合わせなしでファイルの中身のGrepができます。

# 現在ディレクトリを基点に再帰的に'foo'が含まれる行を検索する
$ rg foo
# オプションなしで正規表現が使用可能
$ rg 'ba[rz]'
# 検索する基点となるディレクトリを指定可能(barディレクトリを基点に検索)
$ rg foo bar
# 拡張子を絞って検索することも可能
$ rg foo -g '*.css'

bat – catの代替コマンド

batcatの代替コマンドです。catコマンドよりも高機能かつ見やすい表示にしてくれます。

インストール方法

macOSへインストールするにはHomebrewを使用します。

brew install bat

使用方法

catと同じでファイル名を指定してファイルの中身を表示できるのは同じですが、moreコマンドなどと組み合わせる必要がなく、色も付けてくれるのでかなり見やすくなります。

tldr – manの補完コマンド

こちらは代替コマンドというよりは補完するコマンドだと言えます。tldrコマンドで調べて、それでも不足している場合はmanコマンドで調べるという使い方がよさそうです。

インストール方法

macOSへインストールするにはHomebrewを使用します。かつてはtldrという名前でインストールできていたようですが、現在はtlrcという名前でインストールします。

brew install tlrc

使用方法

manコマンドと同じようにコマンド名を引数にします。

すっきりとした見た目なので非常にわかりやすいですね。さっと調べたい場合はtldrコマンドで調べて、もっと詳しくいろいろなオプションを調べたい場合はmanコマンドを使うようにするとよいと思います。

コマンドの使い方を調べるコマンドは他にもあるので簡単に紹介します。

cheat

tldrと似ていますが、チートシートをローカルにダウンロードして、それを編集して自分で育てることができます。

$ brew install cheat
# コマンドを始めて調べるときはコミュニティ提供のチートシートをダウンロードできる
$ cheat sort
A config file was not found. Would you like to create one now? [Y/n]: y
Would you like to download the community cheatsheets? [Y/n]: y
Cloning community cheatsheets to /Users/t0k0sh1/.config/cheat/cheatsheets/community.
Enumerating objects: 335, done.
Counting objects: 100% (335/335), done.
Compressing objects: 100% (310/310), done.
Total 335 (delta 43), reused 213 (delta 23), pack-reused 0
Cloning personal cheatsheets to /Users/t0k0sh1/.config/cheat/cheatsheets/personal.
Created config file: /Users/t0k0sh1/.config/cheat/conf.yml
Please read this file for advanced configuration information.
# 以降はチートシートを表示する
# To sort a file:
sort <file>

# To sort a file by keeping only unique:
sort -u <file>

# To sort a file and reverse the result:
sort -r <file>

# To sort a file randomly:
sort -R <file>

# To sort a file and store the output in another file:
sort <inputFile> -o <outputFile>

# Sort by default uses /var/tmp to store temp files but size of /var/tmp directory is limited. In order to sort huge use a directory with adequate size:
sort -T <tempDirectory> <file>
# 一度ローカルに保存したチートシートは編集できる
$ cheat -e sort

howdoi

プログラマ向けのQ&Aサイトから回答を調べて表示するコマンドです。

$ brew install howdoi
# JavaのStream APIでsortをするときの書き方を調べるにはキーワードを並べる
$ howdoi java stream sort
List result = list.stream().sorted((o1, o2)->o1.getItem().getValue().
                                   compareTo(o2.getItem().getValue())).
                                   collect(Collectors.toList());

まとめ

個人的にはfd、rgコマンドはこれからも使っていきたいと思いました。もちろん、シェルスクリプトではfindコマンドやgrepコマンドはこれからも使うと思います。

伝統的なコマンドを使いこなすことも重要ですが、使いにくいと思うコマンドを別のコマンドに置き換えてより効率的に作業をこなすのも重要なことだと思いますので、積極的に試していきたいと思います。

モバイルバージョンを終了