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

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

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コマンドはこれからも使うと思います。

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

Spring FrameworkにおけるDIのベストプラクティス: コンストラクタインジェクションの優位性

Spring Frameworkでの依存性注入には、

  • フィールドインジェクション
  • コンストラクタインジェクション
  • セッターインジェクション

の3つの方法があります。これらの方法は、オブジェクトが他のオブジェクトに依存している場合に、その依存オブジェクトを自動的に挿入するために使用されます。この中で主に使われるのはフィールドインジェクションとコンストラクタインジェクションです。

Springのコミュニティでは、フィールドインジェクションよりもコンストラクタインジェクションの方が推奨されています。なぜコンストラクタインジェクションが推奨されているのかをフィールドインジェクションのデメリットとコンストラクタインジェクションのメリットの観点から見ていきましょう。

フィールドインジェクション

フィールドインジェクションは、クラスのフィールドに直接依存性を注入する方法です。これは@Autowiredアノテーションをフィールドに適用することで行われます。例えば、

@Component
public class MyService {
    @Autowired
    private DependencyClass dependency;
}

のように書きます。

コンストラクタインジェクション

コンストラクタインジェクションは、オブジェクトのコンストラクタを通じて依存性を注入する方法です。コンストラクタに@Autowiredアノテーションを付けることで、Springが自動的に依存オブジェクトを挿入します。例えば、

@Component
public class MyService {
    private final DependencyClass dependency;

    @Autowired
    public MyService(DependencyClass dependency) {
        this.dependency = dependency;
    }
}

のように書きます。

コンストラクタインジェクションが推奨される理由

Spring Framework でコンストラクタインジェクションが推奨される理由のうち、特に理由としてふさわしいと思ったものには以下が2つがあります。

依存コンポーネントの不変性を宣言できる

前述のコード例を見てもらうとわかりますが、フィールドインジェクションではオブジェクト生成時点でフィールドの値が初期化されないため、final宣言をつけることができません。一方、コンストラクタインジェクションではオブジェクト生成時点でフィールドの値が初期化されるため、final宣言をつけることができます。

これにより依存コンポーネントがイミュータブル(不変)であることを明示することができるようになります。

実際にイミュータブルであることで誤って再初期化してしまうことを防ぐことはほとんどないと思いますが、実際に変更してしまうかどうかは別として、変更するつもりがないという設計意図をコードに表現できることが重要だと考えています。

循環依存を回避できる

コンストラクタインジェクションを使用するとコンストラクタの呼び出し時点でインジェクションされてイミュータブルになります。このとき循環依存があると、アプリケーションの起動時にエラーが発生して循環依存を検知することができます。

一方、フィールドインジェクションの場合は実際に対象のフィールドが呼び出されるまで検知できないため、循環依存の発見が遅れたり、発見自体が困難になる場合があります。

単一責任の原則に従っていないことに気づきやすくなるのか

単一責任の原則に従っていないことに気づきやすくなるというメリットを挙げているのを見かけますが、個人的にはそれほど効果がないのではないかと思っています。

まったくないとは言いませんが、Lombokを使用していると、ほぼフィールドインジェクションのような使い心地でコンストラクタインジェクションを使えてしまうので、レビューで検出する方が確実だと思います。例えば、

@Component
@RequiredArgsConstructor
public class MyService {
    private final DependencyClass dependency;
}

のように@RequiredArgsConstructorアノテーションを使用することで、コンストラクタを省略することが可能です。これ自体はボイラープレートコードを書かなくて済むというメリットがありますが、コンストラクタ自体を書かないため、依存コンポーネントが多すぎることに気づきにくいと思います。

コンストラクタインジェクションに頼るのではなく、きちんとレビュー前のセルフチェックやレビューで依存関係の複雑さや単一責任の原則に従っているのかなどをチェックすることが重要だと考えています。

単体テストコードを書きやすくなるのか

元々はコンストラクタインジェクションの方が書きやすかったと思うのですが、現在ではほとんど変わらないと思いました。

フィールドインジェクションを使ったコンポーネントの例

フィールドインジェクションを使ったコンポーネントのテストを書く例として、次のようなサービスクラスに対するテストケースを考えてみましょう。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SampleService {

    @Autowired
    private DummyComponent dummyComponent;

    public String doSomething() {
        return dummyComponent.doSomething();
    }
}

このサービスクラスに対するテストは以下のようになります。

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SampleServiceTest {

    @InjectMocks
    private SampleService sampleService;

    @Mock
    private DummyComponent dummyComponent;

    @Test
    public void testDoSomething() {
        when(dummyComponent.doSomething()).thenReturn("test");

        assertEquals("test", sampleService.doSomething());
    }
}

コンストラクションインジェクションを使ったコンポーネントの例

コンストラクタインジェクションを使ったコンポーネントの例も見てみましょう。同じサービスクラスをコンストラクタインジェクションで書き換えた場合、以下のようになります。

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SampleService {

    private final DummyComponent dummyComponent;

    @Autowired
    public SampleService(DummyComponent dummyComponent) {
        this.dummyComponent = dummyComponent;
    }

    public String doSomething() {
        return dummyComponent.doSomething();
    }
}

このサービスクラスに対するテストは以下のようになります。

package com.example.demo;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class SampleServiceTest {

    @InjectMocks
    private SampleService sampleService;

    @Mock
    private DummyComponent dummyComponent;

    @Test
    public void testDoSomething() {
        when(dummyComponent.doSomething()).thenReturn("test");

        assertEquals("test", sampleService.doSomething());
    }
}

見ての通り同じテストケースクラスでテストできます。

現状ではテストのしやすさという点ではその差はほぼないと考えて差し支えないと思います。

まとめ

依存コンポーネントに対して不変性を明示でき、循環依存を回避できるようになる点を考慮すると、コンストラクタインジェクションを積極的に使用していくことは重要だと思います。

一方で、単一責任の原則に従っているかどうか、依存関係の多いファットなコンポーネントになっていないかどうかについては、ペアプログラミング時に議論したり、セルフチェックやレビューによってチェックすることが重要だと思います。

単一責任の原則とオープン・クローズドの原則の関係性について

ここ最近、SOLID原則の「単一責任の原則」と「オープン・クローズドの原則」に関する検討を行っていて、オープン・クローズドの原則と単一責任の原則の関係性に考えていました。

具体的には、オープン・クローズドの原則に従った設計を行うと、自然と単一責任の原則に従うのではないかということです。必ずしも正しいとはいえませんが、多くの場合に成り立つのではないかと思います。

単一責任の原則とは

単一責任の原則(Single Responsibility Principle)は、オブジェクト指向プログラミングにおける設計原則の一つであり、「モジュール、クラス、関数などは、単一の機能についてのみ責任を持ち、その機能をカプセル化するべきである」というものです。

単一責任の原則のメリット

単一責任の原則を適用することで得られるメリットには以下のようなものがあります。

  • 理解しやすさの向上: 責務が明確に分かれているため、コードの理解が容易になり、保守性も向上します。
  • 変更の容易化: 特定の機能を変更する際、影響を受ける範囲が限定されるため、変更に伴うリスクが低減されます。
  • 再利用性の向上: 単一の機能に特化したモジュールは、他の箇所でも容易に再利用することができます。
  • テストの容易化: 単一の機能に特化したモジュールは、個別にテストしやすくなります。

単一責任の原則を満たすための手法

単一責任の原則を満たすための手法として以下のような手法が挙げられます。

  • クラスごとに単一の責務を割り当てる: あるクラスは、特定の機能のみを担当するように設計します。
  • メソッドを小さくする: メソッドは、単一のタスクを実行するように設計します。
  • インターフェースを使用する: 共通の機能をインターフェースで定義し、それを複数のクラスで実装します。

オープン・クローズドの原則とは

オープン・クローズドの原則(Open/Closed Principle)は、オブジェクト指向プログラミングにおける設計原則の一つであり、「ソフトウェアは、既存のコードを変更することなく、機能拡張できるようにすべきである」というものです。

オープン・クローズドの原則のメリット

オープン・クローズドの原則を適用することで得られるメリットには以下のようなものがあります。

  • 柔軟性の向上: 新機能の追加や変更を、既存のコードに手を加えることなく行うことができます。
  • 保守性の向上: 既存のコードを触らずに変更できるため、コードの保守性が向上します。
  • 再利用性の向上: 既存のコードを拡張することで、新しい機能を追加することができます。
  • テストの容易化: 既存のコードを変更しないため、テストが容易になります。

オープン・クローズドの原則を満たすための手法

オープン・クローズドの原則を満たすための手法として以下のような手法が挙げられます。

  • 抽象化を利用する: 共通の機能を抽象化し、それを継承したクラスで具体的な処理を実装します。
  • 依存関係を明確にする: クラス間の依存関係を明確にし、できるだけ疎結合な設計を目指します。
  • ポリモーフィズムを利用する: 異なるクラスで同じインターフェースを実装することで、柔軟な振る舞いを可能にします。

オープン・クローズドの原則に従って設計の例を確認する

ここからが本題です。

オープン・クローズドの原則に従うと、自然と単一責任の原則に従うことになるのではないかと考えました。もちろん、「オープン・クローズドの原則に従っている=単一責任の原則」が必ずしも成り立つわけではありませんが、多くのケースにおいては成り立つのではないかと思います。

拡張に対して開いていて、修正に対して閉じている

オープン・クローズドの原則には、「拡張に対して開いていて、修正に対して閉じている」という考え方があります。「拡張に対して開いている」とは、既存のコードを変更することなく、新しい機能を追加したり、既存の機能を拡張したりできることを意味します。一方、「修正に対して閉じている」とは、既存のコードを変更することなく、新しい機能を追加したり、既存の機能を拡張したりできることを意味します。

新しい機能を既存コードの変更をせずに追加する方法として、継承があります。抽象クラスやインターフェースを使用し、個別の実装をサブクラスで実現することで、新しい機能の追加を新しいクラスの作成で実現することができます。

また、修正に対して閉じているためには、各機能が別々のクラスに分かれているだけでなく、ひとつのメソッドに複数の処理が含まれていないようにすることが重要です。他の処理が含まれているとコードを修正した際に、修正対象ではない処理にも影響を与えている可能性があります。

この点について、具体的なコードで見ていきましょう。

オープン・クローズドの原則に従っていない設計例

例えば、図形クラスを考えてみます。

このクラスは様々な図形の面積を計算することができます。

class Shape:
def __init__(self, type: str):
self.type = type

def get_area(self) -> float:
if self.type == "circle":
return 3.1415 * self.radius * self.radius
elif self.type == "rectangle":
return self.width * self.height
elif self.type == "triangle":
return 0.5 * self.base * self.height
else:
raise ValueError(f"Unknown shape type: {self.type}")

この図形クラスでは、新しい図形を扱えるようにするためにクラス自身に手を加えなくてはなりません。上記のコードにおいては、新しい図形を扱えるようにするにはif文に条件を追加する必要があります。そのため、新しい図形を追加する度にコードの複雑度が増し、同じメソッドに修正を加えるため、デグレのリスクがつきまといます。

オープン・クローズドの原則に従っている設計例

オープン・クローズドの原則の原則に従うようにコードを修正してみましょう。

from abc import ABC, abstractmethod


class Shape(ABC):
@abstractmethod
def get_area(self) -> float:
pass


class Circle(Shape):
def __init__(self, radius: float):
self.radius = radius

def get_area(self) -> float:
return 3.1415 * self.radius * self.radius


class Rectangle(Shape):
def __init__(self, width: float, height: float):
self.width = width
self.height = height

def get_area(self) -> float:
return self.width * self.height


class Triangle(Shape):
def __init__(self, base: float, height: float):
self.base = base
self.height = height

def get_area(self) -> float:
return 0.5 * self.base * self.height

抽象クラスShapeでインターフェースを定義し、具体的な図形はこれを継承する形で定義するようにします。どの具象クラスをインスタンス化するかによってget_areaメソッドの動作が変わります。新しい図形に対応する時には既存コードに手を加えずに新しいクラスを作成することで実現します。新たな図形の追加に対して既存の図形に対して影響を及ぼしません。

拡張(新しい図形の追加)に対して開いており(既存コードに修正を加えずに機能を追加できる)、修正(ある図形の計算方法が誤っている問題を修正)に対して閉じている(他の機能に影響を与えずに修正できる)ことがわかります。

各図形クラスは単一の図形に特化しており、get_areaメソッドは面積を返すという単一の機能を実現しているため、単一責任の原則に従っているといえます。

まとめ

成り立たないこともありますが、オープン・クローズドの原則に従うことは、単一責任の原則に従うことにもなります。これは、それぞれの原則は完全に独立しているのではなく、互いに関連していることを意味しています。一方の原則に従うことは他方の原則に従うことを意味することもあれば、トレードオフの関係にある場合もあります。必ずしも原則に従うことが正しいわけではなく、要求や仕様に応じてバランスをとることが重要です。

Docker Composeのversionが不要になっていた

かなり前からdocker-compose.yamlversionが不要になっていたようですが、macOS上ではversion is 'obsolete'のメッセージが出ていなかったので、気づきませんでした。

Windows環境でversion is 'obsolete'のメッセージが表示されるのを見かけたので、調べてみると後方互換性を維持するために残っているだけで、意味のない項目になっているようです。

この説明を読む限り、ワーニングメッセージが表示されるようですが、macOSのDocker Desktopに同梱されているDocker Composeでは特にメッセージが出ていませんでした。ただ、versionがなくても問題なく動作するので、無理に削除する必要はないものの、新たに作成する場合はversionをつけないようにした方がよさそうです。

ChromebookにインストールしたVSCodeがちらつく場合の対処方法

ChromebookにインストールしたVisual Studio CodeのExplorerなどの表示がちらついたり、何も表示されないという事象に遭遇しました。すべてのChromebookで起きるとは限りませんが、私の使用している「ASUS Chromebook Detachable CM3」で発生したため、対処法を共有します。

対処方法

結論から書くと、「--disble-gpu」オプションをつけてVisual Studio Codeを起動すると解消します。

$ code --diable-gpu

もし、カレントディレクトリをcodeコマンドで開く場合は、

$ code . --disable-gpu

とします。

毎回オプションを付けるのが面倒な場合は、エイリアスを使って.bashrcなどに

alias code="code --disble-gpu"

と定義しておくと、毎回オプションを指定しなくて済みます。

何が原因なのか

明確な記述を見つけられませんでしたが、VSCodeとGPUとの相性で発生するようです。ただし、今回のケースではGPU非搭載のマシンでGPUアクセラレーションが有効になっているのが問題だと思われます。

本事象はChromebookでのみ起きる事象ではなく、VirtualBox上でも起きるようですので、リモートデスクトップ環境を含む仮想マシンや低スペックマシンで発生する可能性があります。

また、直接的な影響かわかりませんが、ターミナルの文字描画が遅れます。コピペでは特に問題ありませんが、キーボードで文字を入力していると最後の方の文字が表示されていないことがあります。こちらについては引き続き調査していきます。

ローマ字表記が70年ぶりに改定の見通し

学校教育では訓令式ローマ字表記で教えていますが、社会に出てみるとヘボン式ローマ字で書かれている場合がほとんどです。文化審議会国語分科会の国語課題小委員会によると、実態とそぐわない状況を受けて、改定することも視野に入れて検討を進めているそうです。

ヘボン式ローマ字とは

ヘボン式ローマ字(ヘボンしきローマじ)は、日本語の音をローマ字(ラテン文字)で表記する方法の一つです。この方式は、19世紀にアメリカ合衆国の宣教師であるジェームス・カーティス・ヘボン(James Curtis Hepburn)によって考案されました。ヘボン式は、日本語の発音を英語の読み方に近い形で表記することを特徴としています。

ヘボン式ローマ字の特徴は以下のとおりです。

  • 母音の表記:「あ」は「a」、「い」は「i」、「う」は「u」、「え」は「e」、「お」は「o」と表記します。
  • 子音の表記:基本的に日本語の子音は、英語の発音に近い形でローマ字に変換されます。例えば、「か」は「ka」、「さ」は「sa」、「た」は「ta」、「ふ」は「fu」などとなります。
  • 撥音(「ん」):「ん」は、単独で「n」と表記されますが、次に続く音が「b」、「m」、「p」の場合は「m」と表記されることがあります(例:「さんぽ」→「sanpo」)。
  • 長音:長い母音は、基本的に母音を重ねて表記します(例:「おおきい」→「ookii」)。
  • 促音(小さい「っ」):促音は、次に続く子音を重ねて表記します(例:「きっぷ」→「kippu」)。

訓令式ローマ字とは


訓令式ローマ字(くんれいしきローマじ)は、日本語の音をローマ字(ラテン文字)で表記する方法の一つです。この方式は、日本政府が1946年に公式に採用したもので、ヘボン式ローマ字とは異なる特徴を持っています。

訓令式ローマ字の特徴は以下のとおりです。

  • 母音の表記:「あ」は「a」、「い」は「i」、「う」は「u」、「え」は「e」、「お」は「o」と表記します。
  • 子音の表記:一部の子音についてはヘボン式と異なり、より日本語の発音に近い形で表記されます。例えば、「し」は「si」、「ち」は「ti」、「つ」は「tu」、「ふ」は「hu」となります。
  • 撥音(「ん」):「ん」は常に「n」として表記されます。
  • 長音:長い母音は、アクセント記号を付けて表記します(例:「おおきい」→「ôkii」)。
  • 促音(小さい「っ」):促音は、次に続く子音を重ねて表記します(例:「きっぷ」→「kippu」)。

業務上はヘボン式ローマ字がメイン

最近だとかなり減ってきた印象ですが、DBのテーブル名、カラム名、プログラムの関数名などで英語に訳せない・訳しにくい場合にローマ字表記を使うことがあります。

その際のルールとして「ヘボン式ローマ字を使用すること」と明文化されているプロジェクトもありますが、そうでないプロジェクトもあります。そのせいか、すでに作成されているテーブルやプログラムを見てみると、ヘボン式と訓令式が混在した書き方になっていることもしばしばです。

これは、訓令式ローマ字に慣れているからではなく、ヘボン式ローマ字に慣れていないことが原因だと考えており、キーボード入力の癖がそのままローマ字に現れているのではないかと思います。

どう改定されるのか

新聞によって書き方は様々ですが、実態に合っていないことが改定の一因になっているため、ヘボン式ローマ字に改定されると予想されています。社会としてはほとんど影響ありませんが、教科書を改定するということになると、生徒や学生、教えている教師の間では混乱が生じるかもしれません。

ヘボン式ローマ字以外の表記方法になるというのはあまり考えにくいですが、どう検討されていくかについては今後も注目していきたいです。

[SQL]INSERT SELECTでテーブルから直接INSERTする

先日投稿した記事のテーブルを使って、商品別売上実績のレコードが必ず存在するようにしてみます。

月末処理で売上平均金額を求める前に売上のない商品について0円の商品別売上実績レコードを作成する状況を想定しています。

ここではSELECTした結果をINSERTするINSERT SELECTを使ってデータを登録します。

INSERT SELECTとは

INSERT SELECTとは、INSERTするデータをSELECTで作成する手法のことです。例えば、テーブルAにテーブルBから抽出したレコードをそのままINSERTする場合、以下のように書きます。

INSERT INTO A
SELECT
  *
FROM
  B

この例では、テーブルAとテーブルBは同じカラムを持っており、テーブルBから全件抽出してそのままテーブルAにINSERTしています。このやり方はバックアップ対象と同じカラムを持つバックアップ用のテーブルを作成して、そこにテックアップ対象のレコードを全件抽出しておく、というときによく使います。

SELECT時に対象のカラムと行を絞り込む

今回実現したいのは、SELECTするテーブルとINSERTするテーブルのカラムは異なっています。これはSELECTするカラムをINSERTするカラムに合わせることで対応します。

もう一つ、すべてのカラムを抽出するのではなく、すでにINSERTするテーブルに存在している商品コードはINSERTする必要がない(INSERTすると一意制約違反となる)ため、商品テーブルにあって商品別売上実績テーブルにない商品コードだけINSERTするように対象レコードを絞り込みます。

作成したSQLは以下のようになります。

INSERT INTO sales_by_product
SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

INSERTするテーブルのカラムと同じカラムを抽出する

SELECTではINSERTするテーブルと同じカラムを抽出します。

SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1

INSERT先の商品別売上実績テーブルはproduct_codetotal_sales_amountを持っているため、これらのカラムだけになるように抽出しています。product_codeproductsテーブルのカラムを使用し、total_sales_amountは今回やりたいことに合わせてデフォルト値の0を設定するようにしています。0 total_sales_amountのように固定値を直接記述することができるかどうかは使用しているRDBMSによって異なりますので注意してください。

INSERTするテーブルに存在していないレコードのみ抽出する

少しややこしいですが、NOT EXISTSを使ってsales_by_productに存在しない商品コードのみ抽出するようにします。

FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

SELECTしているproducts T1NOT EXISTS内のSELECT文のsales_by_product T2product_codeで結合することで、T1T2双方に存在するsales_by_productが抽出できます。ということは、T1NOT EXISTS以外に絞り込み条件を持たないので、すべてのT1のレコード(すべての商品)について、それぞれのレコードに対応するT2(商品別売上実績)を抽出してみて、それが存在しなかったら(NOT EXISTS)、SELECTされるということになります。

NOT EXISTS内のSELECTしている列が1となっていますが、これは行が抽出できていること自体にしか意味がないので、無駄なデータを抽出しないために適当な1という値を指定しているだけです。'x'とか3とかでも問題ありませんし、データ量のことを気にしないのであれば*でも構いません。

INSERTする前にSELECTだけを実行してみる

実際にINSERTをする前にINSERT部分以外のSELECT文単独で実行結果を確認しておきます。実務においてもまずはINSERTしたい形と同じかをSELECTだけで確認し、それが確認できたらINSERTするのが安全です。

SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

まずは、INSERT前の商品別売上実績テーブルを確認してみます。

SELECT * FROM sales_by_product;

=>
S001,50
S003,250
S004,350
S006,450

商品コードS002S005が欠損していることがわかります。これが売上合計金額0で抽出できればINSERTしたい内容と一致します。

SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

=>
S002,0
S005,0

問題なさそうです。

では実際にINSERTまでやって、再度全件抽出してみます。

INSERT INTO sales_by_product
SELECT
    T1.product_code,
    0 total_sales_amount
FROM
    products T1
WHERE
    NOT EXISTS (
        SELECT
            1
        FROM
            sales_by_product T2
        WHERE
            T1.product_code = T2.product_code
    );

SELECT * FROM sales_by_product;

=>
S001,50
S002,0
S003,250
S004,350
S005,0
S006,450

商品コードS002S005が売上合計金額0で登録されていることが確認できました。

トランザクション機能を使って確認後にコミットする

今回は自動トランザクションでINSERTしたら自動コミットされるようにしています。

実務では手動トランザクションで明示的にコミットするまではDBにコミットされないようにしておくことで、INSERT後の確認でOKになるまでは作業のやり直しができるようにしておくことが重要です。

まとめ

INSERT SELECTを使ってあるテーブルから直接INSERTする方法について確認しました。バッチなどでのバルクインサートやテーブルのバックアップ・リストアなどINSERT SELECTを行うシーンは意外とあります。

INSERTしたい結果を抽出できるSELECT文を書いて、その前にINSERT INTO テーブル名を付けるということを覚えておけば、それほど悩むこともないと思います。細かい点を抜きにすればUPDATE SELECTとは異なり、RDBMSによって構文が変わるということもないので、どれかのRDBMSでやり方を覚えておけば他のRDBMSでもほぼそのまま適用できます。

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