バックエンド社内勉強会: 共通化について

はじめに

"どうしてあなたの共通化は間違っているのか"という、以下の@wolfmagnate(進捗ゼミ) さんの記事の第1章から3章をベースに勉強会を実施しました。

https://qiita.com/wolfmagnate/items/73c3770cf036eada630d


第1章

所感

単一責任原則(SRP)からドメイン駆動設計(DDD)へ発展するのは、だいぶ飛躍があるように思う。しかし、「単一」や「責任」をどう捉えるかは人によるので、理想は明確な指針を決めてプロジェクトに臨みたいというのはわかる。

ただ下記の懸念もある

  1. ドメインの範囲定義も千差万別
  2. プロジェクト、案件ごとに決定する必要があるので、その度に工数かかる
  3. ドメインエキスパートの負担が大きいDDDに時間を割きすぎて、成果までの時間が伸びるのは望ましくはない。どの程度かを判断するのはドメインエキスパートの采配にかかる。多く状況を経験するごとに最適が変わりそう。 それなりのレベル感のチームであれば、そこまでかっちり決めなくても良いのだろうとも思う。

第2章

所感

抽象度、文脈ともに心がけてはいるが、実装時に関連すると思わなかった部分が仕様変更で関連づけられて、下位モジュールの抽象度を高くして利用してしまうことがしばしばある。怠けず個別的に分割できるところはしていきたい。

分割に関するメリットの分業促進は確かに感じる。うまく抽象度を整えてくれていると深くコードを見ずに利用でき、それでいて名前や引数から何を処理しているかわかる。

「抽象化したほうが再利用性が上がるよ」と説明することは間違っている。というのは言われるまで気づかなかった。抽象化≒再利用のためと考えていた。ただ隠蔽もしすぎるとバグの時に原因究明しづらい気もする。


第3章

所感

どちらが良い悪いではなく使い分けるのが良いことはわかるが、どちらが最適かケースごとに見極めるのが難しく、それがスキルなのだと感じた。

また「どれだけいい命名をしようが、命名によってモジュールの分割の不備をごまかすことは出来ません」という言葉は図星を突かれた気分。名前に情報を詰め込みすぎた時、スルーせずしっかり分割することを意識したい。

議事録

  • 全体としての所感・感想はあるか?
    • 最初の認識共有は重要
      • 外部の人が多く入ってくると背景が追いつけなかったりするので
      • 共有すべきところは共有して、後から細かく精査する必要がないようにする
  • 今回の話を聞いての感想
    • エンジニアはたいてい、真似はするが、仕様書・ドキュメントは読まない傾向
      • 雑にサンプル作るとそれが元になるので後で大変になる
      • なので実装の例となるサンプルをかっちり作るのが重要

フロントエンド社内勉強会:nuqsを使ってnext.jsのクエリパラメータの同期する

ELWでエンジニアをしております。井立田です。

概要

  • nuqsとは
    • クエリパラメータとアプリケーション状態を型安全に同期するためのライブラリ
      • hooks経由でクリパラメータと同期されたstateを扱える
      • クエリパラメータに型を定義できる
      • server componentでも扱える
      • ライブラリのサイズは約 4.15 kB と軽量 nuqs.47ng.com

使い方

layout.tsx

import { NuqsAdapter } from 'nuqs/adapters/next/app'
import { ReactNode } from 'react'
         
export default function RootLayout({
  children
}: {
  children: ReactNode
}) {
  return (
    <html>
      <body>
        <NuqsAdapter>{children}</NuqsAdapter>
      </body>
    </html>
  )
}

client component

  • useQueryState
    • ローカルstateとクエリパラメータを同期する
    • 第一引数にクエリパラメータのkey名、第二引数にparserで型を指定する nuqs.47ng.com
    • クエリ文字列がURLに存在しない場合はnullを返す
    • default値はdefaultValueオプション、parserの .widhDEfault メソッドで指定できる
'use client'
        
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
        
export function Demo() {
  const [name, setName] = useQueryState('name', parseAsString.withDEfault(''))
        
  return (
    <div>
      <input
        value={name || ''}
        onChange={e => setQuery({ name: e.target.value || null })}
      />
    </div>
  )
}
  • useQueryStets
    • 複数のクエリパラメータをオブジェクト形式でまとめて管理できる nuqs.47ng.com
'use client'
        
import { parseAsInteger, parseAsString, useQueryStates } from 'nuqs'
 
export function Demo() {
  const [query, setQuery] = useQueryStates({
    name: parseAsString.withDefault(''),
    age: parseAsInteger.withDefault(0),
  })
        
  return (
    <div>
       <input
         value={query.name || ''}
         onChange={e => setQuery({ name: e.target.value || null })}
       />
       <input
         value={query.age || ''}
         onChange={e => {
           const value = e.target.value
           setQuery({ age: value ? parseInt(value) : null })
         }}
       />
    </div>
  )
}

server componentでの利用

  • createSearchparamsCache
    • ネストされたServerComponents内で型安全にクエリパラメータを取得できる nuqs.47ng.com
import {
  createSearchParamsCache,
  parseAsInteger,
  parseAsString
} from 'nuqs/server';
        
// クエリパラメータの定義とキャッシュの作成
export const searchParamsCache = createSearchParamsCache({
  name: parseAsString.withDefault(''),  
  age: parseAsInteger.withDefault(0),
});
        
type PageProps = {
  searchParams: URLSearchParams; // Next.jsが提供するクエリパラメータオブジェクト
};
        
// ページコンポーネント
 export default async function Page({ searchParams }: PageProps) {
       
  const { name, age } = await searchParamsCache.parse(searchParams);

  return (
    <div>
      <h1>Profile</h1>
      <p>Name: {name}</p>
      <Age />
    </div>
  );
}
        
// 子コンポーネント
function Age() {
  // ネストされたコンポーネントでpropsを介さずにクエリパラメータにアクセスできる
  const age = searchParamsCache.get('age');
  return <span>The entered age is {age} years old.</span>;
}        

オプション

デフォルトではnuqsはクエリパラメータを以下のように更新する

  1. クライアント側でのみクエリ更新を行い、サーバーには通知しない
  2. 現在の履歴エントリを置き換える。ブラウザバックに対応。
  3. ページのトップにスクロールしない

上記の挙動はオプションで設定可能

  • オプションの指定方法
    1. フックレベルで指定
    2. 更新関数呼び出し時に指定
      • コールレベルの方がフックレベルより優先される
// フックレベルで指定
const [state, setState] = useQueryState(
  'foo',
  parseAsString.withOptions({ history: 'push' })
)

// コールレベルで指定
setState('foo', { scroll: true })

nuqs.47ng.com

メリットとデメリット

  • メリット
    • 軽量かつ簡単
      • クエリパラメータの型変換や同期を自前で実装する必要がない
  • デメリット
    • ローカルstateになるため、クエリパラメータをグローバルstateで管理したい場合は不向き

zero-ETL統合時の制約メモ

以下留意した方がよさそうなものを抜粋。

  • AuroraとRedshiftは同一リージョンに存在する必要あり
  • 統合済のDBクラスタは削除不可
    • 先に統合を全て削除
  • Aurora(PostgreSQL) : Redshift = 1 : 1
    • クォータのデフォルト
  • オブジェクト識別子は 英数字、$、_(アンダースコア) のみ使用可
  • VIEWはレプリケートされない
    • システムやテンポラリも
  • geometry型はサポートしていない
    • bigint、text、date、timestamp など基本的な型のみ

バックエンド社内勉強会: Aurora PostgresSQLとRedshiftのzero-ETL統合について

CTOをしております、村上です。

先日、フロントエンドの勉強会の初回記事を掲載させていただきましたが、 そのバックエンド版の初回となります。 フロントエンド版と同様に担当制で、各回の担当者には議論したいアジェンダとその材料を持って来てもらいます。 業務に完全に直結するものは業務でアウトプットすると思うので、世の中で話題になっているものや自分の中で興味が出ているものなどを想定しています。 ただ、業務と少しずらしていれば問題なしです。

techblog.elw.co.jp

ゼロ ETL 統合により、トランザクションデータまたは運用データが Amazon Redshift でシームレスに利用できるようになるため、抽出、変換、ロード (ETL) オペレーションを実行する複雑なデータパイプラインを構築して管理する必要がなくなります。ソースデータの Amazon Redshift へのレプリケーションが自動化され、同時にソースデータが更新されるため、Amazon Redshift の分析や機械学習 (ML) 機能で利用して、タイムリーなインサイトを導き出し、時間が重要な要素となる重大なイベントに効果的に対応できます

https://aws.amazon.com/jp/blogs/news/amazon-aurora-postgresql-and-amazon-dynamodb-zero-etl-integrations-with-amazon-redshift-now-generally-available/

データ取り込みの変遷

昔:

  • RDBだとデータ量が多い場合に、レポート機能を実現するのにどうしても限界がある
    • (複雑なクエリが必要になることが多い)
  • → RedshiftやBigQueryに入れたくなる
  • → 差分更新が難しい、ETLが面倒、同期タイミングが難しい、という問題がある

近年:

Redshift zero-ETLのメリット:

  • DDLに強い

    One of the most notable innovations is support for expanded DDL events, which allows handling of create, alter, drop, and rename of databases, schemas, and tables, including advanced relationships like cascade operations

仕組み

WALを使うのは同様のようだが、WALの処理をしやすく工夫している様子。

https://aws.amazon.com/blogs/database/amazon-aurora-postgresql-zero-etl-integration-with-amazon-redshift-is-generally-available/

We have mitigated traditional PostgreSQL logical replication challenges by separating storage of the transaction log from storage of the logical replication records (WALs). We have built a specialized storage layer optimized for storing and decoding WALs. This storage layer has added logic that makes it possible for the database engine to push down the filtering, sorting, and ordering of WALs to the storage layer. This allows us to increase parallelization, reduces locking, and shorten the commit times in the database engine, while still achieving ordered writes

議事録

  • テーブルの列の位置は意外に気にしている会社・プロジェクトもあるらしい
    • 定義書と合わせるためなど
    • 並び順でレビュー指摘されることも
  • 案件で、BigQueryのデータを取り込みたいという要望が出ている
    • 逆パターンなので、ETLツールなど使ってデータパイプラインを作成する必要がありそう
  • 既存のクラスタに追加出来るか
    • 以下のように書かれていたので出来るはず
    • Additionally, zero-ETL integrations can now be enabled or disabled on existing Aurora PostgreSQL clusters on database engine version 16.4 or higher.

フロントエンド社内勉強会:HTMX, Hotwireについてキャッチアップ・議論

CTOをしております、村上です。 今回から、社内勉強会の内容をログとして投稿させていただきます。(今回の発表者は私です)
担当制で、各回の担当者には議論したいアジェンダとその材料を持って来てもらいます。 業務に完全に直結するものは業務でアウトプットすると思うので、世の中で話題になっているものや自分の中で興味が出ているものなどを想定しています。 ただ、業務と少しずらしていれば問題なしです。

ログなので内容として不正確、不十分なところはあると思いますが、ご了承ください。

概要

  • どちらも、SPAのレスポンスの良さを極力Javascriptを使わないで実現する仕組み
    • REST APIではなくHTMLの一部をレスポンスで送る
  • ウェブフレームワークMVCだとページの一部を書き換えるのが難しい
    • かといってReactなどを導入すると、ロジックがウェブフレームワーク側とReact側に分かれてややこしくなる

詳細

議論したい内容

  • SaaSの管理画面を作るのに使えないかという観点で調べました
    • 管理画面のフロントエンド側の作り方案
      • v0でNext.jsのUIを作る
      • アプリケーションのUIを流用する
        • 楽ではありそう
        • 使わない部分も多いので取捨選択どうするか
        • 管理画面にはやや過剰
      • テンプレートエンジンで作る
        • 1から作るコストは掛かる
        • 管理画面で必要な機能・デザイン要件は少ないので、その面は合致している

議事録

  • コンポーネントの再利用はどうするか
    • テンプレートエンジンなら、切り出して参照する形になりはず
    • honoならJSX使えるのでありかも
    • https://twig.symfony.com/
      • extendとか出来るので
      • 言語での置き換え(日本語とか)
  • そもそもReactアプリケーションでも、実運用上コンポーネントが使いまわし出来ているのか
    • 開発段階ではある程度汎用性持たせてコンポーネントを作る
    • リリース後一部の箇所のために、拡張する必要が出たとき
      • 互換性を持たせて拡張するか
        • その場合はテストがないと辛い
      • コンポーネントに切り出して、それに一部の箇所だけ置き換える
    • コンポーネントを細かい粒度にすれば汎用性持たせられるが、作る段階だと今後の全部を対応するのは限界がある
  • hydrationの問題はReactのserver actionで解消されそう
  • 管理画面については、tailwind & 生成AIの構成で新規プロジェクトの前に試してみるのはあり

QuarkusにおけるOIDCでのログインユーザー情報の保持方法

弊社ではバックエンドのフレームワークにQuarkusを採用し、Kotlinで使用しています。 表題通りQuarkusでログインユーザー情報を保持する方法が意外に公式に詳しく書かれていないので紹介させていただきます。

以下公式に、リクエストヘッダーから取得する方法は書いてあるので、これを参考にします。

https://quarkus.io/guides/security-customization#jaxrs-security-context

この例からわかるように、SecurityContextにセットするという方法も取ることができます。 ただ、Principalは文字列しか保持できないため、ログインユーザーの細かい情報を持ち回すのには不便です。 ですので以下のようなイメージで実装しました(一部改変しています)。

@Provider
@PreMatching
class SecurityOverrideFilter : ContainerRequestFilter {
    @Inject lateinit var userRepository: UserRepository
    @Inject lateinit var jwt: JsonWebToken
    @Inject lateinit var userIdentity: UserIdentity

    override fun filter(requestContext: ContainerRequestContext) {
        val email = jwt.getClaim<String?>("specific_claim") ?: return
        val user =
            userRepository.findByEmailOrNotFound(email).also {
                userIdentity.user = it
            }
        requestContext.securityContext =
            object : SecurityContext {
                override fun getUserPrincipal(): Principal = Principal { user.id.toString() }

                override fun isUserInRole(role: String?): Boolean = role?.let { it in accessTargetKindKeys } ?: false

                override fun isSecure(): Boolean = requestContext.securityContext.isSecure

                override fun getAuthenticationScheme() = requestContext.securityContext.authenticationScheme
            }
    }
}

@RequestScoped
class UserIdentity {
    var user: User = User()
}

jwtからメールアドレスを取得し、それを元にユーザーを取得し、RequestScopedのインスタンスに保持します。 これにより、CDIが使える箇所では UserIdentity を依存性注入することで自由にログインユーザー情報を取ることができます。

Quarkusにおける認可の実装方法

Quarkusにおける認可の方法は、公式の次のページに書かれています。

https://quarkus.io/guides/security-jwt

ただ、認可の種類には以下の3つがあると考えられ、そのうち1-iの場合の説明となっています。

  1. 通常のRBACで良い場合
    1. IdPで定義したRoleを元に、JWT中のRoleで判断するので問題ない場合: IdPのRoleを元に@RolesAllowedなどのアノテーションを用いる
    2. 権限設定の変更を即座に反映したい場合: 動的にSecurityContextをセットし、そこで定義したRoleを元に@RolesAllowedなどのアノテーションを用いる
  2. 認可の判断がリソースに基づくなど、動的に変わったりAPIの種類によって認可ロジックが異なる場合: ContainerRequestFilterで動的に処理する

それ以外のケースについては詳しく書かれているものが探した限りは見つからなかったです。 今回は2の場合の私達の実現方法を紹介させていただきます。

方法

具体的な実装は以下のようなイメージです(一部改変しています)

@Provider
class AuthFilter : ContainerRequestFilter {
    @Inject lateinit var authorizationService: AuthorizationService
    @Inject lateinit var userIdentity: UserIdentity

    override fun filter(requestContext: ContainerRequestContext) {
        requestContext.uriInfo.pathParameters.getFirst("customerId")?.let { customerId ->
            authorizationService.run(customerId.toLong(), userIdentity.user).let {
                if (!it) {
                    throw ForbiddenProblem(
                        problemDetail = "当該顧客に対する権限がありません",
                    )
                }
            }
        }
        return
    }
}

Jakarta RESTのContainerRequestFilterを用いて実現しています。 パスパラメータを使用したいのとユーザー情報を用いたいため、PostMatchフィルターを使うのがポイントの一つです。 パスパラメータを取得することができるので、API定義で{customerId}が存在するAPIについては、 リクエスト時のcustomerIdを取得し、それを元にAuthorizationServiceを呼び出しログインユーザーに当該customerに対して権限があるかチェックしています。 ある場合はtrueが返ってくるので、ない場合は例外を飛ばしています。

お気づきの方もいるかと思いますが、個々のAPI実装の最初の部分に同じロジックを呼び出す方法でも勿論同様に実現できます。 ただ、入れ忘れのリスクがあるのと、入れる工数も些細ですがかかります。 この方法では、パスパラメータ名さえ間違えなければAPI側で意識しなくても個々のAPIに導入されるというメリットがあります。 また、この例ではcustomerですが、他のエンティティについては別のロジックがある場合、また別のFilterを定義すれば実現できます。