wancoro blog

学んだことをアウトプットしていきたいと思います

【Java】No qualifying bean of type 'xxx' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations

テストを実行するとエラー

No qualifying bean of type 'xxx' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations

beanが読み込まれていないみたい?
web.xmlコンポーネント追記し忘れていたので追加して解決。

<context:base-package="xxx" />

【MySQL】ユニークキー制約はついてないけど重複させたくない時にやったこと

実装環境

やりたいこと

ユーザ作成APIが同時に実行された際に、同じnameのユーザを登録させたくない。
以下のような userテーブルがあり、nameにはユニークキー 制約はついていない。

CREATE TABLE `user` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(255) NOT NULL,
 `age` int(11) NOT NULL,
 PRIMARY KEY (`id`)
);

試したこと

2つのトランザクション内で同じクエリを発行し、動作を確認する。
1. SELECT FOR UPDATE を使う。

BEGIN;

SELECT * FROM user WHERE name='test' FOR UPDATE;

INSERT INTO user( name, age ) VALUES ( 'test', 20 );

COMMIT;

この方法だと、トランザクション1で INSERT 後、トランザクション2で INSERTするとデッドロックが発生してしまった。
デッドロック発生によりロールバックすることで、同じnameのユーザレコードは重複しないという条件は満たせるが、デッドロックはあまりよろしくない。
2. NOT EXSITS を使う。

BEGIN;

INSERT INTO user( name, age ) 
SELECT 'test', 20
WHERE NOT EXISTS( SELECT * FROM user WHERE name='test' );

COMMIT;

この方法だとトランザクション1で INSERT 後、トランザクション2で INSERTしても、1がcommitされるまで2のクエリは実行されない。
サブクエリでSELECTをつけることにより、テーブルロック(インテンションロック)がかかっている様子。

結論

今回の場合は NOT EXISTS でうまくいった。
ユニークキー制約をつければ、こんな複雑なクエリを実装する必要はないので、
可能であればユニークキー制約をつけ、deplicatedError が発生したらロールバックするような挙動が望ましい。

7/16 追記

MySQL5.5の場合

なんとMySQL5.5で実装する必要があった。 MySQL5.5の場合、上記のNOT EXISTS 文ではsyntaxエラーが出てしまい使えない。 というわけで再度調べた。

BEGIN;

INSERT INTO user(name, age) 
SELECT name, age FROM (SELECT 'test' AS name, 20 AS age ) AS u 
WHERE NOT EXISTS ( SELECT * FROM user WHERE name='test' );

COMMIT;

上記のSQLで成功。

MySQL5.5の場合、FROMをつけて値を取得する場所を明示的に指定する必要があるようだ。
サブクエリにはaliasをつけないと怒られるので、
特に使用していないがAS uFROMの後につけている。

参考:

stackoverflow.com

【NGINX】phusion/passenger-dockerでレスポンスヘッダーを編集する

やりたいこと

HTTPのレスポンスヘッダーにnginxなどのバージョン情報を表示させないようにしたい。
調べるとwget でnginxを取得して展開するような記述が多いが、 今回はdockerイメージに phusion/passenger を使用しており、 イメージ内にnginxが含まれているため、別の方法で実装したい。

実行環境

  • docker
  • phusion/passenger-ruby26
  • nginx 1.14.0

修正前のレスポンスヘッダー

< status: 302 Found
< cache-control: no-cache
...
< x-powered-by: Phusion Passenger 6.0.4 *
< server: nginx/1.14.0 + Phusion Passenger 6.0.4 *

*のついたやつを消したい。

実装方法

  • nginxのモジュールをインストールする。
# Dockerfile
RUN apt-get update \
    && apt-get install -y \
        libnginx-mod-http-headers-more-filter \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
  • 設定ファイルを作成する。
# headers_filter.conf
server_tokens off;
more_clear_headers 'Server' 'X-Powered-By';

server_tokens off: nginxのバージョンを削除
more_clear_headers: 指定した行を削除

  • 設定ファイルをDockerの/etc/nginx/conf.d配下に配置する
ADD ./headers_filter.conf /etc/nginx/conf.d/headers_filter.conf

/etc/nginx/main.d とかもありますが、conf.d でないと動かなかったので、こちらに置いてください。

めちゃめちゃ詰まりましたが、これだけでOK。

そのほか

  • more-clear_header を複数行に分けて書くと動かなかった。
# headers_filter.conf
server_tokens off;
more_clear_headers 'Server';
more_clear_headers 'X-Powered-By';
  • nginx-plus-module-headers-more というモジュールは商用nginxでしか使えないらしい。

Railsアプリからdocker上のmysqlに接続する方法

Railsアプリからdocker上のmysqlに接続する際にエラーが出てしまったため、事象と解決策を備忘録として残す。

開発環境

事象

最初に書かれていたdatabase.ymlはこんな感じ。

# config/database.yml

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /var/run/mysqld/mysqld.sock

この記述だと rails db:create 時にエラーが出てしまった。

Mysql2::Error::ConnectionError: Can't connect to local MySQL server through socket '/var/run/mysqld/mysqld.sock' (2)

mysql側でsocketの値を確認してみる。

mysql> show variables like '%socket%';
+-----------------------------------------+-----------------------------+
| Variable_name                           | Value                       |
+-----------------------------------------+-----------------------------+
| performance_schema_max_socket_classes   | 10                          |
| performance_schema_max_socket_instances | -1                          |
| socket                                  | /var/run/mysqld/mysqld.sock |
+-----------------------------------------+-----------------------------+

こちらは問題なさそう。

解決策

database.ymlにホストを指定する記述を追加して解決。

# config/database.yml

default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  socket: /var/run/mysqld/mysqld.sock
  host: 127.0.0.1    <-- ここを追加

これがないと、dockerを見に行ってくれないよう。

Kubernetes

manifest

  • Service:

    • Type:
      • ClusterIP: クラスター内部からのみ疎通
      • NodePort: 静的なポート(NodePort)上でServiceを公開する
      • LoadBalancer: クラウドプロバイダーのロードバランサーを使用して、Serviceを外部に公開する
    • Ports:
      • Port: 公開するポート
      • TargetPort: 転送する先のポート
  • StatefulSet: 一意で永続的な ID と固有のホスト名を持つ [Pods] のセットを表す
    メリット: pod再作成後も同じホスト名を維持する

  • CronJob: 時間ベースのスケジュールでJobを作成する

  • Job: Jobは1つ以上のPodを作成し、指定された数が正常に終了した際にJobが完了となる

    • kubectl create -f [ファイル名] でJobを手動実行できる
  • Quality of Service(QoS)クラス: 優先度が低いQoSクラスをもったpodからkillされる

  • Pod:

    • Image: GCRを使用する場合、イメージタグやイメージダイジェスト(sha256でハッシュ化された一意の値)で、イメージを指定することができる
      podで読み込むimageIDを変更したい場合は、ダイジェストをimageに指定する
# タグ
image: gcr.io/xxx/xxx:latest
# ダイジェスト
image: gcr.io/xxx/xxx@sha256:xxxxxxxxxxxxxxxx
  • ImagePullPolicy
    • IfNotPresent: ローカルでイメージが見つからない場合にのみイメージをpullする
    • Always: Podの起動時に常にイメージをpullする
  • env: 環境変数を定義する

    • manifestに直接書き込む
      • name: value: を指定することで定義できる
    • podの情報を環境変数化する
      • fieldRef: Pod自身のIP Address、起動時間などのPodに関する情報を呼び出せる
  • Volumes

    • Podやコンテナが削除された場合でも、データを保持してくれる
    • Pod内のコンテナ間でデータの共有ができる
    • emptyDir: Podが削除された際にデータも削除する
    • ボリューム名が一意であれば、複数のボリュームを定義することもできる

第6章 Kubernetes におけるストレージのプロビジョニング - Red Hat Customer Portal

  • PersistentVolume: 永続ボリューム

  • InitContainers

    • 同じPod内の containers で指定したコンテナが起動する前に初期化処理を目的として起動することができる
    • セキュリティの理由からアプリケーションコンテナに含めたくないユーティリティーを含んだり実行できる
    • アプリケーションコンテナの実行の前に起動が完了する
  • Security Context: コンテナに対して権限やアクセス制御の設定をする

    • runAsUser: コンテナのプロセスのUIDを指定する
  • BackendConfig(GKE)

    • アクセスにIP制限をかける
    • ServiceのAnnotationsに beta.cloud.google.com/backend-config を記述して利用する
  • CronJob: 時間ベースのスケジュールでJobを作成する
    • concurrencyPolicy: 古いJobがまだ動いてる際に、新しいJobを実行するかどうかを設定する
      • Allow(default):同時実行に対して制限を行わない
  • Job:
    • 1つ以上のPodを作成し、指定された数が正常に終了するのを保証する
    • 指定された数が正常に終了した際にJobが完了となる
    • Jobを削除するとそのJobによって作成されたPodも削除される
    • restartPolicy:
      • Never:
        Jobが失敗した際、コンテナを再起動しない
        PodがErrorになった際にはJobが新しいPodを作成して実行する
    • backoffLimit: エラー時の再試行回数を指定する
    • activeDeadlineSeconds: Jobの開始時点からの有効制限時間を秒数で指定する
  • Ingress:
    • rules: バックエンドへ振り分ける際のルールを定義する
      • host: HTTP HOSTヘッダの値が一致した場合に振り分ける
        • backend: 振り分け先を指定
    • defaultBackend: ルールに一致しないリクエストの振り分け先を定義する
  • NodeAffinity/NodeAntiAffinity: PodがどのNodeにスケジュールされるかを制限する(NodeSelectorと同じ)
    • nodeSelectorTerms: podをデプロイするノードの条件を定義する
  • PodAffinity: 指定されたラベルに一致するPodが動作しているNodeがもつ topologyKey が同一のNodeにPodを配置させる
  • PodAntiAffinity: topologyKey の範囲内に指定されたラベルに一致するPodが動作していなければ、topologyKey 範囲内のNodeのいずれかにPodを配置する

  • ClusterRoleBinding: ロールとユーザを紐付ける

コマンド

  • コンテナ内のファイルをホストにコピー
kubectl cp some_container:/path/to/target_file ./target_file
  • ホスト上のファイルをコンテナ内の指定ディレクトリにコピー
$ kubectl cp target_file some_container:/path/to/target_dir/

Apache Camel

.routeId(hogehoge)
routeに名前をつける。

.to("mock:")
何も処理しない時に使う。
テストが書きやすくなる。

.process(new Hogehoge)
ProcessorをImplementしたクラスに処理を書いたものを呼び出す。

.constant("hogehoge")
文字列を指定する。

コンポーネント一覧

Mock - Apache Camel

【JUnit5】DBUnitを使ってDaoのテストを実装する

難しくて1週間くらいかかりました・・・。

実装環境

テストしたいコード

@Repository
public class UserDaoImpl implements UserDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public List<UserModel> getUser(Integer id) {
        List<UserModel> list = jdbcTemplate.query("SELECT * FROM users WHERE id=?", new Object[]{id},
                new RowMapper<UserModel>() {
                    public UserModel mapRow(ResultSet resultSet, int rowNum) throws SQLException {
                        UserModel user = new UserModel();
                        user.setId(resultSet.getInt("id"));
                        user.setName(resultSet.getString("name"));
                        return user;
                    }
                });
        return list;
    }
}

idでSELECTしたuserのリストを返却する簡単なもの

事前準備

テスト用のDBを用意しておく

database name user password
springmvc_test pgadmin pgadmin
column type
id integer
name varchar(16)

実装手順

ライブラリを追加

pom.xmlを編集する

<dependency>
  <groupId>org.dbunit</groupId>
  <artifactId>dbunit</artifactId>
  <version>2.6.0</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>com.github.springtestdbunit</groupId>
  <artifactId>spring-test-dbunit</artifactId>
  <version>1.3.0</version>
  <scope>test</scope>
</dependency>

テスト用コンテキストファイルを作成する

// src/main/webapp/WEB-INF/test-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <mvc:annotation-driven/>

    <context:component-scan base-package="com.springMvcTutorial.controllers"/>
    <context:component-scan base-package="com.springMvcTutorial.dao" />
    <context:component-scan base-package="com.springMvcTutorial.models" />
    <context:component-scan base-package="com.springMvcTutorial.services" />


    // テスト用のDBを設定する 
    <bean id="dataSource"
          class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="org.postgresql.Driver" />
        <property name="url" value="jdbc:postgresql://localhost:5432/springmvc_test" />
        <property name="username" value="pgadmin" />
        <property name="password" value="pgadmin" />
    </bean>

    <bean class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg ref="dataSource" />
    </bean>
</beans>

テスト用DBにインサートするxmlファイルを用意する

// src/test/resources/data/insert_test_user.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <USERS
        ID="10"
        NAME="testUser"
    />
</dataset>

テストクラスを実装する

@SpringJUnitWebConfig(locations = { "file:src/main/webapp/WEB-INF/test-context.xml"} )
@TestExecutionListeners({
        ServletTestExecutionListener.class,
        DirtiesContextBeforeModesTestExecutionListener.class,
        DependencyInjectionTestExecutionListener.class,
        DirtiesContextTestExecutionListener.class,
        TransactionDbUnitTestExecutionListener.class})
class UserDaoImplTest {

    @Autowired
    private WebApplicationContext webApplicationContext;
    private JdbcTemplate jdbcTemplate;

    @Inject
    private UserDaoImpl userDao;

    @Test
    @DatabaseSetup("/data/insert_test_user.xml")
    @DisplayName("getUser on userDaoImpl")
    void getUser() throws Exception {
        UserModel user = new UserModel();
        user.setId(10);
        user.setName("testUser");

        List<UserModel> users = Arrays.asList(user);

        List<UserModel> resultWithUser = userDao.getUser(10);
        List<UserModel> resultNoUser = userDao.getUser(1);

        assertEquals(resultWithUser.size(), 1);
        assertEquals(resultWithUser, users);
        assertEquals(resultNoUser.size(), 0);
    }
}

解説

  • テスト用のコンテキストファイルパスを指定する
@SpringJUnitWebConfig(locations = { "file:src/main/webapp/WEB-INF/test-context.xml"} )
  • JdbcTemplateをインジェクトする
@Autowired
private WebApplicationContext webApplicationContext;
private JdbcTemplate jdbcTemplate;
  • テスト対象のクラスをインジェクトする
@Inject
private UserDaoImpl userDao;
  • テスト用のsqlを読み込みする
    (デフォルトではCLEAN_INSERTでの実行となり、
    XMLファイルで参照しているテーブルのデータを削除してから新しくレコードが挿入される)
@DatabaseSetup("/data/insert_test_user.xml")

参考資料

springtestdbunit.github.io