CLOVER🍀

That was when it all began.

Spring Frameworkの@Transactinalのロールバックに関する設定を確認する

これは、なにをしたくて書いたもの?

Spring Frameworkで@Transactionalアノテーションを使った時の、ロールバックに関する設定を確認しておきたいな、と
思いまして。

@Transactionalアノテーションを使ったトランザクション管理

Spring Frameworkのドキュメントとしては、こちらに記述があります。

Using @Transactional

メソッド、またはクラスに@Transactionalアノテーションを付与することで、そのメソッド、またはクラス(およびサブクラス)の
メソッドをトランザクションに参加させることができます。

Used at the class level as above, the annotation indicates a default for all methods of the declaring class (as well as its subclasses).

@Transactionalアノテーションに設定可能な項目は、こちら。

@Transactional Settings

トランザクション分離レベル、Read-Only、ロールバックに関する設定が可能です。

Javadocを見てもよいでしょう。

Transactional (Spring Framework 5.3.6 API)

ちなみに、従来のXMLで設定する方法についてはこちらに記述があります。

Example of Declarative Transaction Implementation

ロールバックについて

さて、ちょっとロールバックに関する説明を見てみましょう。デフォルトではRuntimeExceptionがスローされるとロールバックし、
チェック例外ではそうならない、と記述があります。

Any RuntimeException triggers rollback, and any checked Exception does not.

@Transactional Settings

ちなみに、宣言的トランザクションの説明のところには、Errorも対象になることが書かれています。

That is, when the thrown exception is an instance or subclass of RuntimeException. ( Error instances also, by default, result in a rollback).

Rolling Back a Declarative Transaction

@TransactionalのJavadocを見てみましょう。

If no custom rollback rules apply, the transaction will roll back on RuntimeException and Error but not on checked exceptions.

Transactional (Spring Framework 5.3.6 API)

デフォルトではRuntimeExceptionとErrorが対象となる、で良さそうです。

このあたりをソースコードを見つつ、設定を変えたりしてみようというのが今回のお題です。

ちなみに、今回は扱いませんが「ロールバックしない」例外を定義することも可能ですね。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.11 2021-04-20
OpenJDK Runtime Environment (build 11.0.11+9-Ubuntu-0ubuntu2.20.04)
OpenJDK 64-Bit Server VM (build 11.0.11+9-Ubuntu-0ubuntu2.20.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.1 (05c21c65bdfed0f71a2f2ada8b84da59348c4c5d)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.11, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-72-generic", arch: "amd64", family: "unix"

データベースにはMySQL 8.0.24を使い、172.17.0.2で動作しているものとします。

準備

Spring Initializrでプロジェクトを作成します。jdbcとmysqlを依存関係に含めました。

$ curl -s https://start.spring.io/starter.tgz \
  -d bootVersion=2.4.5 \
  -d javaVersion=11 \
  -d name=transactional-rollback-rule \
  -d groupId=org.littlewings \
  -d artifactId=transactional-rollback-rule \
  -d version=0.0.1-SNAPSHOT \
  -d packageName=org.littlewings.spring.jdbc \
  -d dependencies=jdbc,mysql \
  -d baseDir=transactional-rollback-rule | tar zxvf -


$ cd transactional-rollback-rule

Spring Bootのバージョンは2.4.5、Spring Frameworkのバージョンは5.3.6です。

できあがったプロジェクトの依存関係等は、こちら。

 <properties>
        <java.version>11</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

動作確認はテストコードで行うことにします。

テストコードを実行するために、@SpringBootApplicationアノテーションが付与されたソースコードを用意。

src/main/java/org/littlewings/spring/jdbc/App.java

package org.littlewings.spring.jdbc;

import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class App {
}

テストコードの雛形を作製。

src/test/java/org/littlewings/spring/jdbc/TransactionalRollbackTest.java

package org.littlewings.spring.jdbc;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@SpringBootTest
class TransactionalRollbackTest {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @BeforeEach
    public void createTable() {
        jdbcTemplate.execute("drop table if exists sample");

        jdbcTemplate.execute("create table sample(word varchar(10));");
    }

    // ここに、Beanのインジェクションとテストを書く!
}

テストの実行ごとに、テーブルをdrop、createするようにしています。

    @BeforeEach
    public void createTable() {
        jdbcTemplate.execute("drop table if exists sample");

        jdbcTemplate.execute("create table sample(word varchar(10));");
    }

アプリケーションの設定は、こちらだけです。

src/test/resources/application.properties

spring.datasource.url=jdbc:mysql://172.17.0.2:3306/practice
spring.datasource.username=kazuhira
spring.datasource.password=password

まずは単純に使ってみる

最初は、@Transactionalを単純に使ってみましょう。

@Transactinalを付与したメソッドを持つ、こんなクラスを用意。

src/main/java/org/littlewings/spring/jdbc/SimpleTransactionalService.java

package org.littlewings.spring.jdbc;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class SimpleTransactionalService {
    JdbcTemplate jdbcTemplate;

    public SimpleTransactionalService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional(readOnly = true)
    public List<String> findAll() {
        return jdbcTemplate.queryForList("select word from sample order by word", String.class);
    }

    @Transactional
    public void insertSuccess(String value) {
        jdbcTemplate.update("insert into sample(word) values(?)", value);
    }

    @Transactional
    public void insertWithRuntimeException(String value) {
        jdbcTemplate.update("insert into sample(word) values(?)", value);

        throw new RuntimeException("Oops!!");
    }

    @Transactional
    public void insertWithException(String value) throws Exception {
        jdbcTemplate.update("insert into sample(word) values(?)", value);

        throw new Exception("Commit?");
    }
}

@Transactionalをつけた、insertが成功するメソッド、RuntimeExceptionをスローするメソッド、Exceptionをスローする
メソッド、そしてデータを取得するメソッドを定義しています。

テストコード。

    @Autowired
    SimpleTransactionalService simpleTransactionalService;

    @Test
    public void simply() {
        simpleTransactionalService.insertSuccess("foo");
        assertThatThrownBy(() -> simpleTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!");
        assertThatThrownBy(() -> simpleTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Commit?");

        assertThat(simpleTransactionalService.findAll()).containsExactly("foo", "hoge");
    }

結果を見ると、RuntimeExceptionをスローしたメソッドはロールバックされていますが、Exceptionをスローしたメソッドは
ロールバックされていない(コミットされている)ことが確認できます。

実装を確認する

さて、このあたりはどこで定義されているのでしょう?

@Transactionalアノテーションのロールバック対象の例外のデフォルト値は、特になにも指定がないのです。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java#L166-L200

Spring Frameworkのソースコードを追ってみます。

@Transactional…つまり、TransactionInterceptorが適用されたメソッドが実行され、例外がスローされた時にロールバック
するかどうかを確認しているのはこちらのようです。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/TransactionAspectSupport.java#L670

もうちょっと追っていくと、RuleBasedTransactionAttributeクラスにたどり着きます。@Transactionalにロールバック対象の
例外を明示的に設定している場合は、ここで確認するようです。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java#L140-L146

なにも設定していない場合は、親クラスを呼び出します。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/RuleBasedTransactionAttribute.java#L156

親クラスであるDefaultTransactionAttributeクラスのrollbackOnメソッドを見ると、RuntimeExceptionまたはErrorであれば
trueを返すようになっています。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/interceptor/DefaultTransactionAttribute.java#L186-L188

というわけで、デフォルトの挙動は、設定ではなくてデフォルト実装で実現されているということになります。

また、RuleBasedTransactionAttributeクラスのインスタンスを作成しているのは、こちらですね。

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java#L68-L102

@Transactionalアノテーションの内容を使って、インスタンスを設定しているようです。

設定してみる

実装を見たところで、ちょっと設定を変えてみましょう。

以下のように、@TransactionalのrollbackForにExceptionを指定してみます。

src/main/java/org/littlewings/spring/jdbc/WithRollbackForTransactionalService.java

package org.littlewings.spring.jdbc;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class WithRollbackForTransactionalService {
    JdbcTemplate jdbcTemplate;

    public WithRollbackForTransactionalService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @Transactional(readOnly = true)
    public List<String> findAll() {
        return jdbcTemplate.queryForList("select word from sample order by word", String.class);
    }

    @Transactional(rollbackFor = Exception.class)
    public void insertSuccess(String value) {
        jdbcTemplate.update("insert into sample(word) values(?)", value);
    }

    @Transactional(rollbackFor = Exception.class)
    public void insertWithRuntimeException(String value) {
        jdbcTemplate.update("insert into sample(word) values(?)", value);

        throw new RuntimeException("Oops!!");
    }

    @Transactional(rollbackFor = Exception.class)
    public void insertWithException(String value) throws Exception {
        jdbcTemplate.update("insert into sample(word) values(?)", value);

        throw new Exception("Rollback!");
    }
}

テストコードで確認。

    @Autowired
    WithRollbackForTransactionalService withRollbackForTransactionalService;

    @Test
    public void withRollbackFor() {
        withRollbackForTransactionalService.insertSuccess("foo");
        assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!");
        assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!");

        assertThat(withRollbackForTransactionalService.findAll()).containsExactly("foo");
    }

今度は、スローするのがExceptionでもロールバックされるようになりました。

アノテーションを合成する

ところで、先ほどのような設定にすると、個々のメソッドやクラスに@Transactionalの設定をそれぞれ書いていくため
設定ミスなどが気になるところです。

XMLで設定していた時は、ある意味一括で設定できていた感じがします。

Rolling Back a Declarative Transaction

@Transactionalでトランザクション管理する場合は、どうすればよいのでしょう?

アノテーションを合成すれば良さそうです。

Custom Composed Annotations

こちらのドキュメントに従い、@Transactionalをカスタマイズした新しいアノテーションを定義します。

Using Meta-annotations and Composed Annotations

新しいアノテーションでは、@AliasForアノテーションを使って@Transactionalの設定へ反映させます。

AliasFor (Spring Framework 5.3.6 API)

こんな感じのものを作成。

src/main/java/org/littlewings/spring/jdbc/MyTransactional.java

package org.littlewings.spring.jdbc;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;
import org.springframework.transaction.annotation.Transactional;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface MyTransactional {
    @AliasFor(annotation = Transactional.class, attribute = "readOnly")
    boolean readOnly() default false;

    @AliasFor(annotation = Transactional.class, attribute = "rollbackFor")
    Class<? extends Throwable>[] rollbackFor() default Exception.class;
}

@Transactionalをメタアノテーションとして使い、新しいアノテーションを定義します。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Transactional
public @interface MyTransactional {

rollbackForの設定は、デフォルトでExceptionをロールバック対象にしてみました。

    @AliasFor(annotation = Transactional.class, attribute = "rollbackFor")
    Class<? extends Throwable>[] rollbackFor() default Exception.class;

あとは、readOnlyが設定できるようにしています。

    @AliasFor(annotation = Transactional.class, attribute = "readOnly")
    boolean readOnly() default false;

作成したアノテーションを使ったコード。

src/main/java/org/littlewings/spring/jdbc/WithComposedAnnotationTransactionalService.java

package org.littlewings.spring.jdbc;

import java.util.List;

import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;

@Service
public class WithComposedAnnotationTransactionalService {
    JdbcTemplate jdbcTemplate;

    public WithComposedAnnotationTransactionalService(JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    @MyTransactional(readOnly = true)
    public List<String> findAll() {
        return jdbcTemplate.queryForList("select word from sample order by word", String.class);
    }

    @MyTransactional
    public void insertSuccess(String value) {
        jdbcTemplate.update("insert into sample(word) values(?)", value);
    }

    @MyTransactional
    public void insertWithRuntimeException(String value) {
        jdbcTemplate.update("insert into sample(word) values(?)", value);

        throw new RuntimeException("Oops!!");
    }

    @MyTransactional
    public void insertWithException(String value) throws Exception {
        jdbcTemplate.update("insert into sample(word) values(?)", value);

        throw new Exception("Rollback!");
    }
}

こちらには、rollbackForの設定はありません。

確認用のテストコード。

    @Autowired
    WithComposedAnnotationTransactionalService withComposedAnnotationTransactionalService;

    @Test
    public void withComposedAnnotation() {
        withComposedAnnotationTransactionalService.insertSuccess("foo");
        assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!");
        assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!");

        assertThat(withComposedAnnotationTransactionalService.findAll()).containsExactly("foo");
    }

先ほどの、@Transactional+rollbackForでExceptionを指定した時と同じ結果が得られました。

というわけで、設定を共通化したかったらこういう感じにしたらよさそう…というか、@Transactionalアノテーションから
トランザクション管理の設定を読み取るこのあたりのソースコードを見ていると、これしか方法なさそうな感じが?

https://github.com/spring-projects/spring-framework/blob/v5.3.6/spring-tx/src/main/java/org/springframework/transaction/annotation/SpringTransactionAnnotationParser.java#L68-L102

まとめ

今回は、Spring Frameworkの@Transactionalのロールバックに関する設定を見たり、その実装部分を見たり、設定を変えて
確認したりしてみました。

どうなんでしょう?@Transactionalをそのまま使うのならいいのですが、rollbackForあたりを一律カスタマイズしたい場合は
合成アノテーションを使った方が良さそうなような、そうでもないような。

Spring Frameworkを使うなら、トランザクション管理は@Transactionalでしょう、みたいなところありそうですからね。
変に自前のものを持ち込むと混乱しそうな感じも。

ツールやテストで確認できたらいいのでしょうか。

XMLで埋め込んでいた時は、メソッド名のルールなどで一律適用といった感じですからね。ちょっと違った悩み?

ちなみに、XMLで埋め込んでいた内容を、JavaConfigで行う場合はこのあたりを使うことになりそうです。

BeanFactoryTransactionAttributeSourceAdvisor (Spring Framework 5.3.6 API)

MethodMapTransactionAttributeSource (Spring Framework 5.3.6 API)

RuleBasedTransactionAttribute (Spring Framework 5.3.6 API)

最後に、作成したテストコード全体を載せておきましょう。

src/test/java/org/littlewings/spring/jdbc/TransactionalRollbackTest.java

package org.littlewings.spring.jdbc;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

@SpringBootTest
class TransactionalRollbackTest {
    @Autowired
    JdbcTemplate jdbcTemplate;

    @BeforeEach
    public void createTable() {
        jdbcTemplate.execute("drop table if exists sample");

        jdbcTemplate.execute("create table sample(word varchar(10));");
    }

    @Autowired
    SimpleTransactionalService simpleTransactionalService;

    @Test
    public void simply() {
        simpleTransactionalService.insertSuccess("foo");
        assertThatThrownBy(() -> simpleTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!");
        assertThatThrownBy(() -> simpleTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Commit?");

        assertThat(simpleTransactionalService.findAll()).containsExactly("foo", "hoge");
    }

    @Autowired
    WithRollbackForTransactionalService withRollbackForTransactionalService;

    @Test
    public void withRollbackFor() {
        withRollbackForTransactionalService.insertSuccess("foo");
        assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!");
        assertThatThrownBy(() -> withRollbackForTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!");

        assertThat(withRollbackForTransactionalService.findAll()).containsExactly("foo");
    }

    @Autowired
    WithComposedAnnotationTransactionalService withComposedAnnotationTransactionalService;

    @Test
    public void withComposedAnnotation() {
        withComposedAnnotationTransactionalService.insertSuccess("foo");
        assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithRuntimeException("bar")).isInstanceOf(RuntimeException.class).hasMessage("Oops!!");
        assertThatThrownBy(() -> withComposedAnnotationTransactionalService.insertWithException("hoge")).isInstanceOf(Exception.class).hasMessage("Rollback!");

        assertThat(withComposedAnnotationTransactionalService.findAll()).containsExactly("foo");
    }
}

MySQL Server 8.0の設定を、set persist(またはset persist_only)で変更する

これは、なにをしたくて書いたもの?

MySQLでは、(可能なものについては)設定をsetで動的に変更することができます。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.9 システム変数の使用

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.4 サーバーオプション、システム変数およびステータス変数リファレンス

ただ、setで変更した内容はMySQLサーバーを再起動するともとに戻ってしまいます。

MySQL 8.0では、この状況が変わっているようです。

第94回 SET PERSISTを使ってシステム変数を永続化させる:MySQL道普請便り|gihyo.jp … 技術評論社

日々の覚書: MySQL 8.0.0でSET PERSIST構文が出来て、my.cnfへの反映忘れが防げそう

MySQL 8.0 新機能 Persisting configuration variables - Qiita

set persist/set persist_only

MySQL 8.0からset persist(set persist_only)が追加され、設定ファイル(mysqld-auto.cnf)へ保存できるようになったようです。
以降、このことを「永続化」と呼ぶことにします。

MySQL now supports a SET PERSIST variant of SET statement syntax, for making configuration changes at runtime that also persist across server restarts. Like SET GLOBAL, SET PERSIST is permitted for any global system variable that is dynamic (settable at runtime). The statement changes the runtime variable value, but also writes the variable setting to an option file named mysqld-auto.cnf in the data directory. At startup, the server processes this file after all other option files.

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.0 (2016-09-12, Development Milestone)

MySQL now supports a SET PERSIST_ONLY variant of SET statement syntax, for making configuration changes at runtime that also persist across server restarts. Like SET PERSIST, SET PERSIST_ONLY writes the variable setting to an option file named mysqld-auto.cnf in the data directory. However, unlike PERSIST, PERSIST_ONLY does not modify the runtime global system variable value. This makes PERSIST_ONLY suitable for configuring read-only system variables that only be set can at server startup.

MySQL :: MySQL 8.0 Release Notes :: Changes in MySQL 8.0.2 (2017-07-17, Development Milestone)

set persistは動的に変更可能な設定を、設定変更とともに設定ファイル(mysqld-auto.cnf)へ永続化します。

set persist_onlyは、動的に変更できない設定を設定ファイル(mysqld-auto.cnf)へ永続化するもので、次回の再起動時に
反映されるというものです。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.9.3 永続化されるシステム変数

MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.7.6.1 変数代入の SET 構文

対象のスコープは、グローバルなものとなります。

動的に変更できるかどうかは、このあたりを見ると確認できるでしょう。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.4 サーバーオプション、システム変数およびステータス変数リファレンス

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.5 サーバーシステム変数リファレンス

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.9.2 動的システム変数

set persistで永続化した内容を削除するには、reset persistを使用します。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 13.7.8.7 RESET PERSIST ステートメント

また、永続性のない設定もあるようです。log_bin、hostnameなど。たくさんあるので、以下を参照してください。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.9.4 永続的で永続的に制限されないシステム変数

同じページに、読み取り専用であっても永続化できないものも書かれています。

権限

set persist、set persist_only、reset persistの実行に必要な権限を、それぞれ調べてみます。

こちらに書かれていますね。

MySQL :: MySQL 8.0 リファレンスマニュアル :: 5.1.9.1 システム変数権限

  • set persist
    • SYSTEM_VARIABLES_ADMINまたはSUPER
  • set persist_only
    • SYSTEM_VARIABLES_ADMINおよびPERSIST_RO_VARIABLES_ADMIN
  • reset persist
    • 動的に変更できる設定の場合 … SYSTEM_VARIABLES_ADMINまたはSUPER
    • 読み取り専用の設定の場合 … SYSTEM_VARIABLES_ADMINおよびPERSIST_RO_VARIABLES_ADMIN

永続化の機能を無効にする

永続化した設定ファイルを読み込むかどうかは、persisted_globals_loadで制御できるようです。

persisted_globals_load

あと、重要そうなことがさらっと書かれていますね。

永続化された構成設定をデータディレクトリの mysqld-auto.cnf ファイルからロードするかどうか。 サーバーは通常、このファイルを起動時にほかのすべてのオプションファイルのあとに処理します (セクション4.2.2.2「オプションファイルの使用」 を参照)。

ということは、永続化した設定の優先度が高そうですね?あとで確認してみましょう。

では、情報を見るのはこれくらいにして実際に試してみましょう。

環境

今回の環境は、こちらです。

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.24    |
+-----------+
1 row in set (0.00 sec)

データディレクトリは、/var/lib/mysqlとなっています。

また、操作はrootアカウントで行っているものとします。

set persistを試す

まずは、set persistから試してみましょう。

innodb_buffer_pool_sizeを使いましょう。現在値を確認。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
1 row in set (0.01 sec)

デフォルトの128MBです。

これを、1GBに変更してみましょう。最初は、set globalで永続化なしで変更します。

mysql> set global innodb_buffer_pool_size = 1073741824;
Query OK, 0 rows affected (0.00 sec)

変更されました。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 1073741824 |
+-------------------------+------------+
1 row in set (0.00 sec)

ここで、MySQLサーバーを再起動してもう1度確認してみます。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+-----------+
| Variable_name           | Value     |
+-------------------------+-----------+
| innodb_buffer_pool_size | 134217728 |
+-------------------------+-----------+
1 row in set (0.00 sec)

元に戻りました。では、set persistで変更してみましょう。

mysql> set persist innodb_buffer_pool_size = 1073741824;
Query OK, 0 rows affected (0.00 sec)

今度は、MySQLサーバーを再起動しても元に戻りません。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 1073741824 |
+-------------------------+------------+
1 row in set (0.00 sec)

もうひとつ、max_connectionsも変更してみましょう。

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 151   |
+-----------------+-------+
1 row in set (0.01 sec)

mysql> set persist max_connections = 300;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 300   |
+-----------------+-------+
1 row in set (0.00 sec)

データディレクトリ内には、mysqld-auto.cnfというファイルができ、ここまでset persistで永続化した内容がJSON形式で
保存されています。

/var/lib/mysql/mysqld-auto.cnf

{ "Version" : 1 , "mysql_server" : { "max_connections" : { "Value" : "300" , "Metadata" : { "Timestamp" : 1619882895525080 , "User" : "root" , "Host" : "host01" } } , "mysql_server_static_options" : { "innodb_buffer_pool_size" : { "Value" : "1073741824" , "Metadata" : { "Timestamp" : 1619882888860561 , "User" : "root" , "Host" : "host01" } } } } }

続いて、動的に設定できない項目にも試してみましょう。skip_name_resolveでやってみます。

mysql> set persist skip_name_resolve = on;
ERROR 1238 (HY000): Variable 'skip_name_resolve' is a read only variable

失敗しますね。

set persist_only

というわけで、今度はset persist_onlyを使います。

まずは、先ほど変更できなかったskip_name_resolveの値を確認。

mysql> show variables like 'skip_name_resolve';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| skip_name_resolve | OFF   |
+-------------------+-------+
1 row in set (0.00 sec)

set persist_onlyで変更してみます。

mysql> set persist_only skip_name_resolve = on;
Query OK, 0 rows affected (0.00 sec)

今度は、OKになりました。

ですが、現在の値そのものは変わっていません。

mysql> show variables like 'skip_name_resolve';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| skip_name_resolve | OFF   |
+-------------------+-------+
1 row in set (0.00 sec)

1度MySQLを再起動すると、反映されます。

mysql> show variables like 'skip_name_resolve';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| skip_name_resolve | ON    |
+-------------------+-------+
1 row in set (0.00 sec)

mysqld-auto.cnfにも、値が反映されましたね。

/var/lib/mysql/mysqld-auto.cnf

{ "Version" : 1 , "mysql_server" : { "max_connections" : { "Value" : "300" , "Metadata" : { "Timestamp" : 1619882895525080 , "User" : "root" , "Host" : "host01" } } , "mysql_server_static_options" : { "innodb_buffer_pool_size" : { "Value" : "1073741824" , "Metadata" : { "Timestamp" : 1619882888860561 , "User" : "root" , "Host" : "host01" } } , "skip_name_resolve" : { "Value" : "ON" , "Metadata" : { "Timestamp" : 1619882940640701 , "User" : "root" , "Host" : "host01" } } } } }

reset persist

では、設定した内容を削除してみましょう。reset persistを使います。

reset persistでは、set persistで永続化したもの、set persist_onlyで永続化したものを問わず削除することができます。

先ほど設定したmax_connectionsを

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 300   |
+-----------------+-------+
1 row in set (0.01 sec)

リセットしてみます。

mysql> reset persist max_connections;
Query OK, 0 rows affected (0.00 sec)

成功しました。

ただ、現在の値は変わっていません。

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 300   |
+-----------------+-------+
1 row in set (0.00 sec)

mysqld-auto.cnfからは、値がなくなりましたね。

/var/lib/mysql/mysqld-auto.cnf

{ "Version" : 1 , "mysql_server" : { "mysql_server_static_options" : { "innodb_buffer_pool_size" : { "Value" : "1073741824" , "Metadata" : { "Timestamp" : 1619882888860561 , "User" : "root" , "Host" : "host01" } } , "skip_name_resolve" : { "Value" : "ON" , "Metadata" : { "Timestamp" : 1619882940640701 , "User" : "root" , "Host" : "host01" } } } } }

なので、MySQLサーバーを再起動すると反映されることになります。

mysql> show variables like 'max_connections';
+-----------------+-------+
| Variable_name   | Value |
+-----------------+-------+
| max_connections | 151   |
+-----------------+-------+
1 row in set (0.00 sec)

もしくはreset persistしたうえで、set globalでデフォルト値に戻すなどですね。動的に変更できるものに限定されますが。

reset persistは、設定項目名を指定しない場合は、永続化した項目すべてを削除します。

mysql> reset persist;
Query OK, 0 rows affected (0.00 sec)

`mysqld-auto.cnf``から設定値がなくなりました。

/var/lib/mysql/mysqld-auto.cnf

{ "Version" : 1 , "mysql_server" : {  } }

もちろん、単一の設定項目を指定した場合と動きは変わらないので、設定を反映したければ再起動なりset globalするなり
行う必要があります。

使い方は、だいたいわかった感じですね。

設定ファイルと永続化の両方を使った場合は?

なんとなく結果はわかっている気がしますが、気になるところではあるので。
設定ファイルと永続化の両方で設定をした場合にどうなるかを確認してみます。

たとえば、MySQLの設定ファイルでinnodb_buffer_pool_sizeを1Gに設定してみます。

[mysqld]
...

innodb_buffer_pool_size = 2G

MySQLサーバーを再起動して反映された、現在の値です。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 2147483648 |
+-------------------------+------------+
1 row in set (0.00 sec)

ここで、set persistで1Gに変更してみます。

mysql> set persist innodb_buffer_pool_size = 1073741824;
Query OK, 0 rows affected (0.00 sec)

変更後の値。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 1073741824 |
+-------------------------+------------+
1 row in set (0.01 sec)

mysqld-auto.cnfにも反映されました。

/var/lib/mysql/mysqld-auto.cnf

{ "Version" : 1 , "mysql_server" : { "mysql_server_static_options" : { "innodb_buffer_pool_size" : { "Value" : "1073741824" , "Metadata" : { "Timestamp" : 1619884781853180 , "User" : "root" , "Host" : "host01" } } } } }

ここで、MySQLサーバーを再起動してみます。

結果は、こうなりました。

mysql> show variables like 'innodb_buffer_pool_size';
+-------------------------+------------+
| Variable_name           | Value      |
+-------------------------+------------+
| innodb_buffer_pool_size | 1073741824 |
+-------------------------+------------+
1 row in set (0.00 sec)

set persistの内容になっています。

これは、persisted_globals_loadの説明にもありましたが、mysqld-auto.cnfは他の設定ファイルを読み込んだ後に
読むことになるからですね。

persisted_globals_load

ファイルにまとめて変更する

こういう感じでset persistまたはset persist_onlyで変更できるのなら、ファイルに保存してから実行する方が、管理面などで
便利かもですね。特に、設定ファイルが直接触れない環境の場合など。

set_persit_variables.sql

set persist innodb_buffer_pool_size = 1073741824;

set persist max_connections = 300;

set persist_only max_allowed_packet = 134217728;

こんな感じで実行。

$ mysql -uroot -p < set_persit_variables.sql

まとめ

MySQL 8.0から使えるようになった、set persist、set persist_only、そしてreset persistを試してみました。

setで値を変更するだけではなくて、永続化できるようになって便利ですね。これは覚えておきましょう。