pyenv利用時にパッケージコマンド実行時に出るcommand not found...command exists in these Python versionsエラーを治す

pyenvとpipを使っている環境で、下記のようなエラーが出た時の解消法についてのメモです。

$ hoge
pyenv: hoge: command not found

The `hoge' command exists in these Python versions:
  3.5.2

解消法

まずはコマンドが存在するというPythonバージョンに切り替えます。

$ pyenv global 3.5.2
$ pyenv rehash

該当パッケージをアンインストールします。

$ pip3.6 uninstal hoge

利用したいPythonバージョンにまた切り替えます。

$ pyenv global 3.7.0
$ pyenv rehash

あとは該当パッケージをインストール。

CodePipeline経由と直接実行の場合でCodeBuildのファイルパーミッションが異なるという話

CodeBuildでソースにGitHubを指定した場合のビルドで、

  • 直接CodeBuildで「ビルドの開始」を押した時
  • CodePipelineのBuildから実行された時

のそれぞれで、ファイルパーミッションが変わってしまっている?という話です。

Java(Gradle)でビルドするジョブで、後者の場合 gradlew の実行権限が付与されていないために、ビルドエラーになることで気づきました。

buildspec.yml

以下のように、buildフェーズの際にカレントディレクトリのファイル一覧を出力させてデバッグします。

version: 0.2

phases:
  build:
    commands:
      - ls -la
      - ./gradlew bootJar

artifacts:
  files:
    - build/libs/*.jar

cache:
  paths:
    - '/root/.gradle/**/*'

直接実行

f:id:jappy:20190114145654p:plain

gradlew に実行可能権限が付いています。

CodePipeline経由で実行

f:id:jappy:20190114145609p:plain

gradlew に実行可能権限が付いていません。
実行前に付与してあげれば済むはずですが、一応覚えておきたいポイントだなと思いました。

API Gateway WebSocketのコストメリットやLambdaの同時実行数について

Amazon API GatewayでWebSocketが利用可能になりました。

使用感などは、下記のAWSブログでのチャットアプリのサンプルを構築してみると、わかってきます。それにしても、チャットアプリはWebSocketのサンプルとして定番中の定番ですねぇ…。

[発表]Amazon API GatewayでWebsocketが利用可能 | Amazon Web Services ブログ

使ってみた印象としては、「ルート」に対して、Lambda関数(あるいはその他のインテグレーション)を割り当てていく、というところが面白いところですが、それ以外の設定については、ほとんどRESTと同じだった印象です。
むしろ導入する場合のハードルは、クライアント側の実装の方かと思いました(昔socket.ioでとあるシステムを開発していたことがありますが、今もデファクトなんですかね…?)

それはさておき、本記事では、API GatewayのWebSocketを実運用する場合に、いくつか浮かんだ疑問について考えてみました。

  • ポーリング方式と比べた場合のコストメリット
  • Lambdaの同時実行数は大丈夫?
  • ALBとの比較

想定するユースケース

ユースケースとしては、チャットアプリではなく、AWS側で発生した何らかのイベントを、接続中のクライアントにプッシュする、というものです。下図の赤い矢印(サーバプッシュ)の部分の送信ですね。

f:id:jappy:20190114064014p:plain

ちなみに図中はイベントの発生元がRDSになってますが、まぁ何かしらのイベント発生だと読み替えてください。

これまでは、このようなことをやろうとした場合、

  • 同じくWebSocketをサポートしているALBを使い、EC2やLambdaなどでWebSocketサーバを立てる
  • (諦めて)クライアントからポーリングをする
  • GraphQL Subscription(使ったことない…)

などの方法がありました(参考記事 [1])

[1] リアルタイム革命/The Revolution of Real-time WebApps - Speaker Deck

ポーリング方式と比較してのコストメリット

東京リージョンの場合の料金は、2018年1月14日現在、下表のようになっています(一部のみ。無料利用枠を除く)

HTTP/REST API WebSocket API
リクエスト数に対する料金(100万回あたり) 4.25USD(最初の3億3,300万コールまで) 1.26USD(最初の10億メッセージまで)
接続時間に対する料金(100万分≒16,667時間≒694日あたり) N/A 0.315USD

ポーリングの頻度、WebSocketで送信が必要なイベントの発生頻度、接続時間、など色々と前提条件が異なるので一概に比較はできませんが、多くの場合、WebSocketの方がコストを低く抑えられそうです。

リクエスト数に対する料金は、単純計算で1/3以下になっていますし、ポーリング方式の場合、イベントの発生有無に関わらずAPIコールが発生していました。WebSocketの場合、イベントが発生していた場合だけメッセージ送信すればよいので、送受信するメッセージ数そのものも抑えることができます。

Lambdaの実行単位

当初、「クライアントとLambdaの接続が確立しっぱなしになると、クライアントの数が増えた時にLambdaの同時実行数に達してしまうのでは?」という疑問が浮かびました。1接続につき、Lambda関数が1個占有されてしまうようなイメージですね。

こちらは勘違いだったみたいです。
クライアントの接続の管理は、API Gateway側で行われます。

Lambda関数は、メッセージの送信(あるいは接続、切断)が発生した場合に限り、イベント駆動で起動され、処理が実行されるようでした。

ALBとの比較

同じくre:Inventの発表で、ALBのターゲットにLambdaが利用できるようになりました。
ALBもWebSocketをサポートしているので、ALB + Lambdaでも、イベントドリブンなWebSocketシステムの実現ができそうです。

こちらのパターンは未確認なのですが、API GatewayのWebSocketを利用するメリットとしては、スロットル制御、利用量プラン、認証、Lambda以外のインテグレーション(HTTP, Mock, その他AWSサービス)もできる、といった点だと思います。

gradle-jooq-pluginでカスタムジェネレータを試してみる

JOOQではコード生成に利用するジェネレータを差し替えることで、デフォルトの命名やパッケージのレイアウトをカスタマイズすることができます。

これをgradle-jooq-pluginで試してみました。
結論、プラグインページのサンプルを素直に真似すれば、できます。

https://github.com/etiennestuder/gradle-jooq-plugin/tree/v3.0.2/example/use_custom_generator

最初は、上記のようにマルチプロジェクトではなく、buildSrcに配置して試したのですが、 ClassNotFoundException が解決できませんでした(なぜだろう…)

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);
    }
})