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

カスタムフィールドを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インスペクタなどで眺めてみると良いと思います。