行動すれば次の現実

ほどよくモダンなシステム開発を目指しています。メインテーマは生産性、Ruby、Javascriptです。

Next.js 13のRoute Groupsでページごとにレイアウトを使い分ける方法

Next.jsのRoute Groupsを使用することで、ページの種類ごとに異なるレイアウトを簡単に適用することができます。

例えばログイン済みユーザー用のページ、非ログインユーザー用のページ、管理者用のページといった具合にページの種類ごとにレイアウトを分けることが可能となります。

本記事では、Route Groupsを使用したレイアウトの切替の実装イメージと注意点についてを説明します。

ディレクトリ構成

app
├─ layout.tsx ・・・①共通レイアウト
├─ (auth) ・・・ログイン済みユーザー用
       ├─ layout.tsx ・・・②専用レイアウト
       ├─ home
            ├─ page.tsx ・・・専用ページ(/home)
       ├─ account
            ├─ page.tsx ・・・専用ページ(/account)
├─ (portal) ・・・非ログインユーザー用
       ├─ layout.tsx ・・・専用レイアウト
       ├─ page.tsx ・・・専用ページ(/)

上記のような構成でディレクトリおよび、layout.tsxやpage.tsxを配置します。

このような構成を取ることで、ログイン済みユーザー用と非ログインユーザー用のレイアウトを分けることができます。

ここでポイントなのが、①共通レイアウトの存在です。 この共通レイアウトがなければ、パーシェルレンダリングが機能しなくなるため、条件によっては画面遷移時にFull Page Reloadが発生してしまうことがあります。

各種レイアウトのサンプル

①共通レイアウト

共通レイアウトではシンプルにhtmlタグとbodyタグのみを構成します。こうすることで最低限htmlとbodyタグが再レンダリング対象外になり、Full Page Reloadを防ぐとこができます。

import './globals.css'
import {Inter} from 'next/font/google'
const inter = Inter({subsets: ['latin']})

export const metadata = {
  title: 'Create Next App',
  description: 'Generated by create next app',
}

export default function RootLayout({
                                     children,
                                   }: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
    <body className={inter.className}>
    {children}
    </body>
    </html>
  )
}

②専用レイアウト

対して専用レイアウトでは、htmlタグとbodyタグを含めない構成にします。

import Navigation from "@/app/(auth)/navigation";

export default function RootLayout({
                                     children,
                                   }: {
  children: React.ReactNode
}) {
  return (
    <Navigation>
      {children}
    </Navigation>
  )
}

Next.jsとRails APIのMonorepo管理とDockerを使った開発環境構築

フロントエンドにNext.js、バックエンドにRailsのAPIモードを採用した構成において、一つのリポジトリで管理するMonorepoの環境をDockerを利用して開発環境を構築する方法について解説します。

環境情報

  • Next.js 13.4.7
  • Rails 7.0.5
  • Postgresql 15.3
  • Docker 20.10.12
  • docker-compose 1.29.2

セットアップ前のProcjectの構成

.
├── front
       ├── Dockerfile
├── api
       ├── Dockerfile
       ├── Gemfile
       ├── Gemfile.lock # 空ファイル
       ├── entrypoint.sh 
├── docker-compose.yml

docker-compose.ymlの定義

version: "3.9"
services:
  front:
    build:
      context: ./front/
      dockerfile: Dockerfile
    volumes:
      - ./front/app:/usr/src/app
    command: 'yarn dev'
    ports:
      - "8000:3000"
  api:
    tty: true
    build:
      context: ./api/
      dockerfile: Dockerfile
    ports:
      - 3000:3000
    volumes:
      - ./api:/app
      - bundle:/usr/local/bundle
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -p 3000 -b '0.0.0.0'"
    depends_on:
      - db
  db:
    image: postgres:15.3
    volumes:
      - postgres:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: myapp
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app_development
      TZ: "Asia/Tokyo"
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
volumes:
  bundle:
  postgres:

front/Dockerfileの定義

FROM node:20.3.1
ENV TZ Asia/Tokyo
WORKDIR /usr/src/app

api/Dockerfileの定義

FROM ruby:3.2.2
ENV LANG=C.UTF-8 \
  TZ=Asia/Tokyo
WORKDIR /app
RUN apt-get update -qq && apt-get install -y postgresql-client

COPY Gemfile Gemfile
COPY Gemfile.lock Gemfile.lock

RUN bundle install

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

CMD ["rails", "server", "-b", "0.0.0.0"]

api/Gemfileの定義

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.2.2"

gem "rails", "~> 7.0.5"

api/entrypoint.shの定義

#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

front環境の構築

cd front
# next.jsのセットアップ
docker-compose run --rm front yarn create next-app .
# frontサービスのみ起動
docker-compose up front

http://localhost:8000/ にアクセスして下記のようなウェルカム画面が出力されれば完了です。

api環境の構築

cd api
# Railsのセットアップ
docker-compose run --rm --no-deps api bundle exec rails new . --api --database=postgresql

国際化とタイムゾーンの設定

日本をベースにしたサービスの場合、下記を追加しておきます。

config/application.rb

module App
  class Application < Rails::Application
    config.load_defaults 7.0
    config.api_only = true
    # 下記を追加
    config.time_zone = 'Tokyo'
    config.active_record.default_timezone = :local
    config.i18n.default_locale = :ja
  end
end

ホスト許可の設定

development.rbに以下の設定を追加します。これを追加することでapiホストからのリクエストができるようになります。

config/environments/development.rb

Rails.application.configure do
・・・
  config.hosts << "api"
end

データベースセットアップ

セットアップが完了したら設定ファイルをconfig/database.ymlを以下のように書き換えます。

api/config/database.yml

default: &default
  adapter: postgresql
  encoding: utf8
  host: db
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: myapp
  password: password

development:
  <<: *default
  database: app_development

test:
  <<: *default
  database: app_test

production:
  <<: *default
  database: app_production
  username: myapp
  password: <%= ENV["APP_DATABASE_PASSWORD"] %>

その後、データベースを構築して、起動確認を行います。

docker-compose run --rm api rails db:create
docker-compose up

frontは http://localhost:8000/ でNext.jsの画面が出力され、apiは http://localhost:3000/ で下記のような画面が出力されれば作業は完了です。

セットアップ後のProcjectの構成

バージョンによって違いはあるかもしれませんが、最終的にはプロジェクトは以下のような構成になります。

ChatGPTに負けない技術ブログの書き方

ChatGPTのような言語モデルが登場したことで、情報の収集や問題解決が容易になりました。しかし、それによって技術ブログの存在意義が低下したわけではありません。むしろ、技術ブログが果たすべき役割がさらに重要性を増したと私は考えます。

ググり力がなくても問題が解決できる時代に

ChatGPT登場前は、自分で問題を解決するためには、疑問点を分解して検索エンジンで検索するための「ググり力(ググラビリティ)」が必要でした。しかし、ChatGPTが登場したことで、検索エンジンでの検索に頼る必要が少なくなり、疑問点をそのまま入力すれば答えが得られるようになりました。

これによって、ググり力がなくてもある程度の問題であれば解決できるようになったと言えます。

ChatGPTは個別事象の大量製造機

技術ブログは、個別の技術的な問題や事象について、深堀りした情報を提供しているケースが多くあります。そのため、読者は自分自身の問題について類似の技術ブログを検索し、問題解決のための情報を得ることができます。

全く同じ事象というのはなかなかありませんので、ブログの内容の本質を理解して応用する力が必要とされていました。

しかし、ChatGPTが登場したことで、個別の事象について最適な回答を瞬時に提供することができるようになりました。今や、読者が抱えている問題についての技術ブログが存在しなくても、ChatGPTが最適な回答を提供する世界になりました。そのため、ChatGPTは個別事象の大量製造機と言えるかと思います。

これからの技術ブログの立ち位置

ChatGPTの登場により、情報収集や問題解決に必要な知識がより簡単に得られるようになりました。しかし、技術ブログはまだまだその存在意義があります。今後の技術ブログの立ち位置について、以下に私の考えを示します。

技術の深堀りや現場で活かされる実践的な内容

自分自身が体験して技術的な事象や問題について、深堀りした内容や付随情報を提供することで、ChatGPTとは異なる価値を提供できると考えます。現場レベルの実践的なノウハウや、いろいろな事象をつなぎ合わせた複合的な内容について発信することで、読者にとって有益な情報を提供することができます。

例えば、自分でWebサイトを作成するときに、JavaScriptを利用してスムーズな動きを実現する方法について知りたいとしましょう。このような問題に直面した場合、技術ブログは大きな助けになります。

JavaScriptについて十分な知識がなくても、技術ブログを通じて、実践的な手順や具体的な例を知ることができます。さらに、技術ブログは、読者にとって重要な実践的なアドバイスを提供することもあります。これらの情報は、プログラマーが日々の作業において直面する問題を解決するための貴重な情報源となります。

1つのテーマについてのシリーズ記事

技術ブログは、1つのテーマについて、複数回にわたってシリーズ記事を書くことで、より深く掘り下げた内容を提供することができます。例えば、入門書のようにターゲットを絞ったシリーズ物にすることで、読者にとってより興味深く、わかりやすい形で情報を提供することができます。

セルフブランディングとして活用

技術ブログをセルフブランディングのために活用することができます。自分自身のポートフォリオとして、自分が行ったプロジェクトや、取り組んだ技術についてブログにまとめることで、自分自身のスキルアップや就職活動の際に有益な情報を提供することができます。

また、自分自身の作業ログとして技術ブログを活用することで、自分自身が行ったことを記録することができます。

システム開発に関する思想的な内容

自分自身が考えるシステム開発に関する思想的な内容を発信するために、技術ブログが有効な手段と考えられます。思想には正解はありませんので、自分自身の考え方を発信することができます。

システム開発には、開発者の思想や哲学が大きな影響を与えるため、技術ブログを通じて、開発者の思想的な観点や、開発におけるベストプラクティスなどについて情報提供することができます。

例えば、システム開発におけるアジャイル開発やDevOpsなどの手法、コードの品質や保守性、可読性についての考え方など、より思想的な内容についても技術ブログを通じて発信していくことが有効です。

さいごに

ChatGPTの登場により、個別の事象に関する記事ばかりを書いていると、この先はなかなか読まれなくなるのではないかと考えます。

しかし、技術ブログは、単に個別の事象を解決するための情報提供だけでなく、自分自身が学んだ知識や経験を整理し、スキルアップやキャリアアップにつなげることができる貴重な手段です。また、他の人に役立つ情報を提供することで、業界の貢献につながると考えます。

技術ブログは自己成長や社会貢献に繋がる重要な存在であるため、私は今後も積極的に記事を書いていきたいと思います。

ファイルを指定の件数ごとに分割するLinuxコマンド

10,000行あるrow.csvを1,000行ごとに分割してファイルを生成する例を説明します。

コマンド例

split -l 1000 -d rows.txt rows_
  • -lオプションには分割する行数を指定します。
  • -dオプションを指定すると、ファイル名の末尾につく数字の連番が付与されます。
  • rows_の部分にはファイル名の接頭辞を指定します。この接頭辞に数字の連番が付与されます。

実行後すると以下のファイルが生成されます。

rows_00
rows_01
rows_02
rows_03
rows_04
rows_05
rows_06
rows_07
rows_08
rows_09

このままでは拡張子が付いていないので、下記のスクリプトを実行してcsvの拡張子を付与します。

rename.sh

#!/bin/bash

for f in "$1"*
do
    mv -- "$f" "${f}.csv"
done
sh rename.sh rows_

実行後すると以下のファイルに変更されます。

rows_00.csv
rows_01.csv
rows_02.csv
rows_03.csv
rows_04.csv
rows_05.csv
rows_06.csv
rows_07.csv
rows_08.csv
rows_09.csv

Heroku Data for Redisをアップグレードする方法(v4 to v6)

Herokuからredisのバージョンをv6にアップブレードするようにと催促メールが届いたので対応しました。 公式のアップグレード手順に従って対応しましたが、それだけだとエラーが発生してしまいましたので記事にすることにしました。同じ轍を踏まないためにも参考なれば幸いです。

Herokuからのメール(抜粋)

Running up-to-date software versions is essential for maintaining a highly-available and secure fleet of Heroku Data for Redis instances. Your Redis database redis-xxx on your-app is running a deprecated version (4.0.14) and will not be supported after 30 Jun, 2023.

Redis6ではTLS接続が必須になるため設定ファイルに注意!

Redis6からはPremiumプラン以上の場合はTLS接続が必須となったようなので、Sidekiq等でRedisの接続定義を変更する必要があります

Sidekiq.configure_server do |config|
  config.redis = {
    url: ENV["REDIS_URL"],
    # ↓この部分を追加
    ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
  }
end

Sidekiq.configure_client do |config|
  config.redis = {
    url: ENV["REDIS_URL"],
    # ↓この部分を追加
    ssl_params: { verify_mode: OpenSSL::SSL::VERIFY_NONE }
  }
end

この定義を追加しなかった場合、OpenSSL::SSL::SSLError (hostname "xxxxxx" does not match the server certificate):というエラーが発生してアプリからRedisに接続ができなくなってしまいます。

なぜVERIFY_NONEなのかというとHeroku内部ではSSLを使用していないためです。HerokuのルーターレベルでSSLを終了させて、内部接続にはHTTPSを使用しているため、VERIFY_NONEで問題ないとのことでした。

https://stackoverflow.com/questions/65834575/how-to-enable-tls-for-redis-6-on-sidekiq

アップグレード手順

上記の考慮を踏まえてアップグレードを実行します。 基本的には公式のアップグレード手順と同じことをしています。

1. メンテナンスモードをON

heroku maintenance:on

2. アップグレード実行

heroku redis:upgrade --version 6.2

3. アップグレード進捗状況の確認

heroku redis:info

=== redis-angular-51206 (REDIS_URL)
Plan:                   Premium 0
Status:                 preparing (version upgrade in progress)
Created:                2021-09-08 20:41
Version:                5.0.12
Timeout:                300
Maxmemory:              noeviction
Maintenance:            not required
Maintenance window:     Tuesdays 21:00 to Wednesdays 01:00 UTC
Persistence:            AOF
HA Status:              Available
Requires TLS:           No
Keyspace Notifications: Disabled

私の環境の場合、preparingからavailableに変わるまで20分程度の時間が掛かりました。

4. メンテナンスモードをOFF

heroku maintenance:off

終わりに

以上でアップグレード作業は完了です。 途中OpenSSL::SSL::SSLErrorが発生してかなり焦りましたがすぐに対応できて良かったです。

テスト環境でもリハーサルでアップグレードをしたのですが、その時はエラーが発生しませんでした。なぜ発生しなかったかというとテスト環境ではRedisをPremiumプランにしていなかったためです。Premium未満の場合、TLS接続は必須ではないためOpenSSL::SSL::SSLErrorのエラーが発生しないため防ぐことが出来ませんでした。

テスト環境でも同スペックの構成をしておく必要があるのだと改めて痛感しました。。

参考

https://ogirginc.github.io/en/heroku-redis-ssl-error https://devcenter.heroku.com/ja/articles/connecting-heroku-redis#connecting-in-ruby

【超簡単】HerokuのpostgresDBのbackupデータをrestoreする方法

以下のコマンドで簡単にバックアップしたファイルを使って、簡単にリストアすることが出来ます。

heroku pg:backups:restore b042 DATABASE_URL --app your-appname
  • b042の部分はバックアップデータの名称を指定します
  • DATABASE_URLの部分は、本番環境を指定する場合はこのままで良いです

参考

Heroku PGBackups | Heroku Dev Center

外部キー(null許可)を複数持っているテーブルで検索する時のSQLクエリのサンプル

レガシーシステムの改修案件で以下のような正規化されていない条件で検索することがありましたので、記事にまとめました。

テーブル設計

  • orders (注文)

    • id
    • supplier_id
    • customer_id
  • suppliers (取引先)

    • id
    • is_active (boolean)
  • customers (顧客)

    • id
    • is_active (boolean)

例えば注文テーブルがあり、「取引先の注文」と「顧客の注文」のいずれかを一つのテーブルで管理しているとします。どちらも設定されることはシステムとしてありえないと仮定します。

この注文テーブルから、取引先がアクティブ(is_active=TRUE)である、または顧客がアクティブ(is_active=TRUE)であるテータを抽出したい場合のクエリを紹介します。

SQLのサンプル

SELECT
    orders.*
FROM
    orders
    LEFT OUTER JOIN
        suppliers
    ON  suppliers.id = orders.supplier_id
    LEFT OUTER JOIN
        customers
    ON  customers.id = orders.customer_id
WHERE
    (suppliers.is_active = TRUE OR suppliers.id IS NULL)
    AND
    (customers.is_active = TRUE OR customers.id IS NULL)

Activerecordで実装する場合

Order.left_outer_joins(:supplier, :customer)
  .merge(where(suppliers: { is_active: true })
    .or(where(suppliers: { id: nil })))
  .merge(where(customers: { is_active: true })
    .or(where(customers: { id: nil })))