やる気がストロングZERO

やる気のストロングスタイル

2021年活動まとめ

思ったよりもあっという間だった。
仕事を通じて以前よりもよりエンジニア業について理解が進んだ一年だった。エンジニア業難しい。。

以下その他所感など。

アーキテクチャやコード・データの構造について考える事について

去年から引続き保守しやすいコードやデータ構造とはどういうものかを考えていたが、それなりに自分なりに考えがまとまってきたため考える機会が少なくなった。
代わりにそれを実務でどうやって活かすかについて考えることが多くなった。
実務では一人の独断で方針を決められないので、全員の意見を調整したりする必要があるが簡単ではない。
もちろん、常に自分の意見が正しいとは限らないし。
そのあたりが次の課題。

仕事について

仕事はずっと忙しかった。

続きを読む

Hibernateでクラステーブル継承を実現する

spring bootでデータアクセスにspring-boot-starter-data-jpa(実装的にはHibernateが使われている)を使っている時、 クラステーブル継承の実現の仕方を理解するのに時間がかかったのでメモ。
[クラステーブル継承]については過去postの中で記載している

参考) Hibernate Inheritance Mapping | Baeldung

※one to one の関連で実現しようと頑張ってたがやり方を間違っていた。普通に継承すればよかった模様。
※下記サンプルコードは個人的都合でEntityではなくMapperという命名を使っている。

データ構成

スーパータイプとしてuserテーブルがあって、サブタイプとしてindividual_user(個人ユーザー)がある。
※ここには記載してないが、サブタイプとして他にcorporate_user(企業ユーザー)があったりする想定

usersテーブル(スーパータイプ)

id created_at updated_at
1 2021-11-11 2021-11-11

individual_usersテーブル(サブタイプ)

user_id name
1 yamada

users.idとindividual_users.user_idがリファレンスキーとなっている。

Entityコード

UserMapper.java

import lombok.Data;

import javax.persistence.*;
import java.time.ZonedDateTime;

@Data // <- getter, setterを書くのがめんどくさいので今回lombokを使っているがintellij曰くJPAEntityでは使わないほうがいいらしい。
@Entity
@Inheritance(strategy = InheritanceType.JOINED)  // <- これでクラステーブル継承であることを指定する
@Table(name = "users")
public class UserMapper {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private ZonedDateTime createdAt;
    private ZonedDateTime updatedAt;

IndividualUserMapper.java

import lombok.Data;

import javax.persistence.*;
import java.time.ZonedDateTime;

@Data // <- getter, setterを書くのがめんどくさいので今回lombokを使っているがintellij曰くJPAEntityでは使わないほうがいいらしい。
@Entity
@PrimaryKeyJoinColumn(name = "userId") // <- これでjoinに使うカラムを指定する
@Table(name = "individual_users")
public class IndividualUserMapper extends UserMapper { // <- 普通に継承する
    private String name;
}

Repositoryコード

import org.springframework.data.jpa.repository.JpaRepository;

public interface IndividualUserInfraRepository extends JpaRepository<IndividualUserMapper, Long> {

使う側

// 保存
var userMapper = new IndividualUserMapper();
userMapper.setName("yamada");
userMapper.setCreatedAt(ZonedDateTime.now());
userMapper.setUpdatedAt(ZonedDateTime.now());
var savedIndividualUser = individualUserInnerRepository.save(userMapper);

// 取得
var loadedIndividualUser = individualUserInnerRepository.findById(savedIndividualUser.getId());

ローカルとdockerコンテナ側のUID,GIDを揃える

dockerコンテナを使ってローカルで開発作業をするとき、local側のディレクトリとコンテナ側のディレクトリを共有してコンテナ側で生成されるファイルを永続化したりする事がある。
このとき、コンテナ側で生成されたファイルがroot権限で作成されたりするとlocal側での取り扱いがめんどくさい。(sudoしないとファイルを削除できなかったり)

ローカル側のUID, GIDとコンテナ側でのUID, GIDを揃えられるようにDockerfileやdocker-composeを用意しておくと、このあたりがスムーズに行くようになる。

#Dockerfile
FROM ubuntu:20.04

ARG USERNAME=app
ARG GROUPNAME=app
ARG UID=1000
ARG GID=1000

RUN groupadd -g $GID $GROUPNAME && \
    useradd -m -s /bin/bash -u $UID -g $GID $USERNAME

USER $USERNAME
WORKDIR /home/$USERNAME/

これで

$ docker build . --build-arg UID="`id -u`"  --build-arg GID="`id -g`"

でローカルのUID, GIDでコンテナ側のユーザーを作成し揃えることができる。

僕はdocker-composeを使いたいので以下の感じで書いて

# docker-compose.yml
version: '3'
services:
    app:
        build:
            context: .
            args:
                UID:1000
                GID:1000
                USERNAME:app
                GROUPNAME:app
        volumes:
            - ./data:/home/app

以下の感じで実行する

docker-compose build --build-arg UID="`id -u`" --build-arg GID="`id -g`"

さらに毎回上記コマンドを打つのがめんどくさいのでmakefile

# makefile
build_image:
        bash -c 'docker-compose build --build-arg UID="`id -u`" --build-arg GID="`id -g`"'

と書いておき、

make build_image

でビルドする。
bash -c を書いているのは僕がfishを使っているため。こういうのがあるからやっぱりPOSIX互換のshellを使うようにしたほうがいいのかなぁと最近は思い始めた。

ウィザードUIのコード構成失敗して作り直した

rshinmemoというTUIメモアプリを作ってる。

github.com

操作がいわゆるウィザードUIの構成になっている。

メモを追加する際、[「新規追加」を選択する] => [日付を選択する] => [メモ名を入力する]
(今思えばウィザードUIにせず、1画面で入力させればいい気もするけど、作ったときのライブラリの事情でこうなっている)

このウィザードUIを最初作った時なかなか厳しい構造にしてしまって作り直したのだが、その辺りのことを書く。

最初の構造:呼び出しのネスト構造にしてしまった。

ウィザード画面を進むたびに、次の画面の表示と処理を持ったオブジェクトを呼び出す構造にしていた。
こんな感じ。

f:id:gatsunto-mix:20210814212349p:plain
呼び出しネスト

結果、すごくめんどくさい感じになってしまった。

◆面倒くさいこと。

とにかくデータを持ち回らないといけない。 一番最初の画面は、後続の画面が必要になるデータやオブジェクト等を全部用意して渡さなければならない。 (依存を少なくしたいので、必要なデータ等やオブジェクトはなるべくDIしたかった)

持ち回っているデータが意図通りでなかったりしてバグってたとき、どこでそれが起こったのか非常に追いかけにくかった。

作り直した構造:Viewと処理を完全分離し、ネストにならないようにした。

f:id:gatsunto-mix:20210814211941p:plain
Viewとロジックを分離

View内で行われる処理は、外から渡されるようにした。 そうすることで、処理はすべてのデータに簡単にアクセスできるので、データを持ち回る必要もなくなった。

webだったら画面遷移時に再度リクエストが飛んできてcontrollerに処理が自然に帰ってくるのであまり意識しなくてもよかったが、GUIアプリだと意図的に構造を作らないとViewとロジックが密になりやすいのかもなと思った。

dlvで実行中Processにアタッチしてデバッグ

Goでcuiアプリ作ってる。

ビジネスロジックはメソッドで切り出しているのでIDEの機能でテストコードから実行してデバッグしてたが、画面に近い部分のデバッグするの辛いなと思ってた。

以下のようにすれば動作中プロセスにデバッガをアタッチしてデバッグ可能。

デバッグ対象アプリ起動
$ ./main

プロセス番号を調べる
$ ps aux 

デバッガをアタッチ。
$ dlv attach プロセス番号
(dlv) 

bでbrake pointを指定して、cでコンティニューして操作してればbrake pointで止まる

Golandでも普通にアタッチできる。

デバッガーで実行中の Go プロセスにアタッチする | GoLand

高トラフィックに耐える構成を考える

トラフィックに耐える構成についてざっくり書いてみる。
インフラエンジニアでもないので間違いあるかも。

アプリケーションサーバ

ステートレスに作っておいて、稼働台数を増やしてLBで振り分ける。
LBに対してはDNSラウンドロビンで負荷分散させる。

DBサーバー

やっぱりDBへの負荷をどう下げるかがキモ。
参照と更新で分けて考える。

参照:

キャッシュサーバーを用意してキャッシュし、DBへのアクセス自体をできる限り減らす。
レプリケーションセカンダリを複数用意し、参照リクエストを分散させる。(ここにもLB使える?)

更新:

更新はプライマリに対して行わなければならず、単純に数を増やして対応することができない。
出来る限りスケールアップによって性能を上げる。
特にメモリは多く確保し、DBのデータをすべてメモリに乗せられるようにしてIOの発生を出来る限り避ける。

更新が極端に多い特性のデータはKVSへ保持させることを検討する。
redisはクラスタ構成を組むことで更新に対してもスケールアウトによる負荷分散が可能。

最終手段としてテーブルの水平分割や垂直分割を行って、プライマリDB自体を分割し、アクセスを分散させる。

そのうち検証とかしたい。raspberry piとか複数買って小さい環境作って限界目指してみるとか。

行われている処理がイメージできるようなテーブル構造を設計したい

見れば行われている処理がイメージできるようなテーブル構造を設計するのが良いと思っている。

このテーブル構造で何が読み取れるか?

user

id state
1 active
2 active
3 disactive
... ...

userはステータスを持っているのはわかる。
データを見るとほとんどactiveでたまにdisactiveがあるように見える。
active, disactiveだけなのかどうかはわからない。 どんなふうに遷移するのかもわからない。

だから、こんなテーブル構造がいいと思っている

user

id user_state_id
1 2
2 2
3 3
... ...

user_state

id name
1 preparation
2 active
3 disactive

user_state_history

id user_id user_state_id until_at
1 1 1 2021-03-15 12:00:00
2 2 1 2021-03-15 13:00:00
3 3 1 2021-03-15 13:00:00
3 3 2 2021-03-15 14:00:00
... ... ... ...

先程のテーブルとは違って、userはpreparation, active, disactiveの状態があることがわかる。他にないこともわかる。
stateはpreparation => active => disactive と遷移しているのも分かる。過去どういうstateを辿ったのかもわかる。

コードを見なくてもなんとなくイメージできる。