しおしお

IntelliJ IDEAのことなんかを書いてます

PostgreSQLでREAD COMMITEDとdelete->insertの組み合わせの問題のメモ

PostgreSQLのREAD COMMITEDでdelete->insertを使った場合、あるはずのレコードに対する削除がされずにinsertで一意制約違反が発生するらしい。。。

テーブルの状態

postgres=> select * from hoge;
 id
----
  1

2つのトランザクションで以下のSQLを実行

delete from hoge where id = 1;
insert into hoge values (1);

結果

先に実行されたトランザクションの処理は当然成功する。

postgres=> begin
postgres-> ;
BEGIN
postgres=> delete from hoge where id = 1;
DELETE 1
postgres=> insert into hoge values (1);
INSERT 0 1
postgres=> commit;
COMMIT

後に実行されたトランザクションは、deleteがロックを解除されるのを待ち、delete -> insertが成功されると思っていたが異なる結果となった。
結果は以下のログのようにdelete処理では何も削除されずに、insertで一意制約違反となる。

postgres=> begin;
BEGIN
postgres=> delete from hoge where id = 1;
DELETE 0
postgres=> insert into hoge values (1);
ERROR:  重複キーが一意性制約"hoge_pkey"に違反しています
DETAIL:  キー (id)=(1) はすでに存在します

原因

以下に詳しく書いてありますが、delete対象のレコードに対するロックの取得待ちをしていて、そのレコードがなくなったためにこのような挙動になるようです。
https://dba.stackexchange.com/questions/27688/locking-issue-with-concurrent-delete-insert-in-postgresql

READ COMMITEDでdelete->insertは選択しちゃダメなのですね。

DomaでOracle12CのIdentity Columnを使ってみた

DomaでOracle12CのIdentity Columnを使う方法です。

DomaのOracleDialectなどなどでは、使うことが出来ないので色々いじってあげる必要があります。

Oracle12C用のDialectを作る

基本は、OracleDialectと同じでいいので継承して作ります。
変更点は、Identity Columnを使えるようにするためにsupportsIdentityでtrueを返します。
また、Statement.getGeneratedKeys()でデータベース側で採番した値を取得したいので、supportsAutoGeneratedKeysでもtrueを返します。

object Oracle12Dialect : OracleDialect() {

    override fun supportsIdentity() = true

    override fun supportsAutoGeneratedKeys() = true
}

Configクラスを作る

getDialectでは、先程作ったOracle12C用のDialect実装を返します。
getCommandImplementorsは、insert時のprepareStatementでprepareStatement(String sql, int[] columnIndexes)を使用するように変更します。
Domaの実装だとIdentity Columnの採番された値ではなく、ROWIDの値がStatement.getGeneratedKeys()で返されてしまうので変更しています。

あとは、接続先の設定などなどをしてあげます。

object AppConfig : Config {

    private val dialect: Dialect = Oracle12Dialect

    override fun getCommandImplementors(): CommandImplementors {
        return object : CommandImplementors {
            override fun createInsertCommand(method: Method, query: InsertQuery): InsertCommand {
                return object : InsertCommand(query) {
                    override fun prepareStatement(connection: Connection): PreparedStatement {
                        return if (query.isAutoGeneratedKeysSupported) {
                            connection.prepareStatement(sql.rawSql, intArrayOf(1))
                        } else {
                            super.prepareStatement(connection)
                        }
                    }
                }
            }
        }
    }

    private val dataSource by lazy {
        val oracleDataSource = OracleDataSource()
        oracleDataSource.user = "siosio"
        oracleDataSource.setPassword("siosio")
        oracleDataSource.url = "...."

        LocalTransactionDataSource(oracleDataSource)
    }

    override fun getTransactionManager(): TransactionManager {
        return LocalTransactionManager(dataSource.getLocalTransaction(jdbcLogger))
    }

    override fun getDataSource(): DataSource = dataSource

    override fun getDialect(): Dialect = dialect
}

Entityを作る

主キーのGeneratedValueをGenerationType.IDENTITYなカラムにしてあげます。

@Entity(immutable = true)
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    public final Long id;

    public final String name;

    public User(final Long id, final String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                '}';
    }
}

Daoを作る

@Dao(config = AppConfig.class)
public interface UserDao {

    @Insert
    Result<User> insert(User user);
}

テーブルを作る

idカラムをidentityなカラムにしてあげます。

create table users(
  id number(15) generated always as identity ,
  name varchar2(150 char),
  primary key (id)
)

検証用のmainを作る

DaoのImplをルックアップしてきて、insertを呼び出している感じです。

fun main(args: Array<String>) {

    AppConfig.transactionManager.required {
        val user = User(null, "しおしお")
        val result = dao<UserDao>().insert(user)
        println("result.entity = ${result.entity}")
    }
}

実行!!

登録した結果、Entityの内容がUser{id=9, name='しおしお'}となっているのでいい感じに動きました。

4 18, 2017 8:47:12 午前 org.seasar.doma.jdbc.tx.LocalTransaction begin
情報: [DOMA2063] ローカルトランザクション[142666848]を開始しました。
4 18, 2017 8:47:12 午前 dao.UserDaoImpl insert
情報: [DOMA2220] ENTER  : クラス=[dao.UserDaoImpl], メソッド=[insert]
4 18, 2017 8:47:13 午前 dao.UserDaoImpl insert
情報: [DOMA2076] SQLログ : SQLファイル=[null],
insert into users (name) values ('しおしお')
4 18, 2017 8:47:13 午前 dao.UserDaoImpl insert
情報: [DOMA2221] EXIT   : クラス=[dao.UserDaoImpl], メソッド=[insert]
4 18, 2017 8:47:13 午前 org.seasar.doma.jdbc.tx.LocalTransaction commit
情報: [DOMA2067] ローカルトランザクション[142666848]をコミットしました。
result.entity = User{id=9, name='しおしお'}
4 18, 2017 8:47:13 午前 org.seasar.doma.jdbc.tx.LocalTransaction commit
情報: [DOMA2064] ローカルトランザクション[142666848]を終了しました。

テーブルをのぞいてみるとちゃんと登録されてます。

08:48:29 SQL> select * from users where id = 9;

        ID NAME
---------- ------------------------------
         9 しおしお

おわり。

JBeretでジョブ定義ファイルの置き場所を変えてみる

JBeretでは、org.jberet.spi.JobXmlResolverの実装クラスを作ることで、ジョブ定義のXMLファイルの置き場所を変更できる。

JobXmlResolverの実装クラスを作る

JobXmlResolverJavadocによると、resolveJobXml以外は任意となっているので、
任意メソッドの空実装を持っているAbstractJobXmlResolverを実装するとよい。

実装としては、こんな感じになる。この実装では、環境変数から取得したディレクトリの中からジョブ定義を探して返しています。
ファイルがない場合にnullを返しているので、この場合はデフォルトの実装(META-INF/batch-jobs配下から探すやつ)が動きます。

import org.jberet.tools.*
import java.io.*
import java.nio.file.*

class FileSystemBasedJobXmlResolver : AbstractJobXmlResolver() {

  override fun resolveJobXml(jobXml: String, classLoader: ClassLoader): InputStream? {
    val dir = System.getenv("jobxml-dir")
    val jobXmlFilePath = Paths.get(dir, jobXml)
    println("jobXmlFilePath = ${jobXmlFilePath}")
    return if (Files.isReadable(jobXmlFilePath)) {
      FileInputStream(jobXmlFilePath.toFile())
    } else {した
      null
    }
  }
}

プロバイダ構成ファイルを作る

ファイル名は、org.jberet.spi.JobXmlResolverで、resources/META-INF/servicesの下においてあげます。
ファイルの中には、JobXmlResolverを実装したクラスのクラス名を設定します。

動かしてみる

動作確認するためのBatchlet

ステップ名を出力するだけの簡単な実装です。

@Named
@Dependent
class TestBatchlet @Inject constructor(
    private val stepContext: StepContext
) : AbstractBatchlet() {

  override fun process(): String {
    println("stepContext.stepName = ${stepContext.stepName}")
    return "ok"
  }
}
ジョブ定義のXML

確認用のBatchletを実行するステップを定義する

<job id="test" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
  <step id="myStep">
    <batchlet ref="testBatchlet" />
  </step>
</job>
実行してみる

IntelliJさんで、こんな感じに実行構成を定義します。環境変数には、ジョブ定義フィルをおいてあるディレクトリを指定する。

実行結果はこんな感じになります。
自分で作ったクラスが呼び出されているのがわかります。

jobXmlFilePath = /home/siosio/test.xml
stepContext.stepName = myStep

JBeretのカスタムなCDIスコープを試す

JBeret1.2から追加されたカスタムCDIスコープを試してみた。

JBeretのバージョン

compile 'org.jberet:jberet-core:1.2.2.Final'

StepScope

1つのStepがスコープとなるので、同一ステップ内であれば同じBeanのインスタンスが使用される。
Stepが変わるとBeanのインスタンスが新しく生成される。

StepScopeのBean

StepScopeにするために、org.jberet.cdi.StepScopedアノテーションを設定する。

@StepScoped
@Named
open class SampleBean {

  open var count: Int = 0;

}
Beanを使うBatchlet

StepScopeなBeanをインジェクションし、Beanの内容の出力と値をインクリメントする。

@Named
@Dependent
class SampleBatchlet @Inject constructor(
    private val sampleBean: SampleBean
) : AbstractBatchlet() {

  override fun process(): String {

    println("batchlet: ${sampleBean.count}")
    sampleBean.count++

    return "success"
  }
}
Beanを使うStepListener

StepScopeなBeanをインジェクションし、ステップの実行前に値のインクリメントを行いステップの実行後に値の出力を行う。

@Dependent
@Named
open class SampleStepListener @Inject constructor(
    private val stepContext: StepContext,
    private val sampleBean: SampleBean
) : AbstractStepListener() {

  override fun beforeStep() {
    println("---------- ${stepContext.stepName} ----------")
    sampleBean.count++
  }

  override fun afterStep() {
    println("after step = ${sampleBean.count}")
  }
}
Job定義

StepごとにBeanが新しくなっていることを確認するために、同じBatchletを繰り返し実行する。

  <step id="myStep" next="myStep2">
    <listeners>
      <listener ref="sampleStepListener" />
    </listeners>
    <batchlet ref="sampleBatchlet">
    </batchlet>
  </step>

  <step id="myStep2" next="myStep3">
    <listeners>
      <listener ref="sampleStepListener" />
    </listeners>
    <batchlet ref="sampleBatchlet">
    </batchlet>
  </step>
  
  <step id="myStep3">
    <listeners>
      <listener ref="sampleStepListener" />
    </listeners>
    <batchlet ref="sampleBatchlet">
    </batchlet>
  </step>
実行結果

Stepが変わるごとにBeanが新しくなって、値が初期化されている。
同一ステップ内のListenerとBatchletでは同じBeanが使用されていることがわかる。

---------- myStep ----------
batchlet: 1
after step = 2
---------- myStep2 ----------
batchlet: 1
after step = 2
---------- myStep3 ----------
batchlet: 1
after step = 2

StepScope以外・・・

Step以外には、JOBやPartitionスコープがある。
詳細はこちら→Custom CDI Scopes | JBeret User Guide

DomaをIntelliJ&Gradleの組み合わせで使った場合にIntelliJでビルドできるようにする

DomaIntelliJ&Gradleで使ったば場合にIntelliJ側でもビルドできるようにする手順です。

バージョンなど

Gradle
task wrapper(type: Wrapper) {
  gradleVersion = '3.3'
}
IntelliJ IDEA

下のバージョンで確認をしました。

  • 2016.3.2
  • 2017.1(EAP)
プロジェク構成

下のプロジェクト構成で確認を行いました

  • シングルプロジェクト
  • マルチプロジェクト(rootプロジェクトのsubprojects内に設定を行っています)

Domaのドキュメントを参考にbuild.gradleを作る

ビルド — Doma 2.0 ドキュメントを参考にbuild.gradleを作りましょう。
これで、Gradleでビルドした場合にSQLファイルをうまく参照できるようになります。

IntelliJでビルドできるようにするための設定を追加する

build.gradleに以下の設定を追加します

// ideaプラグインを追加
apply plugin: 'idea'

// モジュールの出力先ディレクトリをcompileJava.destinationDirに変更
idea.module.outputDir = compileJava.destinationDir

IntelliJでビルドしてみると

SQLファイルの出力先がresourcesからclassesになりちゃんとビルドできるようになりました。

ちなみに、上の設定を行わずにビルドした場合はこんな感じに出力されてビルドが失敗します。

inheritOutputDirsを有効しないと動かない場合がある

inheritOutputDirsを有効(true)にしないと、うまく動かないとの指摘をいただきました。
上に書いたバージョンの組み合わせでは、この設定がなくても動いたのですがバージョンの組み合わせによってはこの設定が必要なので、以下の設定をbuild.gradleに追加してあげましょう。

  idea.module.inheritOutputDirs = true



おわり。

JenkinsでIntelliJ IDEAのinspectionを実行して結果をいい感じに表示させてみる

IntelliJさんのinsupectionはCI環境でも実行できるので、Jenkinsで実行&いい感じに結果を表示する方法を調べてみた。 *1

Jenkinsに必要なプラグイン

Warnings Plugin - Jenkins - Jenkins Wiki
IntelliJさんのinspectionの結果をいい感じに集計&表示するために必要になります。

ビルドにinspectionの実行を定義する

inspectionの実行と、inspection実行結果のファイル内のパス置き換えを行ってあげます。
実行結果ファイルのパス書き換えをしないと、指摘箇所のソースへのジャンプ時にエラーになってしまいます。

/var/jenkins_home/idea-IC-163.9166.29/bin/inspect.sh . /var/jenkins_home/my_inspection.xml ./report/inspection -d src/main/java
sed -i 's/file:\/\/\$PROJECT_DIR\$\///' ./report/inspection/*.xml

設定のイメージ的にはこんな感じです。

IntelliJプラグインをCIサーバに配置する

サードパーティ製のプラグインのInspection機能をCIサーバで実行したい場合には、プラグインをCIサーバ上にインストールする必要があります。
インストール先は、$idea.config.path/pluginsとなります。

ビルド後の手順にinspection結果の集計を追加する

下の画層のようにWarnings Pluginを使って結果の集計を行います。
集計するファイルにはinspection実行結果のファイルを指定して、パーサにはIntelliJ IDEA Inspectionsを選択します。

ジョブの実行結果

ジョブを実行するとChecstyleプラグインと同じ感じに結果が見れます。

ジョブの結果画面


指摘の一覧

指摘箇所のソースコード


ハマりポイント

CI環境にIntelliJさんのjdk設定が必要

具体的には、$idea.config.path/options/jdk.table.xmlが必要となります。
この設定がないと、下のようなメッセージがでてinspectionの実行がエラーとなってしまいます。
(他のパターンもあって、下と同じようにJDKの設定がないよ的なエラーがでます)

Please, specify sdk 'null' for module 'inspection_test_pj_main'

デスクトップ環境があれば、IntelliJを起動して、JDKの設定をしてあげるだけでOKです。
もし、デスクトップ環境がない場合には、別の環境で作ったファイルをCIの該当ディレクトリに移動する必要があります。

Warnnings pluginがエラーをはいておわってしまう

こんな感じのエラーをはいて異常終了してしまうことがあります。
これは、inspectionの結果ファイルに行数を示す要素がない場合に発生します。
例えば、パッケージ名やモジュールに対するinspection結果にはこの要素がないためエラーとなってしまいます。

エラーとなった結果ファイルは、例えばgrep -L line report/inspection/*.xmlで検索できます。
これらの結果を除外したい場合は、inspect.sh(bat)実行後に、grep -L line report/inspection/*.xml | xargs rm -rfを実行して削除してあげれば良いと思います。

ERROR: Build step failed with exception
java.lang.NumberFormatException: For input string: "-"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:572)
	at java.lang.Integer.parseInt(Integer.java:615)
	at hudson.plugins.warnings.parser.IdeaInspectionParser.parseProblems(IdeaInspectionParser.java:69)

おわり。

JBeretでスクリプト言語を使用してバッチアーチファクトを作ってみる

JBeretのユーザーズガイド見てたら、「Develop Batch Artifacts in Script Languages」なる章*1があったので試してみた。

JBeretのバージョンは1.3系にする

Gradleだとこんな感じです

  compile 'org.jberet:jberet-se:1.3.0.Beta3'

使用するスクリプト言語のライブラリをdependencyに追加する

Groovyだとこんな感じになります

  compile 'org.codehaus.groovy:groovy-jsr223:2.4.7'
  compile 'org.codehaus.groovy:groovy:2.4.7'

ジョブ定義をしてみる

簡単そうなBatchletで試してみました。
このれいでは、batchletタグ内にscriptタグでBatchletの実装を定義しています。
stepContextやjobContextの参照もできます。

<job id="script-sample" xmlns="http://xmlns.jcp.org/xml/ns/javaee" version="1.0">
  <step id="myStep">
    <batchlet>
      <script type="groovy">
        <![CDATA[
          println("Groovy Script!")
          println("ステップ名:${stepContext.stepName}")
        ]]>
      </script>
    </batchlet>
  </step>
</job>

実行結果

ちゃんと動いた!!!

[main] INFO org.jboss.weld.Version - WELD-000900: 2.4.1 (Final)
[main] INFO org.jboss.weld.Bootstrap - WELD-000101: Transactional services not available. Injection of @Inject UserTransaction not available. Transactional observers will be invoked synchronously.
[main] INFO org.jboss.weld.Bootstrap - WELD-ENV-002003: Weld SE container STATIC_INSTANCE initialized
Groovy Script!
ステップ名:myStep
[Thread-1] INFO org.jboss.weld.Bootstrap - WELD-ENV-002001: Weld SE container STATIC_INSTANCE shut down
Weld SE container STATIC_INSTANCE shut down by shutdown hook

使うシーンがイメージできないけど。。。