2019年12月14日 (土)

Windowsでmatn/go-oci8を使おうとしてとても苦労している話

0. 唐突にOracle Databaseを使おうと思い立った

Oracle Databaseを使ったシステムなど、お仕事くらいでしか使うことはなかったんだけど、Oracle Cloud Free Tierとして使えるDBのPaaSがOracle Databaseだけ、というので、かなり仕方なく使う、というネガティブな話だったりする。

Javaや.NETには公式のデータベースドライバが出ているので、それらを使えば簡単にDBを利用できるけど、せっかくなので今回はGoでやってみることにした。

なお、結論から書くとこの話、実現できてないorz

1. 環境

ビルドには以下が必要。

  • C/C++コンパイラ
  • pkg-config
  • プラットフォームとバージョンに応じたOracle InstantClient

このほか、当然のことながらGitやGoが必要。

1-1. MSYS2

僕のWindows PC(Windows 10 pro version 1909)にはSphinxでPDFをビルドするため、MSYS由来のGNU Makeとshが入っていたので、GCCを入れるためpacmanを起動。
しかしながら、なぜか何もフィードバックを出さずpacmanが途中で終了してしまい、パッケージのインストールもアップデートもできずハマる。

おかしい、version 1809のころはふつうにpacmanが動いていたはずなのに。

これでは何もできないので、MSYS2はあきらめることに。

1-2. Cygwin

ならば、ということでMSYS2をアンインストールしてCygwin x64を入れてみた。

Baseパッケージグループのほか、makeやGCCをCygwin Installerで一とおりインストールし、C:\cygwin64\binなどをWindowsのPath環境変数に追加していった。

Cygwin shellではなくDOS窓からgo get github.com/mattn/go-oci8とやったところ、

  # runtime/cgo
  gcc_libinit_windows.c: 関数 ‘x_cgo_sys_thread_create’ 内:
  gcc_libinit_windows.c:57:12: エラー: implicit declaration of function ‘_beginthread’; did you mean ‘OpenThread’? [-Werror=implicit-function-declaration]
    thandle = _beginthread(func, 0, arg);
              ^~~~~~~~~~~~
              OpenThread
  cc1: all warnings being treated as errors
  go: failed to remove work dir: GetFileInformationByHandle C:\cygwin64\tmp\go-build601027150\NUL: Incorrect function.

と出てビルドできず。後でググると本体のGitHub WikiInstallFromSource

Go does not support the Cygwin toolchain.

などど書かれていた。なん....だと....?

1-3. WSL

最近のエディションにはWSL(Windows Subsystem for Linux)という、読んで字のごとくLinux互換環境を提供するサブシステムが標準搭載されている。デーモンを常駐させられない以外はほぼLinuxそのままなので、こいつで頑張ってみることに。

僕は普段UbuntuのWSLを使っているけど、GCCもpkg-configもapt installで導入できるのでお手軽だ。

ということでapt installでGCCやらpkg-configやらをインストールしてgo getしてみる。
結果、エラー表示は出ず。

いけた、のか? ということで付属のexampleで動作確認。

  $ cd $GOPATH/src/github.com/mattn/go-oci8/_example/nls
  $ GO_OCI8_CONNECT_STRING=system/Orcl19cAdmin@//localhost:1521/ORCLCDB go run main.go
  ORA-12571: TNS:packet writer failure

パケット到達不能、だと....? まぁ、Oracle Databaseは親環境のDockerで動かしているとはいえ、WSLからDockerを制御できているので、親環境とは通信できているはずだが。

てなわけで、この方法もダメということに。

1-4. TDM-GCC

Goの公式としてはTDM-GCCを推奨しているようなので、Cygwin GCCをアンインストールしてこちらに切り替えてみた。

パッケージマネージャは付属していないので、pkg-configを自力でセットアップしなければならない。やり方自体はStackOverflowの記事を参考にやってみた。

一応、直リンクを置いておく。

準備自体はGistを残してくれている方がいたので、それを使わせてもらうことに。

  > go get github.com/mattn/go-oci8
  # pkg-config --cflags  -- oci8
  pkg-config: exit status 3221225595

3221225595をWindowsの電卓に通すと0xC000007Bになるのだが、pkg-configが正常起動できていない、ということのようだ。どうやらABIが違うようなので、win64ではなくwin32に変えることで正常起動するようになった。

で、再挑戦。

  > go get github.com/mattn/go-oci8
  # github.com/mattn/go-oci8
  In file included from GOPATH\src\github.com\mattn\go-oci8\cHelpers.go:3:0:
  ./oci8.go.h:1:17: fatal error: oci.h: No such file or directory
  compilation terminated.

....pkg-configがインクルードパスを正常に処理できていないようだ。もちろん、oci.hの場所は-Iオプションの行で指定しているんだが。

おわりに

最後のTDM-GCCパターンではあと一歩なような気もする。

そんな気はするんだが、MySQLやPostgreSQLのドライバだったり、GoではなくJavaを選択していた場合だと、もっと簡単にデータアクセスを実装できていたであろう、と考えると、「なぜ僕はこんな苦行を....?」などと考えてしまう。

| | コメント (0)

2019年10月 1日 (火)

転職した

0. 突然ですが

転職した。

1. いきさつとか行先とか

今までは開発会社でプログラマーとかをやっていたんだけど、今度はユーザー企業の情シスで働きます。

いきさつとかいわゆる「転職エントリ」で出てくるようなことは、書かないでおく。あと、ウィッシュリストも。

 

| | コメント (0)

2019年9月25日 (水)

Apache Wicketを使ってみる[5] -テスト編-

0. Wicketのテストサポート

WicketTester というヘルパークラスがcoreパッケージに含まれていて、Servletコンテナを介さずとも JUnit 4 でユニットテストを簡単に実行できるようになっている。

1. 簡単な使用例を実際に動かしてみる

前述の WicketTester の先頭にある使用例を Kotlin でかいてみた。
シナリオとしては以下のとおり。

  1. MyPage と YourPage のふたつのページがある
  2. MyPage のリンクをクリックすると YourPage へ遷移する
  3. YourPage はコンストラクタ引数のパラメータをラベルに反映する

プロダクションコード

  • MyPage
class MyPage() : WebPage() {
  override fun onInitialize() {
    super.onInitialize()
    add(Label("myMessage", "Hello!"))
    // クリックすると「YourPage」へ遷移するリンク
    add(object : Link("toYourPage") {
      override fun onClick() {
        setResponsePage(YourPage("Hi!"))
      }
    })
  }
}
  • YouePage
class YourPage(private val message: String) : WebPage() {
  override fun onInitialize() {
    super.onInitialize()
    add(Label("yourMessage", message))
    info("Wicket Rocks ;-)")
  }
}

テストコード

  • MyPageTest
class MyPageTest {
  /** WicketTester のインスタンス */
  private var tester: WicketTester? = null

  @Before
  fun setUp() {
    tester = WicketTester(WicketApplication())
  }

  @After
  fun tearDown() { }

  /**
   * MyPageが表示可能なことを確認するテスト。
   */
  @Test
  fun canRenderMyPage() {
    // MyPageを開始
    tester!!.startPage(MyPage::class.java)

    // MyPageが表示可能なことを確認
    tester!!.assertRenderedPage(MyPage::class.java)

    // MyPageに「Hello!」と表示されていることを確認
    tester!!.assertLabel("myMessage", "Hello!")
  }

  /**
   * MyPageのリンクをクリックするとYourPageへ遷移できることを確認するテスト。
   */
  @Test
  fun canFlipToYourPage() {
    tester!!.startPage(MyPage::class.java)
    // 「toYourPage」リンクをクリックするとYourPageへ遷移することを確認
    tester!!.clickLink("toYourPage")
    tester!!.assertRenderedPage(YourPage::class.java)

    // YourPageのメッセージが「Hi!」であることを確認
    tester!!.assertLabel("yourMessage", "Hi!")
  }
}
  • YourPageTest
class YourPageTest {

  /** WicketTester のインスタンス */
  private var tester: WicketTester? = null

  @Before
  fun setUp() {
    tester = WicketTester(WicketApplication())
  }

  @After
  fun tearDown() { }

  /**
   * コンストラクタ引数を画面反映できることを確認するテスト。
   */
  @Test
  fun canShowConstructorOperand() {
    // コンストラクタ引数に「Unit Testing...」を指定するして起動できることを確認
    tester!!.startPage(YourPage("Unit Testing..."))
    tester!!.assertRenderedPage(YourPage::class.java)

    // コンストラクタ引数に指定した文字列がラベルに反映されていることを確認
    tester!!.assertLabel("yourMessage","Unit Testing...")

    // コンソールに INFO レベルのメッセージが書かれていることを確認
    tester!!.assertInfoMessages("Wicket Rocks ;-)")
  }
}

そのほか、UI部品の有効、無効なども同様の方法でテストできる。

2. Formのテスト

前述の WicketTester から FormTester という、サブミットを含めたFormの操作とテストを行うヘルパークラスを得ることができる。

  /**
   * 正しいユーザー名とパスワードならログオンできることを確認するテスト。
   */
  @Test
  fun canLonginCorrectUserAndPW() {
    // 「form」という名で定義されているFormを、空文字で埋めないよう取得
    val formTester = tester!!.newFormTester("form", false)
    // ユーザー名とパスワードに「user」をセットしてサブミット
    formTester.setValue("username", "user")
    formTester.setValue("password", "user")
    formTester.submit()

    // コンソールにログイン成功が出力されていることを確認
    tester!!.assertInfoMessages("Username and password are correct!")
  }

Form のサブミットには、ほぼ間違いなくその後ろに DB アクセスが発生するだろうけれど、これを JUnit でテストしようと思ったら 既にあるデータを消さないとテスト結果がそれに依存してしまうし、 DB への書き込みをすべてロールバックしないと、ほかの開発データも書きつぶしてしまうことになる。

こういうとき、 DI で DB アクセス部分を分離してやればテストしやすい構造になるんだろうけれど、それは今度試してみよう....。

| | コメント (0)

2019年9月16日 (月)

Apache Wicketを使ってみる[4] -認証/認可編-

0. Wicketで認証/認可を扱うには

wicket-auth-roles というdependencyが必要。

<dependency>
  <groupId>org.apache.wicket</groupId>
  <artifactId>wicket-auth-roles</artifactId>
  <version>8.5.0</version>
</dependency>

1. Wicketでの認証

通常の WebApplication に認証情報を処理できるような拡張が施された AuthenticatedWebApplication と、認証処理と認証情報のセッションストアへの格納を行う AuthenticatedWebSession で構成される。

二つとも abstract なので、使うには継承したうえで実装しなければならない処理がある。

AuthenticatedWebApplication AuthenticatedWebSession
  • protected Class<? extends WebPage> getSignInPageClass()
    サインインページを構成するクラスを返す
  • protected boolean authenticate(String username, String password)
    ユーザー名とパスワードから認証を行う
  • protected Class<? extends AbstractAuthenticatedWebSession> getWebSessionClass()
    アプリケーションで使うセッションストアのクラスを返す

上記のユーザーガイドからの抜粋をKotlinで書いてみると、こんな感じになる。

  • セッションストアクラス
class BasicAuthenticationSession(request: Request) : AuthenticatedWebSession(request) {
  override fun authenticate(username: String, password: String) : Boolean {
    // ここで認証処理を行う
    // 通常であれば、DBアクセスしてユーザーの有無やパスワード比較とかをする、はず
    return username == password && username == "wicketer"
  }

  override fun getRoles() : Roles {
    // ロールベースのアクセス制御用に、ログイン中ユーザーのロールを返す
    // 通常であれば、セッションストアのユーザー情報からロールを引いたりする、はず
    return Roles()
  }
}
  • アプリケーションクラス
class WicketApplication() : AuthenticatedWebApplication() {
  override fun getHomePage() : Class<out WebPage> {
    // いわゆる「スタートページ」を構成するクラスを返す
    return HomePage::class.java
  }
  override fun getWebSessionClass() : Class<out AbstractAuthenticatedWebSession> {
    // アプリケーションでつかうセッションストアを返す
    return BasicAuthenticationSession::class.java
  }
  override fun getSignInPageClass() : Class<out WebPage> {
    // いわゆる「ログインページ」を構成するクラスを返す
    return SignInPage::class.java
  }
}
  • サインインページ
class SignInPage() : WebPage() {
  private var username: String = ""
  private var password: String = ""

  override fun onInitialize() {
    super.onInitialize()

    // ログインフォームを構成するStatelessForm
    val form: StatelessForm<Unit> = object : StatelessForm<Unit>("form") {
      override fun onSubmit() {
        if (username.length == 0) {
          return
        }

        // 認証処理を呼び出し、認証成功のときは前ページで設定されたリダイレクト先へ遷移する
        val authResult = AuthenticatedWebSession.get().signIn(username, password)
        if (authResult) {
          // ここではそのままリダイレクトしているが、ここでユーザーロールのチェックとかをはさむ、はず
          continueToOriginalDestination()
        }
      }
    }

    // ModelとTextFieldをformに追加する
    form.model = CompoundPropertyModel(this)
    form.add(TextField<String>("username"))
    form.add(PasswordTextField("password"))

    add.(form)
  }
}

サインインページのデフォルト実装はあるけれど、同じくデフォルト実装のパネルと組み合わせて使う想定のようで、自力で書くのと大差ないかも....

2. 途中で認証をはさみたいときは

「トップページとかは普通に見れるようにするが、あるページは会員だけに見せたい」といった場合、途中でユーザー認証を行い、認証に成功したらユーザーがもといたページに戻す、といった処理をやることになる。

そういった場合は、以下の二つの方法をとる、とのこと。[出典]

いずれも、実行時に遷移前の状態をセッションストアに保存したあと、認証に成功した場合認証前の状態に復元してくれるが、認証ページへの遷移直前に RestartResponseAtInterceptPageException が内部的にスローされてページ遷移するので、以後の処理は行われなくなる。

3. Wicketでの認可

いわゆる処理に対する権限チェックである。先ほどの例で出てきた AuthenticatedWebSession#getRoles() も、認可に関する処理の一つ。
「ログインしているか否か」も、認可の範囲に入る模様。

基本処理は IAuthorizationStrategy で規定されている

boolean isActionAuthorized(Component, Action) アクションが許可されているかどうかを返す。
boolean isInstantiationAuthorized(Class<T extends IRequestableComponent>) 指定されたコンポーネントをインスタンス化できるかどうかを返す。
boolean isResourceAuthorized(IResource, PageParameters) リクエストのパラメータが、リソースへのアクセスが許可されているかどうかを返す。

だが、すべてを許可するALLOW_ALLという実装のほか、ページ単位のチェックを行う規定処理がすでに組み込まれている。個人的には、一番しっくりきたのがロールベース戦略として紹介されている MetaDataRoleAuthorizationStrategyAnnotationsRoleAuthorizationStrategy だった。
それぞれ、用途的には以下のような違いがある。

MetaDataRoleAuthorizationStrategy アプリケーション起動時に、あらかじめ使用可能な権限をページごとに列挙しておく
AnnotationsRoleAuthorizationStrategy ページクラスに対し、アノテーションで権限を宣言していく

両者とも一長一短ではあるけど、たとえば、 AdminOperatablePage というページに「サインインしていたら表示可能、スーパーユーザーなら中のFormを操作可能」という権限を設定する場合、

  • MetaDataRoleAuthorizationStrategy のばあい
class WicketApplication() : AuthenticatedWebApplication() {
  override fun init() {
    securitySettings.authorizationStrategy = MetaDataRoleAuthorizationStrategy(this)

    // ログインしていたら "AdminOperatablePage" はインスタンス化を許可
    MetaDataRoleAuthorizationStrategy.authorize(AdminOperatablePage::class.java, "SIGNED_IN")

    // adminForm というコンポーネントは、"ADMIN"なら操作可能にする
    // "ADMIN"でないときはロックされる
    MetaDataRoleAuthorizationStrategy.authorize(adminForm, Action.ENABLE, "ADMIN")
  }
}
  • AnnotationsRoleAuthorizationStrategy のばあい
@AuthorizeInstantiation("SIGNED_IN")
@AuthorizeAction(action = "ENABLE", roles = {"ADMIN"})
class AdminOperatablePage(params: PageParameters) : WebPage(params) {
  // @AuthorizeInstantiation で "SIGNED_IN" を持つ場合にインスタンス化を許可
  // ここの @AuthorizeAction は、ページ全体に効果がある
  ...
}

....といった感じになるはず。

なお、権限は AuthenticatedWebSession#getRoles() で取得されるので、先ほどのセッションストアクラスを書き直してみると、

class BasicAuthenticationSession(request: Request) : AuthenticatedWebSession(request) {

  override fun getRoles() : Roles {
    // ロールは中身がHashSet<String>なので、条件に応じたロール識別文字列をaddしていく
    // @see https://ci.apache.org/projects/wicket/apidocs/8.x/org/apache/wicket/authroles/authorization/strategies/role/Roles.html
    val resultRoles = Roles()

    if (isSignedIn()) {
      // たとえば、サインインしていたら"SIGNED_IN"をおく、とか
      resultRoles.add("SIGNED_IN")
    }
    if (username == "superuser") {
      // スーパーユーザーだったら"ADMIN"を置く、とか
      resultRoles.add("ADMIN")
    }
    // このほか、ユーザーのオブジェクトを保管しているなら、そのオブジェクトにぶら下がっている
    // ロールをaddしていくことになる、はず

    return resultRoles
  }
}

...といった具合になるはず。

4. 認可に失敗したときは

認可に失敗したとき、デフォルトでは

  • ユーザーがサインインしていないときは、既定のサインインページへ遷移させる
  • サインインしているがコンポーネントが認可できない場合は、 UnauthorizedInstantiationException がスローされる

....という挙動だが、カスタム処理にしたい場合は IUnauthorizedComponentInstantiationListener というイベントリスナがあるので、これの中で処理する。

  securitySettings.unauthorizedComponentInstantiationListener = IUnauthorizedComponentInstantiationListener {
    // 認可失敗を処理する "AuthWarningPage" というページをレスポンスにする
    it.setResponsePage(AuthWarningPage::class.java)
  }

ロールの検証を追加するなら、こんな感じになるはず。

  securitySettings.unauthorizedComponentInstantiationListener = object : IRoleCheckingStrategy, IUnauthorizedComponentInstantiationListener {
    override fun onUnauthorizedInstantiation(component: Component?) {
      // 認可失敗を処理する "AuthWarningPage" というページをレスポンスにする
      component!!.setResponsePage(AuthWarningPage::class.java)
    }

    override fun hasAnyRole(roles: Roles): Boolean {
      // 指定ロールが見つかったらtrueを返す処理を書く
      var result = roles.hasRole("ADMIN")
      return result
    }
  }

まぁ、このあたりはデフォルト実装でもどうにかなるレベルなので、やるかどうかはケースバイケースか。

| | コメント (0)

2019年9月14日 (土)

MavenのJettyにDataSourceを設定するのに苦労した話

0. やっぱりデータソースは外に出したいのです

前回のデータアクセスの話では、

val database: Sql2o = Sql2o("jdbc:mysql://localhost:3306/mydb", "mydbadmin", "mydbadmin")

と、ソースコードに直接書いていたけれど、実際の運用を考えるとDBの接続先設定とプロダクションコードは極力分離したいところ。

幸いにも、Mavenで作ったプロジェクトのアーキタイプにはJettyがdependencyに組み込まれていて、test側に開発サーバとしてJettyを起動するときの処理が最初から入っている。
ここに DataSource 設定を書いてやれば、理屈の上ではプロダクションコードからDBの接続先設定を分離できることになる。

で、前置きが長くなってしまったけど、Mavenで依存に入っているJettyにDataSourceを設定するのに結構苦労をしたので、その時の備忘録的なメモを書いてみる。

1. やること(というかやったこと)

やったことをざっくりまとめると、以下のとおり。

  1. pom.xmlに依存とプラグイン設定を追加
  2. Jettyの設定ファイル追加
  3. web.xmlにDataSourceルックアップ設定を追加
  4. DataSourceを使うようデータアクセス部分を変更

1. pom.xmlに依存とプラグイン設定を追加

なにはなくともdependencyに必要なライブラリを入れなければならない。
今回はコネクションプールライブラリを挟む方針で、高性能を謳っていることで話題のHikariCPを使ってみることにする。

<dependencies>
  <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.17</version>
    <!-- Jettyがtestスコープで動くのでそちらに合わせる -->
    <scope>test</scope>
  </dependency>
  <dependency>
    <groupId>com.zaxxer</groupId>
    <artifactId>HikariCP</artifactId>
    <version>3.3.1</version>
    <!-- Jettyがtestスコープで動くのでそちらに合わせる -->
    <scope>test</scope>
  </dependency>
</dependencies<

Jettyがtestスコープで動くので、コネクションプールとJDBCドライバもそれに合わせておく。

次に、JettyがHikariCPとconnector-jを扱う設定を読み込めるようにするのだけど、これはMavenプラグインの設定をいじる必要がある。
追加するとこんな感じになるはず。

<plugins>
  <plugin>
    <groupId>org.eclipse.jetty</groupId>
    <artifactId>jetty-maven-plugin</artifactId>
    <version>${jetty9.version}</version>
    <configuration>
      <-- 追加、testスコープのclasspathを優先する設定 -->
      <useTestScope>true</useTestScope>
      <-- 追加ここまで -->
      <systemProperties>
        <systemProperty>
          <name>maven.project.build.directory.test-classes</name>
          <value>${project.build.directory}/test-classes</value>
        </systemProperty>
      </systemProperties>
      <jettyXml>
        ${project.basedir}/src/test/jetty/jetty.xml,${project.basedir}/src/test/jetty/jetty-ssl.xml,${project.basedir}/src/test/jetty/jetty-http.xml,${project.basedir}/src/test/jetty/jetty-https.xml
      </jettyXml>
      <!-- 以下追加 -->
      <webApp>
        <!-- web.xml を読み込む先を指定する設定 -->
        <descriptor>${project.basedir}/src/main/webapp/WEB-INF/web.xml</descriptor>
        <!-- 「データソースとして定義する内容」を書いたファイルを読み込む設定 -->
        <jettyEnvXml>${project.basedir}/src/test/jetty/jetty-env.xml</jettyEnvXml>
      </webApp>
      <!-- 追加ここまで -->
    </configuration>
  </plugin>
</plugins>

2. Jettyの設定ファイル編集

前述のpom.xml

に追加したjetty-env.xmlを、src/test/jettyに作る。

設定例をググっていると、 Configure の class に org.eclipse.jetty.webapp.WebAppContext を使う例が結構ひっかっかると思うけど、これをやると ClassCastException で設定に失敗するので org.eclipse.jetty.maven.plugin.JettyWebAppContext に変更している。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Configure PUBLIC "-//Jetty//Configure//EN" "http://www.eclipse.org/jetty/configure_9_0.dtd">
<!-- Configure の class は、 Maven プラグインから動くのでこっちを使う -->
<Configure id="wac" class="org.eclipse.jetty.maven.plugin.JettyWebAppContext">
  <New id="myDS" class="org.eclipse.jetty.plus.jndi.Resource">
    <Arg><Ref refid="wac" /></Arg>
    <!-- ここが DataSource の名前になる -->
    <Arg>jdbc/datasource</Arg>
    <Arg>
      <New class="com.zaxxer.hikari.HikariDataSource">
        <Arg>
          <New class="com.zaxxer.hikari.HikariConfig">
            <!-- 以下 setter 部分の意味は JavaDoc とかを参照のこと -->
            <Set name="maximumPoolSize">20</Set>
            <Set name="driverClassName">com.mysql.cj.jdbc.Driver</Set>
            <Set name="jdbcUrl">jdbc:mysql://localhost:3306/mydb</Set>
            <Set name="username">mydbadmin</Set>
            <Set name="password">mydbadmin</Set>
          </New>
        </Arg>
      </New>
    </Arg>
  </New>
</Configure>

3. web.xmlにDataSourceルックアップ設定を追加

下回りは設定できたので、プロダクションコードに関連する箇所をいじって、 DataSource をルックアップできるようにする。

pom.xmlに追加したとおり、/src/main/webapp/WEB-INF/web.xmlに以下を追加。

<web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://www.oracle.com/webfolder/technetwork/jsc/xml/ns/javaee/web-app_3_1.xsd"
    version="3.1">

  <!-- "jdbc/datasource" をデータソースとして名付ける設定 -->
  <resource-ref>
    <res-ref-name>jdbc/datasource</res-ref-name>
    <res-type>javax.sql.DataSource</res-type>
    <res-auth>Container</res-auth>
  </resource-ref>

</web-app>

4. DataSourceを使うようデータアクセス部分を変更

ここまでくれば DataSource をルックアップできるようになるので、接続設定を DataSource を使うように変更する。
例によって Kotlin で書いている。

open class DatabaseHelper {
    // init ブロックで初期化されるので空でよい
    //val database: Sql2o = Sql2o("jdbc:mysql://localhost:3306/mydb", "mydbadmin", "mydbadmin")
    val database: Sql2o

    init {
        // InitialContext から "jdbc/datasource" をルックアップする
        val context = InitialContext()
        val datasource = context.lookup("java:comp/env/jdbc/datasource") as DataSource
        // ルックアップした DataSource オブジェクトで Sql2o を初期化
        database = Sql2o(datasource)
    }
}

この状態で mvn package を実行して war を作り、 Tomcat とかのアプリケーションコンテナにデプロイしてもちゃんと動く。ここまで来るのに長かった....。

| | コメント (0)

2019年9月 7日 (土)

Apache Wicketを使ってみる[3] -Model編-

0. Wicketでいう「Model」とは何か

Modelというと、だいたいビジネスロジック層とかデータアクセス層とかの認識でいるわけだが、Wicketにおける「Model」はセッションストアを含むデータアクセスも、Viewへの入出力を含む状態の受け渡しもやる。[出典]
こう書いてしまうと、かなりViewModelっぽい気もする。

1. 基本中の基本

単純なラベル表示の場合、たいていのチュートリアルにはこう書かれているはず。

HTML
<div><span wicket:id="strLabel">Label!!</span></div>
Java
add(new Label("strLabel", "Hello, World!!));

要は「"strLabel"というIDをもつ要素に"Hello, World!!"という文字列を置け」というだけのものだけれど、内部的には

add(new Label("strLabel", new Model<String>("Hello, World!!")));

と、Viewへの出力を行う基本的な実装を持つModelを仲介している、そうな。[出典]

2. 応用編

前述の例は単純なリテラルの出力だけだけど、当然のことながら「Java Beansとのやりとり」も「別のModelが吐いた出力を受けた入力」も、Modelとして処理できる。

全文はhttps://github.com/bitstorm/Wicket-tutorial-examples/tree/master/ModelChainingExampleを参照いただくとして、「ドロップダウンリストの選択値変化に応じてFormのテキストボックスの値を動的に変更する」処理も容易に実現できる。
なお、こちらのコードは例によってKotlinで書いてみた。

class PersonListDetails() : WebPage() {
    /** 連動して値を反映させる先のForm */
    private lateinit var forms: Form<Unit>
    /** ドロップダウンリストの中身(select + option) */
    private lateinit var personList: DropDownChoice<Person%>

    init {
        // selectとformの橋渡しをするModel
        val listModel = Model<Person>
        // selectで選択された値を拾うレンダラー
        personRender = ChoiceRenderer<Person>("fullName")

        // ドロップダウンリストを画面へ割り当てる
        personList = DropDownChoice<Person>("persons", listModel, loadPersons(), personRender)
        // 更新通知を受け取るためのイベントリスナー的なもの
        // これをトリガーに、下で定義しているformにModelが値を反映してくれる
        personsList.add(FormComponentUpdatingBehavior());
        // 画面へコンポーネントを反映
        add(personList)

        // selectの変化を受け取るModelを引数にとるModelをformに張る
        form = Form("form". CompoundPropertyModel<Person>(listModel))
        //   それぞれ、listModelが持っているJava Beansのプロパティに紐づいている
        form.add(TextField("name"));
        form.add(TextField("surname"));
        form.add(TextField("address"));
        form.add(TextField("email"));

        add(form)
    }

    // selectに張る中身のListを作る処理
    fun loadPerson(): List<Person> {...}
}

// 名(name)と姓(surname)を受け取る
data class Person(var name:String, var surname: String): Serializable {
    // 住所
    var address: String
    // メアド
    var email: String
    // 配偶者
    var spouse: Person?
    // 子供
    var children: MutableList<Person>?
}

これをStrutsでやろうと思うと、JSPにhiddenでいっぱい値をねじ込んでおかないとサクッとはできないよな....

ちなみに、前述のチュートリアルを動かすには pom.xmlのパッケージング設定がjarになっているのでwarに変更する 必要があるので注意。

| | コメント (0)

2019年9月 2日 (月)

続:Apache Wicketを使ってみる -データアクセス編-

0. データアクセスをどうするか

Quick Start Wizardで作成したMavenプロジェクトには、当然のことながらデータアクセスに関するライブラリがないので、自力でどうにかするしかない。

とはいえ、Spring Bootのような至れり尽くせりな環境でもないけど、ローレベルなJDBCをスクラッチで書き起こす元気もないので、今回はsql2oというライブラリを使ってみることにする。

1. sql2oとは

sql2o本家、およびGitHubの記述をざっくりまとめると、こんな感じ。

位置づけ的にはDapper.NETRoom Persisitence Libraryに近いかも。

  • 小さく軽量なデータアクセス用ライブラリ
  • JDBCのResultSetをPOJOに変換する機能を提供するが、SQLを生成する機能はない
  • 他の著名なライブラリに比べ、高速に(モノによっては6,7倍高速)動作する

導入はMavenで。執筆時点の最新版は1.6.0だった。
今回はDBMSにMySQLを使ってみることにするので、Connector/Jも一緒に設定しておく。

<dependency>
  <groupId>org.sql2o</groupId>
  <artifactId>sql2o</artifactId>
  <version>1.6.0</version>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>8.0.17</version>
</dependency>

2. 検索系処理

データベースへの接続は、Sql2oのコンストラクタに接続文字列を渡すことで作ってくれる。
なお、あえてすべてKotlinで書いてみた。

val database: Sql2o = Sql2o("jdbc:mysql://localhost:3306/mydb", "mydbadmin", "mydbadmin")

オープン処理とかは、DAOの基底クラスにプロパティを書いておくと扱いやすいかも。

/** トランザクションなしのConnection */
val connection: Connection
    get() {
        return database.open()
    }
/** トランザクション開始ありのConnection */
val connectionWithTran: Connection
    get() {
database.beginTransaction() }

で、検索系はSQLをそのまま書き、それを Connection#createQuery(String) に渡して Query オブジェクトを作り、 Query#executeAndFetch(Class) を実行すると ResultSet を POJO へバインドしてくれる。

// SQLをそのまま書く。長いSQLの場合はヒアドキュメントを使うと楽かも
val sql = "select todo_id, todo_title, created_at, finished from todo"
// 接続をオープン
val conn = connection
try {
    // use 関数でクエリ実行(Javaでいうところのtry-with-resources)
    conn.use {
        // setAutoDeriveColumnNames はスネークケース→キャメルケースを自動変換する設定
        val query = conn.createQuery(sql).setAutoDeriveColumnNames(true)
        val ret: MutableList<ToDo> = query.executeAndFetch(ToDo::class.java)
    }
} catch (e: Exception) {
    // do something...
}

3. 更新系処理

更新系処理も、検索系処理同様 createQuery(String) でプリペアードステートメントを作り、 Connection#executeUpdate() で実行する。

val sql = "insert into todo (todo_title, created_at, finished) values (:todoTitle, :createdAt, :finished)"
// トランザクション開始ありのConnectionを取得
val tran = connectionWithTran
try {
    tran.use {
        val stmt = tran.createQuery(sql)
                // プリペアードステートメントへバインドするパラメータを追加していく
                .addParameter("todoTitle", todo.todoTitle)
                .addParameter("createdAt", todo.createdAt)
                .addParameter("finished", todo.finished)
        stmt.executeUpdate()
        // use 関数内で commit()を発行
        tran.commit()
    }
} catch (e: Exception) {
    // do something...
    // ロールバックは暗黙的に行われる
}

なお、sql2oには宣言的トランザクションはないので、トランザクション制御は自分でやらなければならない。

4. 最後に

自力でSQLのチューニングをしたほうが手っ取り早い場合なんかには、こういうライブラリのほうがいいかもしれない。

| | コメント (0)

2019年8月15日 (木)

Apache Wicketを使ってみる

0. 事の始まり

ある筋から「Apache Wicketを学習しといて」とのお達しがきたので自分向けメモ。

1. Apache Wicketとは

公式の記載をざっくり集約するとこんなところ。

  • 2000年代半ばのJava Webアプリケーションフレームワーク戦争の、数少ない生き残りの一つ(2004年に世に出た)
  • Apacheソフトウェア財団のもと、Apache 2.0 Licenseで公開されているOSS
  • Ajax対応機能を内包
  • 多言語対応(デフォルトで25言語を内包)
  • ページやコンポーネントはJavaオブジェクトとして扱える
  • 複数タブ/ウィンドウへの対応
  • ページやコンポーネントのテストのサポート
  • CDIやSpring、GuiceなどのDIをサポート
  • JPA、Bean ValidationなどのJava EE 6サポート
  • 最新安定版のWicket 8では、Java SE 8の言語仕様に対応するため、Java 8とServlet API 3.1が必須に

まぁ、最後のやつは、いまどきならよほどのことがない限りクリアできていると思うけど。
※そのよほどの(ry

2. セットアップ

プロジェクト自体はMavenで管理可能。
※言うまでもないが、Maven自体の扱いについては割愛。

MavenのコマンドラインはQuick Start Wizardで作成できる。

このページで生成されたコマンドラインをターミナルに投入すれば、Mavenが必要なものをそろえてくれるんだが、Windows使いで、PowerShellを使うときには注意が必要。

PS > mvn archetype:generate `
>>   "-DarchetypeGroupId=org.apache.wicket" `
>>   "-DarchetypeArtifactId=wicket-archetype-quickstart" `
>>   "-DarchetypeVersion=8.5.0" `
>>   "-DgroupId=com.mycompany" `
>>   "-DartifactId=myproject" `
>>   "-DarchetypeRepository=https://repository.apache.org/" `
>>   "-DinteractiveMode=false"

といった具合にmvnの -D オプションをダブルクォーテーションで囲わないと、「出力ディレクトリがわからない」とMavenに怒られる。
ハイフンで始まる部分がPowerShellのオプションと解釈されてしまうのが、どうも原因なようだ。なお、DOS窓なら問題なし。

Mavenがプロジェクトを生成したら、mvn clean installで必要ライブラリをダウンロードする。このあたりは、ほかのMavenプロジェクトと同じ扱い。

あとはIDEにMavenプロジェクトとしてインポートすれば、開発体制はとりあえず整う。
必要があれば、愛用のIDEにプラグインを入れてやろう。

3. 開発サーバの起動

生成されたpom.xmlをみると、dependencyブロックにJetty 9が含まれているので、

PS > mvn jetty:run

で localhost:8080 でJettyが起動する。

4. ページの構成など

Strutsなんかに代表されるクラシックなJava Webアプリケーションと違い、ViewとなるHTMLと、処理を受け持つJavaクラス(≒バッキングビーン)が1対1対応していて、ともに同じJavaパッケージ階層に置く必要がある。

このあたり、どちらかといえばASP.NETに近いかも。ただし、CSSなどを駆使して描画する枠線などは、独自タグのあるASP.NETと違い、普通にCSSで定義する。以下、GitHubにあるexampleから引用。

<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:wicket="http://wicket.apache.org">
<head>
  <title>Wicket Examples - sample panel</title>
</head>
<body>
Everything outside of the <wicket:border> tags will be ignored.
Might be handy as preview code.

  <wicket:border>
    <div style="border: 2px dotted #fc0; width: 400px; padding: 5px;">
      before the border contents <br />
    <wicket:body/>
    <br />after the border contents <br />
    </div>
  </wicket:border>

</body>
</html>

JSPとは違い、HTMLにカスタムタグを埋め込む形式なので、普通にプレビューできるのはいいと思う。

サブミットの処理については、バッキングビーンにViewのformに対応するインナークラスを定義して処理するそうな。
以下、こちらもGitHubにある公式のexampleから引用。

public final class GuestBook extends WicketExamplePage {
  /** A global list of all comments from all users across all sessions */
  private static final List<Comment> commentList = new ArrayList<>();

  /**
   * Constructor that is invoked when page is invoked without a session.
   */
  public GuestBook() {
    // Add comment form
    add(new CommentForm("commentForm"));

    // Add commentListView of existing comments
    add(new PropertyListView<>("comments", commentList) {
      @Override
      public void populateItem(final ListItem listItem) {
        listItem.add(new Label("date"));
        listItem.add(new MultiLineLabel("text"));
      }
    }).setVersioned(false);
  }

  /**
   * A form that allows a user to add a comment.
   *
   * @author Jonathan Locke
   */
  public final class CommentForm extends Form<ValueMap> {
    /**
     * Constructor
     *
     * @param id The name of this component
     */
    public CommentForm(final String id) {
      // Construct form with no validation listener
      super(id, new CompoundPropertyModel<>(new ValueMap()));

      // this is just to make the unit test happy
      setMarkupId("commentForm");

      // Add text entry widget
      add(new TextArea<>("text").setType(String.class));

      // Add simple automated spam prevention measure.
      add(new TextField<>("comment").setType(String.class));
    }

    /**
     * Show the resulting valid edit
     */
    @Override
    public final void onSubmit() {
      ValueMap values = getModelObject();

      // check if the honey pot is filled
      final String _comment = (String) values.get("comment");
      if (_comment != null && !_comment.isBlank()) {
        error("Caught a spammer!!!");
        return;
      }
      // Construct a copy of the edited comment
      Comment comment = new Comment();

      // Set date of comment to add
      comment.setDate(new Date());
      comment.setText((String)values.get("text"));
      commentList.add(0, comment);

      // Clear out the text component
      values.put("text", "");
    }
  }

  /**
   * Clears the comments.
   */
  public static void clear() {
    commentList.clear();
  }
}

5. Ajax対応について

サブミット処理の応用。処理を受け付けるクラスの中で、AjaxFallbackLinkで発動用のリンクと紐づけ、AjaxRequestTargetに処理結果を渡す。

このしくみだと、JavaScriptでイベントハンドラをゴリゴリ書かなくてもAjax対応できるのはいいかもしれない。

6. データアクセス

Wicket自体にはデータアクセス用のコンポーネントはないので、Spring Dataなんかと組み合わせる必要がある。

7. 雑感

今回やったところとしてはこんなところ。

  • Viewがあるページには、普通にHTMLのプレビューがきくので、画面の作成はJSPを使うものより効率よくできそう
  • Ajaxをつかった動きのあるページの作成は、比較的簡単に作れそう

認証とテストについては、また日を改めて挑むことにしよう。

| | コメント (0)

2019年7月20日 (土)

続々:BSoDとの戦いに挑む

収束した....のか?

前回前々回のポストからいくつかやってみた結果の結論から言うと、Version 1903をインストールしたら収まった。ような気がする。

それまでにやったこと

ざっくり書くとこんな感じ。

  1. 余計な仮想スイッチの削除
  2. NICのドライバ更新
  3. Windows 10 Version 1903 (Windows 10 May 2019 Update) のインストール

1. 余計な仮想スイッチの削除

OSはPro版なのでHyper-Vが使えるんだけど、Version 1709から「既定のスイッチ(Default Switch)」と呼ばれる仮想スイッチが自動で作成されるようになった。

既定のスイッチについてはQiitaに解説記事が投稿されているのでそちらを参照いただくとして、過去Hyper-Vゲストにインターネットアクセスさせるために仮想スイッチを作っていたことがあり、そいつと機能的に噛んでいるのでは?ということで既定のスイッチだけ残して残りは削除することに。

ちなみにこれ、Hyper-Vの役割をセットアップしていないと削除できないので、Hyper-Vをセットアップして既定のスイッチ以外を削除して再起動。
これでしばらく様子をみることにしたが、一日置いてBSoD発症。

2. NICのドライバ更新

デバイスマネージャーでNIC(有線、無線とも)を見てみると、Windows Update経由でインストールされたものが使用されていた。

そういえば昔(10年以上前の話だが)、両面自動印刷機能とかが付いたHPのカラーインクジェットプリンタをWindows Updateのドライバで動かそうとしたら、片面印刷だけ、かつカラー不可という非常にイケてない状況になったことがあったので、IntelからNICのドライバをダウンロードしてセットアップ。
同様に様子を見ていたら、作業の3日後にBSoD発症。

3. Windows 10 Version 1903 のインストール

バージョン情報をみると、Version 1903公開から2か月以上たっていたにもかかわらずVersion 1803のままだった。たぶん、1903をダウンロードできるだけのまとまった稼働時間を確保していなかったのだろう。

ということで、更新アシスタントからVersion 1903を手動セットアップすることに。

結果、Intel製NICドライバは見事Windows Update版で上書きされてしまったが、実のところVersion 1903をセットアップして以降BSoDは発症していない。こっちの記事でDocker for Windowsを使っていても、である。

はっきりしないうちに収束した感があるので、モヤモヤするものが若干あるけれど。

| | コメント (0)

DockerにSQL Serverを追い出す件

追い出したい

僕のPCには開発用と称して

  • PostgreSQL 9.6
  • MySQL 8
  • Microsoft SQL Server 2017 Developer

をセットアップしているんだが、ホスト環境にそのままセットアップしているとこういった不満が出てくる(という私見)。

  • アップグレードは簡単にしたい
  • DBMSはアプリと1対1対応させたい
  • 開発していないときは、DBMSのプロセスを下げたい

こんなところか。
ということで、手始めにSQL ServerをDockerに追い出してみることにする。

SQL Server on Docker

いまどきのSQL ServerはLinux上でも動かせるエディションがリリースされているので、Microsoftも公式Dockerイメージを公開している。
今回はホスト環境にデータを永続化しておきたいので、Docker Hubの記載をもとにdocker-compose.ymlをかいてみた。

まだホスト環境でSQL Serverが稼働中なので、ホスト環境からの接続ポートを1433から14330に変更している。

version: '3'

 

services:
# Microsoft SQL Server 2017 Linux (Developer Edition)
db:
image: mcr.microsoft.com/mssql/server:2017-latest
container_name: mssql2017dev
environment:
ACCEPT_EULA: Y
# If you runs in production environment, you should change sa password.
SA_PASSWORD: P@ssw0rd2017
# If you have valid Product ID, set MSSQL_PID valiable.
# See : https://hub.docker.com/_/microsoft-mssql-server
#MSSQL_PID: Express
volumes:
- ./data/data:/var/opt/mssql/data
- ./data/log:/var/opt/mssql/log
ports:
- 14330:1433

docker-compose.ymlを置いているディレクトリで、PowerShellからこう叩くとDocker上にSQL Serverが立ち上がる。

PS> mkdir .\data\data
PS> mkdir .\data\log
PS> docker-compose up -d

 

ツールで接続する

 

立ち上がったら、ホスト環境のsqlcmdで接続して動作確認。

PS> sqlcmd -S localhost,14330 -U sa -P P@ssw0rd2017

注意点としては、ポートの指定がよくあるコロン(:)ではなくカンマ(,)だということ。MSDNにはちゃんと書かれているけれど、これは初見殺しだよなぁ。

接続ができたら、今度はSQL Server Management Studioを使えるようにしてみる。接続ダイアログのサーバ名は、sqlcmdの時同様の書き方でよい。

Connwithssms

これでSQL ServerをDockerに追い出せたので、データ移行をこなせば追い出しが完了する見込み。

 

プロジェクト的なもの

GitHubにプロジェクト的なものを上げてます。
ご参考あれ。

» 続きを読む

| | コメント (0)

«続:BSoDとの戦いに挑む