JPQLで複数の項目をもつValueObjectを条件にクエリする

よく忘れるのでメモ。

ValueObject(条件クラス)

public class FooCondition {

    private final String col1;
    private final String col2;

    public FooCondition(String col1, String col2) {
        this.col1 = col1;
        this.col2 = col2;
    }

    // getterは省略
}

クエリ

public interface FooRepository extends JpaRepository<Foo, FooPK> {

   @Query("SELECT x FROM Foo x "
            + "WHERE x.col1 = :#{#cond.col1} "
            + "AND x.col2 = :#{#cond.col2}")
    List<Foo> findByCondition(@Param("cond") FooCondition condition);

}

JOOQ使い始め(gradle-jooq-pluginによるコード生成)

JPAが辛くなってきたので、JOOQへの乗り換えを検討中。

Why JOOQ?

MyBatisの方が手堅い気がしますが、JOOQを選んだ理由としては、以下のものです。

  • タイプセーフ
  • Annotation Processingではなく、プラグインによるコード生成
    • 今後のJavaのリリースサイクルを考えると、こちらの方が安心…?
      • JOOQはJOOQで1文字アンダースコア識別子( _ )のIssueが上がっていたりするけど…。
  • Spring Boot Starterでサポートされていたり、開発も活発なので、発展性がある&しばらくはなくならない気がする

本記事では、「JOOQって何?」という説明は割愛して、新規プロジェクトでも既存のプロジェクトでも利用できるようなポイントをメモしておきます。

前提条件

DBのスキーマはFlywayで管理されているものとします。ただし、JOOQとFlywayは直接関係はないので、他のマイグレーションツールでも多分やり方は同じはずです。

  • Java 11
  • Gradle 5.1
    • Java 11に対応しているのが5.0からであるため、5.0以上が必須です。
  • Spring Boot 2.1.1.RELEASE
  • JOOQ: 3.11.7
    • Spring Boot の依存関係管理で解決されるため、明示的に指定はしていません。

手順

build.gradle

buildscript {
    ext {
        springBootVersion = '2.1.1.RELEASE'
    }
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        classpath('org.glassfish.jaxb:jaxb-runtime:2.3.1') // (1)
    }
}

plugins {
    id 'org.flywaydb.flyway' version '5.2.4'
    id 'nu.studer.jooq' version '3.0.2'           // (2)
}

apply plugin: 'java'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = 11

repositories {
    mavenCentral()
}

dependencies {
    // 略
    implementation('org.springframework.boot:spring-boot-starter-jooq') // (3)
    // 略
    jooqRuntime('org.glassfish.jaxb:jaxb-runtime') // (1)
    jooqRuntime('javax.activation:javax.activation-api') // (1)
}

flyway {
    url = 'jdbc:mysql://127.0.0.1:3306/testdb'
    user = 'dbuser'
    password = 'dbpass'
}

jooq {  // (4)
    main(sourceSets.main) {
        jdbc {
            url = 'jdbc:mysql://127.0.0.1:3306/testdb'
            user = 'dbuser'
            password = 'dbpass'
        }
        generator {
            name = 'org.jooq.codegen.DefaultGenerator'
            database {
                name = 'org.jooq.meta.mysql.MySQLDatabase'
                includes = '.*'
                excludes = 'flyway_schema_history' // (5)
                inputSchema = 'testdb'
            }
            strategy {
                name = 'org.jooq.codegen.DefaultGeneratorStrategy'
            }
            generate {
            }
            target { // (6)
                packageName = 'com.example.demo.jooq'
                directory = 'src/main/java'
            }
        }
    }
}

project.tasks.getByName('compileJava').dependsOn -= 'generateMainJooqSchemaSource' // (7)
説明
(1) Java 11からはJAXBが外されてしまうため、必要な依存関係を明示的に追加します。
(2) Gradleプラグインとしてgradle-jooq-pluginを適用します。
JOOQのコード生成を行う方法はいくつかありますが、Gradleタスクとして実行できると簡単かつ便利なので、これを使います。
(3) アプリケーションからJOOQを使うための依存を追加。
(4) JOOQコード生成のための設定を記述します。jooq-codegenXMLでの設定の替わりになるものです。
(5) Flywayがスキーマ管理を行うために作成するテーブルは、アプリケーションから使うことはないので、コード生成から除外します。
(6) 自動生成されるコードの出力先です。
(7) デフォルトでは、compileJavaタスクが実行される前にコード生成も実行されるのですが、これを除外します。
こちらは有効化するかどうかはお好みで。

タスク実行

$ ./gradlew generateMainJooqSchemaSource

開発の進め方

  1. Flywayでスキーマ変更を実施
    1. DDL(SQL)を作る
    2. ./gradlew flywayMigrate を実行
  2. generateMainJooqSchemaSource タスクを実行し、コミット

上記の1.と2.で一つのfeature(プルリクエスト)になっているのが分かりやすく、トラブルが起きにくい気がします。

また、自動生成されたコードは、直接編集しないよう、開発チームで徹底しておく必要がありそうです。手動で変更してしまうと次にコード生成された時に上書きされてしまうので。

(追記)自動生成されたコードをコミットするべきか?

並行開発の妨げになるので「自動生成されたコードはコミット対象外!」という主張もあり、なるほどと思いました。

ツール比較しながら語る O/RマッパーとDBマイグレーションの実際のところ

プロジェクトの規模が大きく、スキーマ変更が頻繁に発生する場合は、提言されているとおり、コミット対象外とした方がよいのかもしれません。

Lightning ComponentでLightning ContainerなしでD3.jsを使う #salesforce

凝ったチャートを作りたい場合、D3.jsを使いたいことがあります。

Lightning ComponentでD3.jsを使う場合、v3以下だと以下のようなエラーが起き、表示できません。

[Cannot read property 'document' of undefined] eval()@https://xxxxxx-dev-ed.lightning.force.com/resource/1537117226000/d3lib/d3lib/d3.v3.min.js:3:4187 Proxy.eval()@https://xxxxxx-dev-ed.lightning.force.com/resource/1537117226000/d3lib/d3lib/d3.v3.min.js:5:23548

Lightning Containerを使えば回避できそうですが、d3 v4を使えば、Lightning Containerなしでも表示できました(v5は未確認)。

サンプル

f:id:jappy:20180917154203p:plain

ChromeとIE11での動作を確認しています。

Component

D3のライブラリは静的リソースから読み込みます。バージョンは4.13.0です。

<aura:component implements="flexipage:availableForAllPageTypes">
    <ltng:require scripts="{!join(',',
                  $Resource.d3lib + '/d3lib/d3.min.js')}"
                  afterScriptsLoaded="{!c.initD3Charts}" />
    
    <lightning:card>
        <div id="chart"></div>
    </lightning:card>
</aura:component>

Controller

({
    initD3Charts : function(component, event, helper) {
        helper.renderGraph('#chart');
    }
})

Helper

({    
    renderGraph: function(selector) {
        // margin convention practice
        var margin = { top: 20, right: 20, bottom: 30, left: 50 };
        var width = 960 - margin.left - margin.right;
        var height = 400 - margin.top - margin.bottom;
        
        var num = 100;
        var yMax = 1000;
        
        var xScale = d3.scaleTime()
            .range([0, width])
            .domain([0, num-1]);
        
        var yScale = d3.scaleLinear()
            .range([height, 0])
            .domain([0, yMax]);
        
        // line generator
        var valueLine = d3.line()
            .x(function(d, i) { return xScale(i); })
            .y(function(d) { return yScale(d.value); });
        
        // create dataset
        var dataset = d3.range(num).map(function(d) { return {'value': d3.randomUniform(yMax)()}; });
        
        // add svg tag
        var svg = d3.select(selector).append('svg')
            .attr('width', width + margin.left + margin.right)
            .attr('height', height + margin.top + margin.bottom)
            .append('g')
            .attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
        
        // X axis
        svg.append('g')
            .attr('transform', 'translate(0,' + height + ')')
            .call(d3.axisBottom(xScale));
        
        // Y axis
        svg.append('g')
            .call(d3.axisLeft(yScale));
        
        svg.append('path')
            .datum(dataset)
            .attr('class', 'line')
            .attr('d', valueLine);
    }
})

「アセットファイル」メタデータ #salesforce

あんまり馴染みがなかったのですが、Salesforceには「アセットファイル」というメタデータがあるんですね。

Salesforce Developers

  • 上記に書かれている通り、サフィックス.asset で、contentassets フォルダに保存されます。
  • package.xml においてはワイルドカードの利用も可。
  • 使い所
    • Lightningアプリケーションのブランド設定で、独自のロゴ画像を利用している場合の保管場所
    • 認証されていないユーザにファイルを共有する
    • (他には…?)
  • 権限まわりについては「アセットファイル」設定ではなく、「ファイル」タブの共有の設定で行う

画像などのリソースは静的リソースで管理するものだと思ってました。ファイルにまつわる設定としては、他にも、Files Connectなるものがあったり、まだまだ知らない機能があるんだなぁと思ったのでメモ。

最強のLightningページレイアウト #salesforce

(タイトルはネタですが)

テラスカイさんのブログ「Lightningページレイアウトについて考えてみる - TerraSkyBase」を読んで、以前から考えていたアイディアを試してみました。

それは、「組み込みで用意されているページレイアウトから選択する」ではなく、「どのパターンにも対応可能なレイアウトを用意すれば、迷うことがなくなるんじゃないか?」というものです。

きっかけ

Lightningページの困ることの一つは、一度作ったレイアウト(テンプレート)は、後から変更できないという制限(仕様)です。
なので、「いったん1カラムで作ってから、後で2カラムに変えたい」みたいなことができません。
設定画面(GUI)からではなく、メタデータ(XML)経由でなら変えられるんじゃないか?と思って試したりもしましたが、やっぱりNGでした。

通常の開発・運用であれば、新たに作り直せばいいのかもしれませんが、管理パッケージで配布するような場合、不用意にヘタなレイアウトで作ったLightningページをリリースしてしまうと、後からの差し替えが面倒になることが考えられます。

作ってみる

さて、Lightningページ作成時に選択するテンプレートですが、組み込みで用意されているもの以外に、カスタムで作ってそれを利用することもできます。

developer.salesforce.com

テンプレートの各リージョン(1つ1つの矩形のテンプレート領域を便宜上リージョンと呼んでいます)には、0個以上のLightningコンポーネントをスタックできるので、色んなグリッドのパターンを盛り込んだテンプレートを作ってあげれば、一つのレイアウトで色々なレイアウトのLightningページを作成できると考えました。

というわけで、作ったものがこちら。

f:id:jappy:20180718014824p:plain

(Lightningアプリケーションビルダーでの画面ですが、見やすさのために各リージョンにリッチテキストコンポーネントを配置しています)

タイトルで最強と言いつつも、以下の5タイプだけ対応しました。

  • 1カラム
  • 2カラム(等分割)
  • 3カラム(等分割)
  • 2カラム(右サイドバー形式)
  • 2カラム(左サイドバー形式)

また、順番の入れ替えもいったん未対応です。

コード

Component

カスタムのテンプレートはLightningコンポーネントバンドルとして実装します。
その際に必要なのはComponent(HTML)、Design(HTML)が必須で、後は必要に応じてSVGやStyleを作ります。今回は、Component、Design、Styleの3つを定義しました。

<aura:component implements="lightning:appHomeTemplate" description="All-in-one Template">
    <aura:attribute name="header" type="Aura.Component[]" />
    <aura:attribute name="leftOfBisection" type="Aura.Component[]" />
    <aura:attribute name="rightOfBisection" type="Aura.Component[]" />
    <aura:attribute name="leftOfTrisection" type="Aura.Component[]" />
    <aura:attribute name="centerOfTrisection" type="Aura.Component[]" />
    <aura:attribute name="rightOfTrisection" type="Aura.Component[]" />
    <aura:attribute name="leftOf2to1" type="Aura.Component[]" />
    <aura:attribute name="rightOf2to1" type="Aura.Component[]" />
    <aura:attribute name="leftOf1to2" type="Aura.Component[]" />
    <aura:attribute name="rightOf1to2" type="Aura.Component[]" />
    
    <div>
        <lightning:layout horizontalAlign="spread" multipleRows="true" pullToBoundary="medium">
            <lightning:layoutItem flexibility="grow" size="12" padding="horizontal-small">
                <div class="layout__col">{!v.header}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="6" padding="horizontal-small">
                <div class="layout__col">{!v.leftOfBisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="6" padding="horizontal-small">
                <div class="layout__col">{!v.rightOfBisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.leftOfTrisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.centerOfTrisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.rightOfTrisection}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="8" padding="horizontal-small">
                <div class="layout__col">{!v.leftOf2to1}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.rightOf2to1}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="4" padding="horizontal-small">
                <div class="layout__col">{!v.leftOf1to2}</div>
            </lightning:layoutItem>
            <lightning:layoutItem flexibility="grow" size="8" padding="horizontal-small">
                <div class="layout__col">{!v.rightOf1to2}</div>
            </lightning:layoutItem>
        </lightning:layout>
    </div>
    
</aura:component>

あんまり解説が必要なところがないのですが、レイアウトはLightningのnativeコンポーネントを使って楽にグリッドシステムを組み立てました。

Design

<design:component label="All-in-One Template">
    <flexipage:template>
        <flexipage:region name="header" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOfBisection" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOfBisection" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOfTrisection" defaultWidth="MEDIUM" />
        <flexipage:region name="centerOfTrisection" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOfTrisection" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOf2to1" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOf2to1" defaultWidth="MEDIUM" />
        <flexipage:region name="leftOf1to2" defaultWidth="MEDIUM" />
        <flexipage:region name="rightOf1to2" defaultWidth="MEDIUM" />
    </flexipage:template>
</design:component>

Style

CSS力が足らず少し時間がかかったのがこの部分。コンポーネントが存在しない行の場合に上下の空白の大きさを調節するため、CSS3のセレクタを使ってマージンを調整しています。

.THIS .layout__col > div:not(:empty) {
    margin-bottom: .75rem;
}

おわりに

ひとまず、目論んでいたようにカスタムテンプレートを作ることはできました。
ここで作ったテンプレートがどれくらい実用性があるのかは不明ですが、少なくとも、標準で用意されているテンプレートで対応できないケースの場合、カスタムのテンプレートを作るのは一つの手段として有力なので、こんなこともできるんだと触っておくとよいのかなと感じました。

Salesforce DXのforce:source:pushで「The "path" argument must be of type string.」エラーが出る時 #salesforce

ある時Salesforce DX (SFDX)の force:source:push で次のエラーが出ました。

$ sfdx force:source:push
ERROR:  The "path" argument must be of type string.

その時の対処法についてのメモです。

いきなり結論

*-meta.xml に対応するファイル(もしくはフォルダ)が正しく存在するか確認しましょう。

今回のケースでは、静的リソースフォルダ ( force-app/main/default/staticresources )に hoge.resource-meta.xml というファイルがあるのに、実際のリソースファイル(もしくはフォルダ)がないことが原因でした。

調査記録

原因を特定するために行ったことなどの記録です。

詳しいエラーメッセージを出してみる

一行だけのエラーメッセージでは原因に皆目検討がつかないので、 --json オプションをつけてもう少し詳細なエラーメッセージが表示されるようにします。 (出力形式をJSONにすることでなぜエラー出力が詳細になるのか?という疑問はさておき…)

するとこんな感じでした。

$ sfdx force:source:push --json
{"message":"The \"path\" argument must be of type string","status":1,"stack":"TypeError [ERR_INVALID_ARG_TYPE]: The \"path\" argument must be of type string\n    at assertPath (path.js:28:11)\n    at Object.resolve (path.js:1184:7)\n    at Object.copy (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/salesforce-alm/node_modules/fs-extra/lib/copy/copy.js:27:28)\n    at Object.copy (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/universalify/index.js:5:67)\n    at Object.tryCatcher (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/util.js:16:23)\n    at Object.ret [as copyAsync] (eval at makeNodePromisifiedEval (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promisify.js:184:12), <anonymous>:15:23)\n    at Promise.resolve.then (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/salesforce-alm/dist/lib/sourceConvertApi.js:143:46)\n    at tryCatcher (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/util.js:16:23)\n    at Promise._settlePromiseFromHandler (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promise.js:510:31)\n    at Promise._settlePromise (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promise.js:567:18)\n    at Promise._settlePromiseCtx (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/promise.js:604:10)\n    at Async._drainQueue (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/async.js:143:12)\n    at Async._drainQueues (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/async.js:148:10)\n    at Immediate.Async.drainQueues [as _onImmediate] (/Users/xxx/.nodebrew/node/v9.2.0/lib/node_modules/sfdx-cli/node_modules/bluebird/js/release/async.js:17:14)\n    at runCallback (timers.js:800:20)\n    at tryOnImmediate (timers.js:762:5)","name":"TypeError [ERR_INVALID_ARG_TYPE]","warnings":[]}

うーん、イマイチよくわかりませんね…。慣れている人ならピンとくるのかもしれませんが…。

sfdx-cliを最新にしてみる

最新バージョンにアップデートしました。

$ npm install --global sfdx-cli

(結果)状況変わらず…。

地道に原因を潰していく

どこのリビジョンからpushできなくなったのかを切り分けていき、原因を見極めていきます。
こういう時のためにも、意味のある最小単位で小さくコミットしていくことは重要ですね(自戒)

で、原因は、静的リソースのファイル不足であることがわかりました。

sfdxプロジェクトで開発をしている場合でも、webpackなどのNode.jsのエコシステムを使って静的リソースをビルドしてたので、ビルドの成果物をバージョン管理に含まれないようにするため、 .gitignore でこんなことを書いていたんですね…。

force-app/main/default/staticresources/**
!force-app/main/default/staticresources/*-meta.xml

そのため、ある人の環境できちんと動作でき、リポジトリに忘れずにpushされたはずの状態でも、別の人の環境で静的リソースファイルが存在せず、pushできなかった、という状況が起きていたようです。

Lightning Componentのコンポーネントライブラリが便利 #salesforce

Spring'18の時点でベータリリースはされていましたが、正式リリースされたっぽいです。

コンポーネントライブラリとは

developer.salesforce.com

実際のURLは https://developer.salesforce.com/docs/component-library/overview/components ですね。

上記のページを見てもらうのがよいですが、要するにLightningの基本コンポーネント(入力フォームとかグリッドシステムとか)のリファレンスページです。

どのようなコンポーネントがあるのか一目で分かるようになっており、各オプションの動作やコード表示も備わっているので、便利ですね。また、公式なので、リリースに伴う新しいコンポーネントやオプションもきちんと反映されていくことでしょう。

Lightningコンポーネントのライブエディタ

(ここからはただのこぼれ話です)

少し前(2017年11月頃)まではライブラリ集みたいなものはなく、ドキュメントもそこまで便利にはなっていなかったので、その不便さを解消するため、個人的に以下のようなプレビューアプリ(Lightningコンポーネントのライブエディタ)を作ったりしていました。

jappy.hatenablog.com

このアプリ、もうすぐ完成というところで、Spring'18が間近に迫り、その中でコンポーネントライブラリのベータ版 *1 もリリースされたことで、役割の被る拙作アプリの開発は止まっちゃっていました…。

当初目標に掲げていた以下のコンセプトも大体クリアできましたし、もう少しコードを整理して、別の形で活用法は考えたいなと思います。

  • Apex, Visualforceを一行も書かずに使えるアプリを作ること
  • 極力Lightning Design Systemを直に使わず、アプリ自身もLightning基本コンポーネントを徹底的に活用して作ること
  • $A.createComponents でどこまで柔軟なUIが作れるのか見極めること

*1:当時はhttps://<myDomain>.lightning.force.com/componentReference/suite.app みたいなURLにアクセスすると見ることができました。