ShortcutKey2URLの新バージョン(Chrome版1.4.0、Firefox版4.3.1)をリリースしました

ShortcutKey2URLの新バージョンをリリースしました。

2つの機能追加を行っています。どちらも利用者の方から要望があったものになります。

候補のインタラクティブな絞り込み

ショートカットキーの候補をインタラクティブに絞り込んで表示できるようにしました。

f:id:onozaty:20200329210943g:plain

たとえば、"G1" "G2" といったショートカットキーが登録されていた場合、"G"の時点で候補となるショートカットキーのみに絞って表示されるようなイメージです。
ショートカットキーがたくさん登録されている場合に有効かと思います。

この機能を有効にするためには、ShortcutKey2URLの管理画面で「Interactive filter of shortcut keys on the popup」にチェックを付ける必要があります。

ポップアップ上で非表示に

ショートカットキーの設定で「Hide in shortcut key list displayed in popup」にチェックを付けると、ポップアップ上でショートカットキーを非表示とできるようにしました。
一覧に出ないだけで、ショートカットキーとしては有効なままです。

人に見られたくないショートカットキーなどは、非表示にしておくと良いかもしれませんね。

期日近くになったら警告を表示する(Redmine View Customize Plugin)

上記の問い合わせに対応したサンプルコードを書いてみました。(これが求めているものなのかはちょっと怪しいかも...)

期日まで残り3日になったらチケット画面に警告を表示します。

設定内容

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

  const daysLeft = 3;

  const dueDateArray = $('#issue_due_date').val().split('-');
  const alertDate = new Date(dueDateArray[0], dueDateArray[1] - 1, dueDateArray[2] - daysLeft);

  const now = new Date();
  const nowDate = new Date(now.getFullYear(), now.getMonth(), now.getDate());

  if (nowDate >= alertDate) {
    $('#content').prepend('<div class="warning">It is only ' + daysLeft + ' days until the due date.</div>');
  }
})

動作

f:id:onozaty:20200327233745p:plain

PostgreSQL COPY Helper を作りました

PostgreSQLのCOPYコマンドをJavaで簡単に利用するためのライブラリとして、PostgreSQL COPY Helper を作りました。

登録したいデータの構造を表すクラスを定義しておいて、、

@Table("items")
public class Item {

    @Column("id")
    private final int id;

    @Column("name")
    private final String name;

    public Item(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public int getId() {
        return id;
    }

    public String getName() {
        return name;
    }
}

CopyHelper.copyFrom でそのオブジェクトの一覧を渡すだけです。

List<Item> items = generateItems();
CopyHelper.copyFrom(connection, items, Item.class);

COPYコマンドは、Batch INSERTと比べてもかなり(手元で測定したところ10倍くらい)高速なので、大量データのINSERTを高速化したいというときにぜひお試しください。

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上で保証されているというのは、プログラムを書くうえで大きな安心を得られますし、生産性をあげるものだとも思っています。