java.sql.Timeとjava.time.LocalTime間の変換でミリ秒が破棄される

java.sql.Timeには、下記のようなLocalTimeとの間で変換を行うメソッドがあります。

  • public LocalTime toLocalTime()
  • Time java.sql.Time.valueOf(LocalTime time)

ただ、これらメソッドは秒までしか対象にしておらず、ミリ秒が破棄されています。

    /**
     * Obtains an instance of {@code Time} from a {@link LocalTime} object
     * with the same hour, minute and second time value as the given
     * {@code LocalTime}.
     *
     * @param time a {@code LocalTime} to convert
     * @return a {@code Time} object
     * @exception NullPointerException if {@code time} is null
     * @since 1.8
     */
    @SuppressWarnings("deprecation")
    public static Time valueOf(LocalTime time) {
        return new Time(time.getHour(), time.getMinute(), time.getSecond());
    }

    /**
     * Converts this {@code Time} object to a {@code LocalTime}.
     * <p>
     * The conversion creates a {@code LocalTime} that represents the same
     * hour, minute, and second time value as this {@code Time}.
     *
     * @return a {@code LocalTime} object representing the same time value
     * @since 1.8
     */
    @SuppressWarnings("deprecation")
    public LocalTime toLocalTime() {
        return LocalTime.of(getHours(), getMinutes(), getSeconds());
    }

面倒ではありますが、LocalDateTimeなどを経由させて変換することによって、ミリ秒を維持したまま変換できます。

SimpleDateFormat timeFormat = new SimpleDateFormat("HH:mm:ss.SSS");
DateTimeFormatter localTimeFormat = DateTimeFormatter.ofPattern("HH:mm:ss.SSS");

LocalTime baseLocalTime = LocalTime.now();

// baseLocalTime: 01:41:44.369
System.out.printf(
        "baseLocalTime: %s\n",
        baseLocalTime.format(localTimeFormat));

{
    // NGなパターン
    Time time = Time.valueOf(baseLocalTime);
    LocalTime localTime = time.toLocalTime();

    // time: 01:41:44.000 localTime: 01:41:44.000 equals: false
    System.out.printf(
            "time: %s localTime: %s equals: %s\n",
            timeFormat.format(time),
            localTime.format(localTimeFormat),
            baseLocalTime.equals(localTime));
}

{
    // OKなパターン
    Time time = new Time(
            baseLocalTime.atDate(LocalDate.ofEpochDay(0)) // LocalDateTimeに変換
                    .atZone(ZoneId.systemDefault()) // ZonedDateTimeに変換
                    .toInstant().toEpochMilli()); // Epochミリ秒へ

    LocalTime localTime = LocalDateTime.ofInstant( // Epochミリ秒からLocalDateTimeへ
            Instant.ofEpochMilli(time.getTime()),
            ZoneId.systemDefault()).toLocalTime();

    // time: 01:41:44.369 localTime: 01:41:44.369 equals: true
    System.out.printf(
            "time: %s localTime: %s equals: %s\n",
            timeFormat.format(time),
            localTime.format(localTimeFormat),
            baseLocalTime.equals(localTime));
}

親のカスタムフィールドが選択されたら、子のカスタムフィールドを有効化する(Redmine View Customize Plugin)

github.com

上記の問い合わせに対応したサンプルコードを書いてみました。

親のカスタムフィールド(キーバリュー形式のリスト)が選択されたら、子のカスタムフィールドを有効化するコードです。

Redmineのデフォルトのスタイルだと、input:disabledbackground-colorが設定されておらず、disabledになったことがわかりずらかったので、明示的にbackground-colorを変えるようにしています。(CSSにしても良かったけど、一つのサンプルコードにまとめたかったので、、)

設定内容

  • Path pattern: .*
  • Insertion position: Bottom of issue form
$(function() {

  const parentField = $('#issue_custom_field_values_1');
  const childFields = [
    $('#issue_custom_field_values_2'),
    $('#issue_custom_field_values_3'),
    $('#issue_custom_field_values_4')
  ];

  const changeEnable = function() {

    if (parentField.val() != '') {
      childFields.forEach(function(child) {
        child.prop('disabled', false);
        child.css('background-color', '');
      });
    } else {
      childFields.forEach(function(child) {
        child.prop('disabled', true);
        child.css('background-color', '#ebebe4');
      });
    }
  }

  parentField.change(changeEnable);

  changeEnable();
})

動作

f:id:onozaty:20200210001354g:plain

RDBMS上でデータの整合性を保つこと

先日Twitter上で外部キーが話題にあがっていました。自分も大昔は外部キーを重要視していませんでしたが、1x年以上たった今では、様々な制約等を使って、RDBMS上でデータの整合性を保つべきと考えています。

なぜ制約を使うのか

データの不整合を、プログラム側で検知するのは難しいです。プログラム側ではデータがこうあるべきといった定義ができないためです。
そこでデータの格納先であるRDBMS側でデータの整合性を保証することが重要です。

プログラムのバグなどで不整合なデータが格納されてしまうと、不整合なデータを格納しようとしたトランザクションだけでなく、システム全体に影響を及ぼすことになりかねません。制約等ではじければ、エラーとなるのは不整合なデータを格納しようとしたトランザクションだけで、システム全体に影響を及ぼすということを防げます。

また、制約として定義することによって、「アプリケーションの仕様的に、こういったデータが格納されることはない」っていうのではなく、スキーマとしてデータの仕様を可視化することができます。

制約の種類

RDBMSはデータの整合性を保つために、様々な制約が用意されています。

  • 主キー制約
  • 外部キー制約
  • 一意性制約
  • 非NULL制約
  • 検査制約

下記はPostgreSQLのマニュアルで各制約の説明をしたページです。他のRDBMSでも同じようなものだと思います。(MySQLでも最近検査(CHECK)制約が追加されましたね!)

検査制約では、関数呼び出しもできるので、複雑なチェック処理をFUNCTIONとして定義しておいて、それを呼びだしてチェックするといったこともできます。

また、一意制約でも関数を使えるので、大文字小文字関係なく一意であるといったような制約も付与できます。

※各制約がどういったものかは後で追記するかも

制約以外の方法

当たり前のことかもしれませんが、データの型を正しく利用するといったことも重要です。たとえば数値を文字列型(VARCHARやTEXT)に格納してしまうと、データとしての整合性を維持する手段を手放してしまっていることになります。

また、数種類の文字列しか入らないようなものは、ENUM(列挙型)を使うことによって、何がはいるのかを限定することができます。種別を数値で入れているようなところも、ENUMにした方がデータとしてわかりやすくなるかもしれません。
ただし、取りえる値が変わるようなものに対して、このような制約を付けるのはメンテナンス性を落とす可能性もあるのでご注意ください。(SQLアンチパターンでも触れられているところで、参照テーブルを用意してそちらとの外部キーで制限した方がよい)

制約を避けることについて

INSERTやUPDATE、DELETE時のパフォーマンスが落ちるから、、といった理由で外部キー制約や検査制約を避けることがあるというのを、ネット上で見かけたことがあります。

実際に制約がパフォーマンス的に問題を起こしているということがわかったならば、制約を外すなり、違った形での制約に変えるなりすればよいと思いますが、最初から避けるのはとてももったいないかな、、と思っています。データの整合性がRDBMS上で保証されているというのは、プログラムを書くうえで大きな安心を得られますし、生産性をあげるものだとも思っています。

カスタムフィールドを3カラムで表示する(Redmine View Customize Plugin)

上記の問い合わせの中で、カスタムフィールドを3カラム表示したいといったのがあったので、サンプルコードを書いてみました。

設定内容

  • Path pattern: .*
  • Insertion position: Bottom of issue form
$(function() {

  const field1 = $('#issue_custom_field_values_1');
  const field2 = $('#issue_custom_field_values_2');
  const field3 = $('#issue_custom_field_values_3');

  // Change layout
  const content = $('<div class="splitcontent">')
    .append($('<div class="splitcontentleft">').append(field1.parent()))
    .append($('<div class="splitcontentleft">').append(field2.parent()))
    .append($('<div class="splitcontentright">').append(field3.parent()));

  $('#attributes').append(content);
})

動作

f:id:onozaty:20200119224923p:plain

カスタムフィールドが大量にあって、少しでも詰めて表示したい、、って時には有用かもしれません。

特定のプロジェクトでファイル添付を無効にする(Redmine View Customize Plugin)

この記事はRedmine Advent Calendar 2019 19日目の記事です。

Redmine Users (Japanese)のメーリングリストで流れていた件で、既にJavaScriptで解決する方法が他の方から出ていましたが、CSSでもできるのでCSSで書いてみました。

設定内容

  • Path pattern: .*
  • Insertion position: Head of all pages

下記はaという識別子のプロジェクトに対して無効とする書き方です。

.project-a #issue-form #attachments_form, .project-a #issue-form .attachments_form {
  display: none;
}

動作

下記のような感じで非表示になります。

f:id:onozaty:20191215224743p:plain

チケットの編集時はちょっと微妙でタイトルまでは消しませんでした。ちょうど良いidやclassが振られていなくて、タイトル含めて消そうとすると #issue-form .filedroplistner fieldset:last-child で指定するくらいしかないのですが、必ず最後になるのか確信がもてなかったのでやめました。

f:id:onozaty:20191215224747p:plain

CSSの使いどころ

JavaScriptの方がいろいろなことができるので、View customizeのサンプルもJavaScriptを使ってものが多くなりますが、CSSの方がシンプルに書けることも多いので、シンプルなデザイン的なもの(今回の非表示のように)は、CSSで書くことが多いです。

プロジェクトの識別子のように、いろんな情報がHTML上に付与されているので、一度DOMインスペクタなどで眺めてみると良いと思います。

諭吉佳作/men

この1か月くらい、家では諭吉佳作/menさんの曲を聴きまくっている。それくらいハマっている。

最初に諭吉佳作/menさんを知ったのは、9月にNHK Eテレで放送された「前山田×体育のワンルーム☆ミュージック」だった。インタビューで、「打ち込みだったら腕が3本なければ叩けないような曲も作れる」(もっと違うニュアンスだったかも)ってとこが印象に残っている。

この時点では、名前は知っている(名前の印象はとても強い)くらいでしかない状態で、特に曲を聴いてみようとはならなかった。この時本人が歌っている曲が流れていたら、きっとこの時点ですぐにハマっていたと思う。
このとき番組で流れていたのは楽曲提供していたでんぱ組.incのMVだった。

そのあと、SpotifyでWeekly Buzzか何か聞いているときに、崎山蒼志くんの「むげん・ (with 諭吉佳作/men)」が流れて、この声って諭吉佳作/menさん!?ってなった。
Spotifyにはほとんど曲がなかったので、YouTube、SoundCloudで曲を聴いた。すごくいい。

曲自体も良いのだけど、そこに声が重なってさらに魅力的なものになっている。この曲とこの声が組み合わさることによる相乗効果というかなんというか。
どれもオリジナリティあふれる楽曲で、ビリー・アイリッシュを初めて聞いた時の感覚に似てる。(ビリー・アイリッシュの曲を初めて聞いた時も、とてもインパクト受けた)

一番のお気に入りは「洋装の語る今日は」で、リズミカルに流れる曲の上でさらに流れるような声、、すごくいい。

soundcloud.com

ちなみに、うちの娘たちは「の ぞ き」がお気に入りで、よく口ずさむようになった。

移動中などにも聞きたいので、Spotifyで配信(課金してるので、ネット接続なくても聴ける)するか、アルバム出して欲しいです。もしくはダウンロード販売でも。
同じように思っている人は既にいっぱいいそうなので、すぐ近い将来そうなるんだろうな。とても楽しみ。

プロジェクト切り替え時にウォッチャーを変更する(Redmine View Customize Plugin)

プロジェクト切り替え時にウォッチャーを変えたいのだけどうまくいかないといった問い合わせもらったので、サンプルコード書いてみました。

入力フォーム差し替えと絡んでハマりそうな箇所なので、他の方の参考にもなればと思います。

入力フォームの差し替え

プロジェクトやステータス、トラッカーを切り替えた際には、入力フォーム自体が差し替えられます。サーバ側を呼び出して、受け取ったHTMLの断片で差し替えるようなイメージです。

チケットの題名や説明、カスタムフィールドなどは、空の入力フォームを作った後に、もともと設定されていた情報に移し替えています。
ただ、ウォッチャーのところは、チェックの付与情報自体が付いたHTMLがサーバ側からかえってきて、それをそのまま差し替えるだけです。(サーバ呼び出しの際にフォームの情報も送っている)
そのため、サーバの呼び出しが行われた後に画面側の情報を変えてもそれが反映されません。

ということで、サーバ側呼び出しの前に画面側を変えてあげる必要がありますが、そうなるとイベントのキャプチャフェーズで拾ってあげる必要があります。jQueryではキャプチャフェーズを指定できないので、addEventListenerで指定する形となります。

設定内容

  • Path pattern: /issues/
  • Insertion position: Head of all pages
$(function() {

  const checkWatcher = function(userId) {
    if ($('#issue_watcher_user_ids_' + userId).length == 0) {
      // When the number of users exceeds 20, the check box is not initially displayed, so add a check box.
      const label = $('<label>').attr({
        id: 'issue_watcher_user_ids_' + userId,
        class: 'floating'
      }).append(
        $('<input>').attr({
          type: 'checkbox',
          name: 'issue[watcher_user_ids][]',
          value: userId
        })
      );

      $('#watchers_inputs').append(label);
    }
    
    $('#issue_watcher_user_ids_' + userId + ' input').prop('checked', true);
  }

  const changeWatcher = function() {

    // reset all watchers
    $('input[name="issue[watcher_user_ids][]"]').prop('checked', false);

    switch($('#issue_project_id').val()) {
      case '1':
        checkWatcher(1);
        break;

      case '2':
        checkWatcher(5);
        break;

      case '3':
        checkWatcher(6);
        break;
    }
  };

  const allAttributes = document.getElementById('all_attributes');
  if (!allAttributes) {
    return;
  }

  allAttributes.addEventListener(
    'change',
    function(e) {
      if (e.target.id == 'issue_project_id') {
        changeWatcher();
      }
    },
    // Capturing phase (To work before updateIssueFrom is executed)
    true);
});

追記@2022-12-25

ユーザ数が20を超えていた場合の考慮を追加しました。
20を超えると、ウォッチャーをチェックするためのチェックボックスが未選択のものだと表示されないため、うまく動きませんでした。