【備忘録】失敗しないバッチ処理設計・運用の鉄則 (※AI生成記事)

【備忘録】失敗しないバッチ処理設計・運用の鉄則

※この記事は自分用の備忘録として、実務で得た知見をまとめたものです

バッチ処理の設計や運用を誤ると、リソースの逼迫、データ不整合、処理遅延といった深刻な問題を引き起こすことを何度か経験してきました。

ここでは、実務経験から得られた「バッチ処理で絶対にやってはいけないこと」と「積極的にやるべきこと」を自分用にまとめておきます。将来の自分が同じ轍を踏まないように。

❌ やってはいけないこと(アンチパターン

1. 実行中のタスクを安易に中断すること(最重要原則)

実行中のバッチ処理を途中で止めると、以下のリスクが発生します。

  • データ不整合: 処理が中途半端な状態で終了し、データベースやストレージに不完全なデータが残る
  • リカバリの困難さ: どこまで処理が進んだかを把握できず、再実行時に重複処理や漏れが発生する

対策: 致命的なエラーやシステム障害以外で、処理を強制終了させることは避けるべき。中断する可能性がある場合は、トランザクションやチェックポイント処理を厳密に設計する必要があります。

2. 重複タスクの実行を許容するスケジュール設計

同時刻に同じバッチが複数起動してしまうと、リソースの無駄遣いになるだけでなく、データの二重更新やデッドロックの原因となります。

対策: スケジューラー側(例: Cron, Cloud Scheduler, AWS EventBridgeなど)の制御に加え、バッチプログラム自体に排他制御の仕組みを組み込む。

  • ロックファイル/ロックテーブルの利用: 実行開始時にDBの特定レコードやファイルサーバーにロック情報を書き込み、終了時に解放する
  • ランタイムチェック: 既に同じプロセスが起動していないかを、プロセスID(PID)などで確認する

✅ やった方がいいこと(ベストプラクティス)

1. リソースを活かした並列処理の最適設計

バッチ処理の高速化は、リソースの効率的な利用にかかっています。

  • リソース増強: 処理対象のデータ量に応じて、事前にメモリ (RAM) と CPU のスペックを上げること(スケーリング)を検討する
  • 並列処理の適用: スケーリングした上で、Go言語のGoroutineやPythonconcurrent.futuresなど、言語が提供する並列・並行処理の機能を使って、複数のタスクを同時に実行できる設計にする

ポイント: I/O待機が多い処理(DBアクセス、API通信)は並行処理の恩恵を大きく受けます。

2. コマンドラインオプションによる柔軟な対応

複数の処理対象(例: 異なる拡張子のファイル、異なる種類のデータ)に対応する必要がある場合、バッチ処理を柔軟に実行できるようにします。

CLI (Command Line Interface) コマンドの活用: 処理の種類や対象を、オプション (-t--type) として指定できるように設計します。

オプション例 実行例 目的
-e, --extension $ batch_proc -e jpg 特定の拡張子に絞って処理をかける
-m, --mode $ batch_proc -m cleanup クリーンアップモードで実行する
-i, --id $ batch_proc -i 100-200 特定のIDレンジのみ処理する

3. ループとページネーションによる逐次並列実行の工夫

膨大なデータを一度に取得・処理しようとすると、メモリを圧迫し、データベースにも大きな負荷がかかります。

取得件数を抑える工夫: データベースのページネーション(OFFSET/LIMIT)を適切に利用し、一回の実行で全件ではなく、分割された単位(チャンク)でデータ取得を行います。

全体の処理フロー:

  1. バッチ開始時に、全処理対象の総件数や分割に必要なキーを取得
  2. 取得したキーやオフセット/リミットを使ってループ処理を実行
  3. ループ内で、分割された小さなチャンクごとに逐次的に並列処理を実行

この設計により、「一回の実行で全ての処理が回る」という要件を満たしつつ、メモリ負荷を抑え、安定した高速処理を実現できます。

💡 まとめ:安定バッチへのロードマップ

項目 Do (やるべきこと) Don't (やってはいけないこと)
中断・再実行 チェックポイント、トランザクション設計 安易な強制終了
並行処理 メモリ/CPUを上げた上での並列実行 リソース不足下での無計画な並列化
スケジュール 排他制御の実装(ロックテーブルなど) 重複実行の許容
柔軟性 CLIオプションによる処理対象の柔軟な指定 固定値による融通の利かない設計
データ取得 ページネーションとループによる分割処理 全件取得によるメモリ・DB負荷増大

将来の自分へ:この原則を守れば、バッチ処理で大きなトラブルを避けられるはず。頑張ろう!

データベース設計における正規化の実践例:カスタム設定と固定設定の混在パターン ※生成AIを活用した記事

概要

システム設計において、カスタム設定固定設定(曜日、カテゴリなど)が混在するパターンは非常に一般的です。この記事では、曜日を含む機能実装を通じて、正規化の重要性と実装時の注意点を解説します。

よくある実装パターンと問題点

パターン1: 名前ベースの判定(非推奨)

-- 問題のある設計例
CREATE TABLE templates (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),           -- "月曜日", "カスタムテンプレート"
    template_type ENUM('weekly', 'custom'),
    -- その他のカラム
);

問題点: - テンプレート名に曜日情報が埋め込まれている - カスタムテンプレートと曜日テンプレートの混在リスク - 名前変更時の影響範囲が大きい

パターン2: 列挙型の拡張(部分的解決)

-- 改善された設計例
CREATE TABLE templates (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),
    template_type ENUM('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday', 'custom'),
    -- その他のカラム
);

改善点: - 曜日情報が明示的に管理される - 型安全性が向上

残る問題: - 新しい曜日やカテゴリの追加時にスキーマ変更が必要 - カスタム設定の柔軟性が制限される

正規化の段階別問題分析

第1正規化(1NF)の違反

問題: 複合属性の存在

-- 違反例
name = "月曜日"  -- テンプレート名 + 曜日情報

-- 正しい形
name = "平日朝のテンプレート"  -- テンプレート名のみ
day_of_week = "monday"        -- 曜日情報は別属性

初学者が陥りがちなポイント: - 属性名に複数の意味を含めてしまう - 検索・ソートのロジックが複雑になる - データの原子性を理解していない

第2正規化(2NF)の違反

問題: 部分関数従属

-- 違反例:同じ情報が複数テーブルに存在
templates.name = "月曜日"
schedules.day_of_week = "monday"

-- 正しい形:情報の一元化
templates: 曜日情報なし
schedules.day_of_week: 曜日情報のみ

初学者が陥りがちなポイント: - データの重複を避けられない - 更新時の整合性保証が困難 - 正規化の概念を表面的にしか理解していない

第3正規化(3NF)の違反

問題: 推移的関数従属

// 違反例:計算による依存関係
func extractDayOfWeek(name string) string {
    switch name {
    case "月曜日": return "monday"
    // ...
    }
}

初学者が陥りがちなポイント: - ビジネスロジックでデータの正規化を回避しようとする - パフォーマンスを理由に正規化を避ける - データベース設計とアプリケーション設計の分離ができていない

推奨される設計パターン

パターン1: メタデータ分離(推奨)

-- テンプレート(内容のみ)
CREATE TABLE templates (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),           -- "平日朝", "週末", "ハイシーズン"
    description TEXT,
    template_type ENUM('custom'), -- カスタムのみ
    is_active BOOLEAN
);

-- スケジュール(適用ルール)
CREATE TABLE schedules (
    id BIGINT PRIMARY KEY,
    template_id BIGINT,
    day_of_week ENUM('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'),
    selected_date DATE,          -- カスタム日付の場合
    FOREIGN KEY (template_id) REFERENCES templates(id)
);

利点: - 責任の分離が明確 - 拡張性が高い - データの整合性が保たれやすい

パターン2: 継承テーブル(複雑な場合)

-- 基底テーブル
CREATE TABLE templates (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),
    description TEXT,
    is_active BOOLEAN
);

-- 曜日テンプレート
CREATE TABLE weekly_templates (
    template_id BIGINT PRIMARY KEY,
    day_of_week ENUM('monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'),
    FOREIGN KEY (template_id) REFERENCES templates(id)
);

-- カスタムテンプレート
CREATE TABLE custom_templates (
    template_id BIGINT PRIMARY KEY,
    selected_date DATE,
    FOREIGN KEY (template_id) REFERENCES templates(id)
);

実装時の注意点

1. データの一貫性

// 悪い例:名前ベースの判定
func sortByDayOfWeek(templates []Template) {
    for i, template := range templates {
        if template.Name == "月曜日" {
            // ソート処理
        }
    }
}

// 良い例:メタデータベースの判定
func sortByDayOfWeek(templates []Template) {
    for i, template := range templates {
        if template.Schedule != nil && template.Schedule.DayOfWeek != "" {
            // ソート処理
        }
    }
}

2. パフォーマンスの考慮

-- インデックスの適切な設定
CREATE INDEX idx_schedules_day_of_week ON schedules(day_of_week);
CREATE INDEX idx_schedules_template_id ON schedules(template_id);

3. エラーハンドリング

// 無効なデータの処理
func validateTemplate(template *Template) error {
    if template.Type == "weekly" && template.Schedule == nil {
        return errors.New("曜日テンプレートにはスケジュールが必要")
    }
    return nil
}

初学者向けチェックリスト

設計段階

  • 属性に複数の意味が含まれていないか
  • 同じ情報が複数箇所に存在していないか
  • 計算による依存関係を作っていないか
  • 将来的な拡張性を考慮しているか

実装段階

  • 名前ベースの判定を避けているか
  • 適切なインデックスを設定しているか
  • エラーハンドリングを実装しているか
  • テストケースを網羅しているか

保守段階

  • データの整合性を定期的にチェックしているか
  • パフォーマンスを監視しているか
  • 変更時の影響範囲を把握しているか

よくある質問(FAQ)

Q: 正規化するとパフォーマンスが悪くなるのでは?

A: 適切なインデックスとクエリ最適化により、正規化された設計でも十分なパフォーマンスを実現できます。

Q: カスタム設定と固定設定を分けると複雑にならないか?

A: 短期的には複雑に見えますが、長期的な保守性と拡張性を考えると、分離する方が有利です。

Q: 既存システムの正規化はどのように進めるべきか?

A: 段階的に移行し、データ移行ツールとロールバック計画を準備してから実施してください。

まとめ

カスタム設定と固定設定の混在パターンは、多くのシステムで見られる一般的な設計課題です。正規化の原則に従い、責任を明確に分離することで、保守性と拡張性の高いシステムを構築できます。

重要なポイント: 1. データの原子性を保つ 2. 重複を避ける 3. 計算による依存関係を作らない 4. 将来的な拡張性を考慮する

これらの原則を守ることで、長期的に価値のあるシステム設計が実現できます。

Docker開発環境でもローカル言語環境が必要な理由:Go言語と動的型付け言語の違い ※生成AIを活用した記事

はじめに

問題の発見経緯

この記事を書くきっかけとなったのは、実際の開発現場で遭遇した問題でした。

状況: Dockerコンテナ内でGoアプリケーションを実行していた開発環境で、エディタ(Cursor)でのコードジャンプや型補完が正常に動作しない問題が発生しました。

最初の仮説:  - Cursorの設定問題 - Go言語サーバー(gopls)の設定ミス - プロジェクトの依存関係の問題

調査過程:

# 1. 環境変数の確認
go env GOPATH GOROOT GOPROXY
# 結果: GOROOTが/opt/homebrewに設定(異常)

# 2. エラーメッセージの分析
go mod tidy
# エラー: "GOPROXY list is not the empty string, but contains no entries"

# 3. 根本原因の特定
go env GOROOT
# 出力: /opt/homebrew (Homebrewのルートディレクトリ)
# 正しくは: /opt/homebrew/Cellar/go/1.24.4/libexec

発見: GOROOTが正しく設定されていないため、Goの標準ライブラリが見つからず、goplsが正常に動作していませんでした。

解決後: 環境変数を正しく設定することで、エディタでのコードジャンプや型補完が正常に動作するようになりました。

新たな疑問: なぜ動的型付け言語(PythonRubyPHP)では、Docker内で開発していてもエディタの補完機能が正常に動作するのか?

この経験を通じて、静的型付け言語と動的型付け言語では、エディタサポートの仕組みが根本的に異なることに気づきました。本記事では、この違いを詳しく解説し、各言語に最適な開発環境の構築方法を紹介します。


言語の分類と特性

静的型付け言語 vs 動的型付け言語

言語 型チェック コンパイル エディタサポート
Go コンパイル 必要 ローカル環境必須
Java コンパイル 必要 ローカル環境必須
C# コンパイル 必要 ローカル環境必須
Python 実行時(型ヒント可) 不要 Docker内でも可能
Ruby 実行時 不要 Docker内でも可能
PHP 実行時 不要 Docker内でも可能

Go言語の特性とローカル環境の必要性

1. コンパイル時の型チェック

// Go言語 - コンパイル時に型が確定
package main

import "fmt"

type User struct {
    ID   int
    Name string
}

func processUser(user User) error {
    // コンパイル時に型チェックが行われる
    fmt.Printf("User ID: %d, Name: %s\n", user.ID, user.Name)
    return nil
}

func main() {
    user := User{ID: 1, Name: "Alice"}
    // この時点で型エラーが検出される
    processUser(user)
}

重要なポイント: - コンパイル時にすべての型が確定 - インターフェースの実装チェックが静的解析で行われる - エディタの補完機能が型情報に基づいて動作

2. gopls(Go言語サーバー)の動作

# goplsが実行する処理
1. ソースコードの構文解析
2. 型情報の収集と解決
3. 依存関係の解決
4. インターフェースの実装チェック
5. エラーの検出と報告

必要な環境: - Goコンパイラgoコマンド) - 標準ライブラリ($GOROOT/src) - 依存パッケージ($GOPATH/pkg/mod

動的型付け言語との比較

Pythonの場合

# Python - 実行時まで型が不明
def process_user(user):
    # 実行時までエラーが分からない
    print(f"User ID: {user.id}, Name: {user.name}")
    return user.some_method()  # 実行時エラー

# 型ヒントを使用(オプション)
from typing import TypedDict

class User(TypedDict):
    id: int
    name: str

def process_user_typed(user: User) -> None:
    print(f"User ID: {user['id']}, Name: {user['name']}")

エディタ対応: - 型ヒント(typing)を使用した静的解析 - mypy、pyrightなどの型チェッカー - Docker内でも完全に動作可能

注意: 動的型付け言語はローカルに言語をインストールしていなくても基本的な補完機能が動作しますが、詳細な型情報やライブラリ情報は制限される場合があります。完全な機能を利用するにはローカル環境のインストールが推奨されます。

Rubyの場合

# Ruby - 実行時まで型が不明
def process_user(user)
  # 実行時までエラーが分からない
  puts "User ID: #{user.id}, Name: #{user.name}"
  user.some_method  # 実行時エラー
end

# 型チェック(オプション)
require 'rbs'

class User
  attr_reader :id, :name
  
  def initialize(id:, name:)
    @id = id
    @name = name
  end
end

エディタ対応: - RBSRuby Signature)による型定義 - Steep、Sorbetなどの型チェッカー - Docker内でも完全に動作可能

PHPの場合

<?php
// PHP - 実行時まで型が不明
function processUser($user) {
    // 実行時までエラーが分からない
    echo "User ID: {$user->id}, Name: {$user->name}";
    return $user->someMethod(); // 実行時エラー
}

// 型宣言を使用(PHP 7.4以降)
class User {
    public function __construct(
        private int $id,
        private string $name
    ) {}
}

function processUserTyped(User $user): void {
    echo "User ID: {$user->id}, Name: {$user->name}";
}
?>

エディタ対応: - 型宣言(PHP 7.4以降) - PHPStan、Psalmなどの静的解析ツール - Docker内でも完全に動作可能

実際の開発環境での違い

Go言語の開発フロー

# 1. ローカル環境必須
export GOROOT=/opt/homebrew/Cellar/go/1.24.4/libexec
export GOPATH=$HOME/go
export GOPROXY=https://proxy.golang.org,direct

# 2. エディタで開発(ローカルGo環境が必要)
code .  # goplsがローカル環境を使用

# 3. Dockerで実行・テスト
docker-compose up -d
docker-compose exec app go test ./...

制約: - エディタの補完・型チェックにローカルGo環境が必須 - Dockerコンテナ内のGo環境はエディタから直接アクセス不可

動的型付け言語の開発フロー

# Python/Ruby/PHP - Docker内でも開発可能
docker-compose up -d
docker-compose exec app python manage.py runserver
# または
docker-compose exec app rails server
# または
docker-compose exec app php -S localhost:8000

メリット: - エディタがDocker内の言語環境を直接使用可能 - 環境の一貫性が保たれる - セットアップが簡単

技術的な背景

1. 言語サーバープロトコル(LSP)の違い

Go言語(gopls)

// goplsの設定例
{
    "go.useLanguageServer": true,
    "go.goroot": "/opt/homebrew/Cellar/go/1.24.4/libexec",
    "go.gopath": "/Users/username/go"
}

特徴: - コンパイラとの密結合 - 型情報の詳細な解析 - 静的解析による高速な補完

動的型付け言語

// Python/Ruby/PHPの言語サーバー設定
{
    "python.languageServer": "Pylance",  // Python
    "ruby.useLanguageServer": true,      // Ruby
    "intelephense.environment.phpVersion": "8.2"  // PHP
}

特徴: - インタープリタとの連携 - 実行時情報の活用 - 柔軟な型推論

2. ファイルシステムアクセスの違い

Go言語

# ローカルファイルシステムへの直接アクセスが必要
$GOROOT/src/     # 標準ライブラリ
$GOPATH/pkg/mod/ # 依存パッケージ
$GOPATH/bin/     # インストールされたツール

動的型付け言語

# インタープリタが実行時に解決
python -c "import sys; print(sys.path)"  # モジュール検索パス
ruby -e "puts $LOAD_PATH"                # ライブラリ検索パス
php -r "print_r(get_include_path());"    # インクルードパス

実践的な解決策

1. Go言語開発の推奨構成

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    volumes:
      - .:/app
    environment:
      - GOROOT=/usr/local/go
      - GOPATH=/go
      - GOPROXY=https://proxy.golang.org,direct
  
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
      MYSQL_DATABASE: myapp

開発フロー: 1. ローカルにGo環境を構築 2. エディタでローカル環境を使用して開発 3. Dockerでアプリケーションを実行・テスト

2. 動的型付け言語の柔軟な構成

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "3000:3000"
    volumes:
      - .:/app
    environment:
      - RAILS_ENV=development  # Ruby
      - DJANGO_SETTINGS_MODULE=myapp.settings  # Python
      - APP_ENV=development  # PHP

開発フロー: 1. Docker内で直接開発可能 2. エディタがDocker内の環境を使用 3. 環境の一貫性が自動的に保たれる

パフォーマンス比較

エディタの応答性

言語 補完速度 型チェック速度 メモリ使用量
Go 高速 高速
Python 中速 中速
Ruby 中速 中速
PHP 中速 中速

開発体験の違い

Go言語

// リアルタイムでの型エラー検出
func processUser(user User) error {
    return user.SomeMethod() // コンパイル時エラー
}

動的型付け言語

# 実行時までエラーが分からない
def process_user(user):
    return user.some_method()  # 実行時エラー

まとめ

言語特性による違い

  1. Go言語(静的型付け)    - コンパイル時の型チェック    - ローカル環境が必須    - 高速な補完・型チェック    - セットアップに手間が必要

  2. 動的型付け言語(PythonRubyPHP    - 実行時の型チェック    - Docker内でも開発可能    - 柔軟な開発環境    - セットアップが簡単

推奨アプローチ

Go言語開発: - ローカルにGo環境を構築 - Dockerはアプリケーション実行・テスト用 - 開発はローカルエディタで行う

動的型付け言語開発: - Docker内で直接開発 - エディタがDocker環境を活用 - 環境の一貫性を重視

技術選定のポイント

  • プロジェクトの規模: 大規模プロジェクトでは静的型付けの安全性
  • チームのスキル: 動的型付け言語の方が学習コストが低い
  • 開発速度: 動的型付け言語の方が初期開発が高速
  • 保守性: 静的型付け言語の方が長期保守に有利

この違いを理解することで、プロジェクトに最適な開発環境を選択できます。


参考リンク


この記事は実際の開発現場での経験に基づいて執筆されました。コメントや質問があれば、お気軽にお聞かせください。

Firebase Dynamic Links期限切れ対応:Universal Links/App Linksへの緊急移行実践記 AIによる作成(自分用)

 

📱Firebase Dynamic Links期限切れ対応:Universal Links/App Linksへの移行実践記

はじめに

Firebase Dynamic Linksの期限切れにより、緊急的にネイティブのUniversal Links(iOS)/App Links(Android)への移行が必要となりました。実際のプロジェクトで実施した移行作業の手順と知見を共有します。

📋 TL;DR

  • Firebase Dynamic Linksの期限切れにより、ネイティブのUniversal Links/App Linksへの移行が必要
  • iOS: Info.plist、Entitlements、AppDelegate.mmの修正
  • Android: AndroidManifest.xml、MainActivity.ktの修正
  • サーバー側: AASAファイル、assetlinks.jsonの作成
  • React Native側: 期限切れライブラリの削除とURL処理の修正

移行の背景

緊急対応が必要な理由:
  • Firebase Dynamic Linksの期限切れによる機能停止のリスク
  • ユーザー体験の継続性確保
  • ビジネス継続性の維持

🔧実装手順

Step 1

影響範囲の特定

まず、現在使用中のDynamic Linksと影響を受けるURLパターンを確認します。

// 期限切れ前のDynamic Links設定
const dynamicLink = await dynamicLinks().buildLink({
  link: 'https://app.example.com/menu',
  domainUriPrefix: 'https://example.page.link', // 期限切れ
  android: {
    packageName: 'com.example.app',
    minimumVersion: '1'
  },
  ios: {
    bundleId: 'com.example.app',
    minimumVersion: '1.0.0'
  }
})

移行対象URL:

  • 期限切れ: https://example.page.link/xxxxx
  • 移行後: https://app.example.com/menu
Step 2

iOS設定の修正

2.1 Info.plistの修正

ファイル: ios/RootC/Info.plist
<!-- 期限切れ前: Dynamic Links用の設定 -->
<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>com.googleusercontent.apps.XXXXXXXXXX</string>
    </array>
  </dict>
</array>

<!-- 修正後: Universal Links対応 -->
<key>CFBundleURLTypes</key>
<array>
  <!-- 既存のカスタムスキーム -->
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>Bundle ID</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>app</string>
    </array>
  </dict>
  <!-- 追加: Universal Links -->
  <dict>
    <key>CFBundleTypeRole</key>
    <string>Editor</string>
    <key>CFBundleURLName</key>
    <string>Universal Links</string>
    <key>CFBundleURLSchemes</key>
    <array>
      <string>https</string>
    </array>
  </dict>
</array>

2.2 Entitlementsファイルの設定

ファイル: ios/RootC/*.entitlements
<key>com.apple.developer.associated-domains</key>
<array>
  <string>applinks:app.example.com</string>
  <string>applinks:dev.example.com</string>
</array>

2.3 AppDelegate.mmの修正

ファイル: ios/RootC/AppDelegate.mm
// 期限切れ前: Dynamic Links処理(動作しなくなる)
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  return [[FIRDynamicLinks dynamicLinks] handleUniversalLink:url completion:^(FIRDynamicLink * _Nullable dynamicLink, NSError * _Nullable error) {
    // Dynamic Links処理(期限切れで動作停止)
  }];
}

// 修正後: Universal Links処理
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
  // サードパーティSDK処理
  if ([[FBSDKApplicationDelegate sharedInstance] application:app openURL:url options:options]) {
    return YES;
  }
  // Universal Links処理
  return [RCTLinkingManager application:app openURL:url options:options];
}

// 追加: Universal Links専用メソッド
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> * _Nullable))restorationHandler {
  return [RCTLinkingManager application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
}
Step 3

Android設定の修正

3.1 AndroidManifest.xmlの修正

ファイル: android/app/src/main/AndroidManifest.xml
<!-- 期限切れ前: Dynamic Links用の設定 -->
<activity android:name=".MainActivity">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="https" android:host="example.page.link"/>
  </intent-filter>
</activity>

<!-- 修正後: App Links対応 -->
<activity android:name=".MainActivity">
  <intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="https" android:host="app.example.com"/>
    <data android:scheme="https" android:host="dev.example.com"/>
  </intent-filter>
  <intent-filter>
    <action android:name="android.intent.action.VIEW"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <category android:name="android.intent.category.BROWSABLE"/>
    <data android:scheme="app"/>
  </intent-filter>
</activity>

3.2 MainActivity.ktの修正

ファイル: android/app/src/main/java/com/example/app/MainActivity.kt
// 期限切れ前: Dynamic Links処理(動作しなくなる)
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  // Dynamic Links処理(期限切れで動作停止)
  FirebaseDynamicLinks.getInstance()
    .getDynamicLink(intent)
    .addOnSuccessListener(this) { pendingDynamicLinkData ->
      // Dynamic Links処理
    }
    .addOnFailureListener(this) { e ->
      // エラー処理
    }
}

// 修正後: App Links処理
override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  // App Links処理はReact Native側で自動的に処理される
  intent?.data?.let { uri ->
    // カスタム処理
  }
}
Step 4

サーバー側の設定

4.1 Apple App Site Association (AASA) ファイル

ファイル: https://app.example.com/.well-known/apple-app-site-association
{
  "applinks": {
    "apps": [],
    "details": [
      {
        "appID": "TEAM_ID.BUNDLE_ID",
        "paths": [
          "/menu*",
          "/coupon-*",
          "/station-alert*",
          "/match*",
          "/current-orders",
          "/past-orders",
          "/email-verify*",
          "/password-reset*"
        ]
      }
    ]
  }
}

4.2 Android App Links設定

ファイル: https://app.example.com/.well-known/assetlinks.json
[{
  "relation": ["delegate_permission/common.handle_all_urls"],
  "target": {
    "namespace": "android_app",
    "package_name": "com.example.app",
    "sha256_cert_fingerprints": ["SHA256_FINGERPRINT"]
  }
}]

4.3 エラーハンドリング用ファイル

以下のファイルも作成することを推奨します:

  • https://app.example.com/.well-known/404.html
  • https://app.example.com/.well-known/error.html
  • https://app.example.com/.well-known/security.txt
  • https://app.example.com/robots.txt
Step 5

React Native側の修正

5.1 期限切れライブラリの削除

ファイル: package.json
{
  "dependencies": {
    // 削除: 期限切れのDynamic Links
    // "@react-native-firebase/dynamic-links": "^x.x.x",
    // 維持: その他のFirebase機能
    "@react-native-firebase/app": "^x.x.x",
    "@react-native-firebase/auth": "^x.x.x"
  }
}

5.2 アプリケーション設定の修正

ファイル: src/application.tsx
// 削除: 期限切れのDynamic Links関連のimport
// import dynamicLinks from '@react-native-firebase/dynamic-links';

const handleIncomingUrl = async (url: string | null) => {
  if (!url) return
  
  // ユーザー情報の取得
  await dispatch(fetchFirebaseMe())
  
  // URL処理
  dispatch(handleDeepLinkTransition(url))
}

// URLリスナーの設定
useEffect(() => {
  const urlOpenSubscription = Linking.addEventListener('url', e => {
    handleIncomingUrl(e.url)
  })
  
  return () => {
    urlOpenSubscription?.remove()
  }
}, [])

5.3 Deep Link処理の修正

ファイル: src/core/deep-links/action-creator.ts
// 削除: 期限切れのDynamic Links処理
// const handleDynamicLink = async (dynamicLink: any) => {
//   // Dynamic Links処理(期限切れで動作停止)
// };

export function handleDeepLinkTransition(linkOrUrl: { url: string } | string | null) {
  const url = typeof linkOrUrl === 'string' ? linkOrUrl : linkOrUrl?.url
  
  // メール認証
  if (url && url.match(DeepLinkDirectories.EMAIL_VERIFY)) {
    dispatch(signUp(url))
    return
  }
  
  // パスワードリセット
  if (url && url.match(DeepLinkDirectories.PASSWORD_RESET)) {
    dispatch(setOobCode(url))
    RootNavigation.navigate('PasswordReset')
    return
  }
  
  // 通常のdeeplinkルート
  const targetLink = DeepLinkRoutes.find(
    ConstDynamicLink => url && url.match(ConstDynamicLink.DIRECTORY)
  )
  
  if (targetLink) {
    RootNavigation.navigate(targetLink.ROUTE_NAME)
  }
}

5.4 ルート定義の確認

ファイル: src/util/const-deep-link-routes.ts
export const DeepLinkRoutesArray = [
  { DIRECTORY: '/menu', ROUTE_NAME: 'Menu', NEED_SIGN_UP: false },
  { DIRECTORY: '/coupon', ROUTE_NAME: 'Coupon', NEED_SIGN_UP: true },
  { DIRECTORY: '/station-alert', ROUTE_NAME: 'StationAlert', NEED_SIGN_UP: false },
  { DIRECTORY: '/match', ROUTE_NAME: 'RootcMatch', NEED_SIGN_UP: true },
  { DIRECTORY: '/current-orders', ROUTE_NAME: 'OrdersHistory', NEED_SIGN_UP: true },
  { DIRECTORY: '/past-orders', ROUTE_NAME: 'OrdersHistory', NEED_SIGN_UP: true },
  { DIRECTORY: '/email-verify', ROUTE_NAME: 'EmailVerification', NEED_SIGN_UP: false },
  { DIRECTORY: '/password-reset', ROUTE_NAME: 'PasswordReset', NEED_SIGN_UP: false }
]

📋修正ファイル一覧

ファイル 作成方法 変更内容 重要度
ios/RootC/Info.plist 既存ファイル 修正 ⭐⭐⭐
ios/RootC/*.entitlements 既存ファイル 修正 ⭐⭐⭐
ios/RootC/AppDelegate.mm 既存ファイル 修正 ⭐⭐⭐
android/app/src/main/AndroidManifest.xml 既存ファイル 修正 ⭐⭐⭐
android/app/src/main/java/.../MainActivity.kt 既存ファイル 修正 ⭐⭐
package.json 既存ファイル 修正 ⭐⭐
src/application.tsx 既存ファイル 修正 ⭐⭐
src/core/deep-links/action-creator.ts 既存ファイル 修正 ⭐⭐
apple-app-site-association 新規作成 作成 ⭐⭐⭐
assetlinks.json 新規作成 作成 ⭐⭐⭐

⚠️実装時の注意点

緊急対応時の問題

  • 時間的制約: 期限切れまでの限られた時間
  • 機能停止のリスク: 移行中の機能停止
  • テスト時間の不足: 十分なテスト時間の確保困難
  • 設定ミスのリスク: 緊急対応による設定ミス

リスク軽減策

  • 段階的移行: 機能ごとの段階的移行
  • ロールバック準備: 問題発生時の迅速なロールバック
  • 監視強化: 移行後の詳細な監視
  • ユーザー通知: 機能変更の事前通知

🎯移行の効果

1. 機能継続

  • deeplink機能の維持: 期限切れによる機能停止を回避
  • ユーザー体験の継続: 既存機能の継続利用
  • ビジネス継続: サービス停止による影響を最小化

2. 技術的改善

  • 依存関係の解消: 期限切れサービスへの依存を解消
  • セキュリティ向上: 直接的なURL処理
  • メンテナンス性: 設定の簡素化

3. 今後の安定性

  • 長期安定性: ネイティブ機能による長期安定性
  • コスト削減: 期限切れサービスの使用料金削減
  • 技術的負債の解消: 期限切れサービスへの依存解消

🔍今後の改善点

1. 追加対応予定

  • より詳細なアナリティクス
  • エラーハンドリングの強化
  • パフォーマンス監視

2. 監視とメンテナンス

  • URL処理の成功率監視
  • エラーログの分析
  • ユーザーフィードバックの収集

📝まとめ

Firebase Dynamic Linksの期限切れ対応は、技術的負債の解消とユーザー体験の継続を同時に実現する重要な対応でした。

移行のポイント:

  1. 緊急対応の重要性: 期限切れによる機能停止の回避
  2. 段階的移行: リスクを最小化する段階的移行
  3. テストの重要性: 限られた時間での十分なテスト
  4. 監視の強化: 移行後の詳細な監視

この移行により、機能停止を回避し、より安定したdeeplink機能を実現できました。

注意: この記事は、実際のプロジェクトでの期限切れ対応経験に基づいて執筆されています。具体的な設定値やドメイン名は、セキュリティ上の理由で匿名化しています。

🔗参考サイト

GitLabパイプラインのディスク容量不足エラーを解消する方法

GitLab CI/CDのパイプラインを実行していると、ビルドエージェントであるコンテナやVMのディスク容量が不足し、パイプラインが失敗してしまうことがあります。特にDockerを多用している環境では、不要なコンテナイメージやボリュームが蓄積し、ストレージを圧迫していることが原因となっているケースが多く見られます。

ここでは、その解決策として効果的なコマンドと、その背景について解説します。

 

1. ディスク使用量の現状を把握する

 

まずは、何がストレージを消費しているのかを特定することが重要です。以下のコマンドを使ってディスクの使用状況を確認しましょう。

  • df -h: ディスク全体のパーティションごとの空き容量を確認します。

  • du -sh /var/lib/docker: Docker関連のデータがどのくらいディスクを使用しているかを把握します。

通常、EC2インスタンス上でDockerコンテナを動かしている場合、duコマンドで確認したDockerのデータ量が肥大化していることが多いです。

 

2. 不要なDockerデータを一括削除する

 

Docker関連のデータが原因でディスク容量が不足している場合、以下のコマンドを実行することで不要なデータを一括で削除できます。

Bash
 
sudo docker system prune -a --volumes

このコマンドは、以下のデータを削除します。

  • -a (all): 停止中のコンテナ、未使用のイメージ、未使用のネットワークを削除します。

  • --volumes: 未使用のボリュームも削除します。このオプションを付けないと、ボリュームデータが残り、思ったよりディスク容量が解放されないことがあるので注意が必要です。

このコマンドを実行することで、パイプラインの実行中に作成された一時的なデータや、過去のビルドで使われたイメージなどがきれいに整理され、ディスク容量が大幅に解放されます。

 

3. GitLab Runnerが動作するサーバーの特定

 

この作業は、GitLab Runnerが動作しているサーバー上で行う必要があります。もし、どのサーバーがGitLab Runnerとして機能しているか不明な場合は、GitLabの管理画面から確認できます。

  • GitLabの左メニューから Admin Area -> Runners へ移動します。

  • Runnerのリストから、対象のパイプラインで使用されているRunnerを特定します。

  • Runnerの詳細情報から、そのRunnerが動作しているサーバーを特定します。

AWS EC2の場合、通常はインスタンスSSHで接続し、上記コマンドを実行します。接続方法はEC2のコンソールから「接続」ボタンをクリックすると案内が表示されるので、そちらを参考にしてください。


この手順で、GitLabパイプラインのディスク容量不足エラーを解決できる可能性が高いです。定期的なメンテナンスとして、この作業を組み込むことを検討してみてください。



また、手動での実行も有効ですが、定期的なメンテナンスを自動化することで、未然にディスク容量不足を防ぐことができます。自動化の方法としては、主に以下の2つが考えられます。

 

1. GitLab CI/CDのジョブとして実行する

 

GitLab CI/CDのパイプラインに、Dockerキャッシュのクリアジョブを組み込む方法です。

メリット:

  • GitLabのパイプラインの一部として管理できるため、実行履歴やログを一元管理できる。

  • パイプラインの実行後や、特定のスケジュールで自動的に実行することが可能。

実装例: .gitlab-ci.yml に以下のようなジョブを追加します。

 

YAML
 
stages:
  - maintenance

docker_cache_cleanup:
  stage: maintenance
  script:
    - sudo docker system prune -a --volumes -f
  rules:
    - if: '$CI_PIPELINE_SOURCE == "schedule"'
  tags:
    - your-runner-tag

 

  • -f (force) オプションを付けることで、確認プロンプトをスキップして自動で削除を実行します。

  • rules セクションを使うことで、手動実行を禁止し、スケジュール実行のみに限定することができます。

 

2. cronジョブとして実行する

 

GitLab Runnerが動作しているサーバー上で、定期的にコマンドを実行するcronジョブを設定する方法です。

メリット:

  • GitLabのCI/CDとは独立して実行できるため、GitLabのパイプライン実行に影響を与えない。

  • サーバーのOSレベルで管理できるため、シンプルな設定で済む。

実装例: サーバーにログインし、以下のコマンドでcronジョブを編集します。

 

Bash
 
sudo crontab -e

 

以下の行を追加して、例えば毎週日曜日の午前3時に実行するように設定します。

 

コード スニペット
 
0 3 * * 0 /usr/bin/docker system prune -a --volumes -f > /dev/null 2>&1

 

  • 0 3 * * 0: 毎週日曜日(0)の午前3時(3時0分)に実行。

  • /usr/bin/docker: dockerコマンドのフルパスを指定します。which dockerで確認できます。

  • > /dev/null 2>&1: 出力を破棄することで、メール通知などを防ぎます。

 

どちらを選ぶか?

  • GitLab CI/CDのパイプラインに慣れていて、GitLab上で全てを完結させたい場合: GitLab CI/CDのジョブとして実行するのがおすすめです。

  • サーバーのメンテナンスをシンプルに自動化したい場合: cronジョブとして実行するのがシンプルで効果的です。

どちらの方法も、docker system prune -a --volumes -f コマンドを使用することで、手動で確認することなく安全に不要なデータを削除できます。これにより、ディスク容量不足によるパイプラインの失敗を未然に防ぐことができます。

契約利用者ごとの利用状況管理を実現するためのAWS ALB活用ガイド

1. 目的と背景
契約利用者ごとの利用状況を管理することは、サービスのパフォーマンス最適化、利用状況のトラッキング、契約更新、料金プランの見直しに非常に重要です。AWSのApplication Load Balancer(ALB)を使用し、各契約利用者のリクエストを適切に振り分けることで、効率的な管理が可能になります。

2. ALBの利点
個別のリクエスト振り分け:

ALBは、リクエストのヘッダーやURLパスに基づいてリクエストを異なるターゲットグループに振り分けます。これにより契約利用者ごとの処理を簡素化します。
スケーラビリティ:

各契約利用者に対して必要なリソースを柔軟に割り当てることができるため、トラフィックの変動にも対応可能です。
モニタリングと分析:

CloudWatchと連携することで、各契約利用者のリクエスト数、応答時間といったメトリクスを収集し、利用状況を把握できます。
3. 利用状況管理の具体的な手法
a. ターゲットグループの設定
各契約利用者向けに異なるターゲットグループを作成し、ユーザーごとのバックエンドサービスを分けます。この設計により、各利用者に特化した応答が可能になります。
b. ヘッダーやパスに基づくルーティング
リクエストのヘッダー(例: X-Service)やURLパスを利用して、契約利用者を特定し、適切なターゲットグループにリクエストを振り分けます。

例:

X-Service が userA の場合: UserATargetGroup
X-Service が userB の場合: UserBTargetGroup
c. メトリクスとロギングの活用
CloudWatch: 各ターゲットグループのパフォーマンスメトリクス(リクエスト数やレスポンスタイム)を収集し、契約利用者ごとの利用状況を可視化します。
ログ管理: ALBのアクセスログを利用し、各契約利用者の利用状況を詳細に分析できます。
4. 成果の可視化と報告
適切なダッシュボードを設定し、契約利用者ごとの利用状況やパフォーマンスをリアルタイムで監視できるようにします。この情報は契約更新や新しいビジネス戦略の検討に必要です。
5. まとめ
AWSのALBを利用することで、契約利用者ごとの利用状況の管理が簡素化され、効率的なサービス提供が可能になります。ヘッダーやパスに基づくリクエスト振り分け、CloudWatchを活用したメトリクスの収集により、各利用者に特化したサービスを提供しつつ、パフォーマンスの最適化と業務の継続的な改善を図ることができます。

初めてのQML

# QMLを使う

新しくアサインされた案件でQMLという宣言型プログラミング言語を触ることになったので認識の整理を行う。

 

# はじめに

QMLとは?

QMLは、Qtが提供する宣言方のユーザーインターフェース記述言語で

、視覚的な要素やアニメーションを簡潔な構文で記述できるJavascriptベースなフロント技術です。

特徴は

1. 宣言的な設計。

reactと同様コンポーネント単位での管理が行え、わかりやすいコードが書くことができます

2. 高いパフォーマンス

グラフィック、アニメーションに特化して最適化されているそうで、滑らかなUXが期待されるみたい。

3. プラットフォーム間の互換性

幅広いOSでの取り扱いができ、自分が担当した仕事ではLinuxで動かしてました。

 

# QMLの基本構造

 

このような記述で仕組みとしてはreactに近い感覚で記述できます。

それぞれのプロパティも割とわかりやすく。一部癖のあるものもありますが見たままの通りのレイアウトになってきます。

 

# QMLの利点

構成がある程度決まっていると、htmlと比較してflexを使用して調節するよりも容易にレイアウトできると感じました。

 

ただ、日本語での情報が全くないので公式リファレンスを読み解きながらのキャッチアップになるので英語に慣れていないとややしんどさを感じてしまう方もいらっしゃるのかなという感じです。

 

苦労したところもこれと言ってなくリファレンスをしっかり読めば使いこなせる内容でした。

 

# まとめ

自身はQMLのタスクを始めて3日ぐらいで慣れ始めてサクッと描き始めることができたので、javascript、reactに慣れている人だと割と簡単に書くことができるんじゃないでしょうか。

 

また、動的な挙動についてもqmlにjavascriptのような感覚で書き込むことができるので出来ることもかなり多そうと感じました。

 

今度またQMLを触ることがあればより深いところまで考察した記事を書きたいと思います。