読者です 読者をやめる 読者になる 読者になる

この日記は私的なものであり、所属会社の見解ではありません。 GitHub: takahashikzn

テーブルにカラムがない場合に、エンティティの属性を無かったことにする

EclipseLink

とあるシステムにおいて、「エンティティの属性について、テーブルにカラムがある場合のみマッピングを行う」必要が出てきました。

要するに、

@Entity
public class Sample {

    @Column
    private int foo;

    // テーブルにbarカラムがない場合は何もしない
    @Column
    @OptionalColumn
    private int bar;

    ...
}

のようなエンティティを使いたいということです。

おそらく標準の機能では不可能ですが、EclipseLink限定で次のようにすれば可能です。 list()とかset()は自作のJava言語拡張ライブラリIndolentlyを使用しています。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OptionalColumn {
}


public class OptionalColumnListener
    extends SessionEventAdapter, SessionCustomizer {

    // 自分自身をセッションリスナーに追加
    @Override
    public void customize(final Session session) throws Exception {
        session.getEventManager().addListener(new OptionalColumnListener());
    }

    @Override
    public void postLogin(final SessionEvent event) {

        final Session session = event.getSession();

        try (Connection conn = (Connection) session.getDatasourceLogin().connectToDatasource(null, session)) {

            for (final ClassDescriptor cd : session.getDescriptors().values()) {
                this.removeFields(conn, cd);
            }
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private void removeFields(final Connection conn, final ClassDescriptor cd) throws SQLException {

        // テーブルのカラム一覧
        final SSet<String> columns = this.getColumnNames(conn, cd.getTableName());

        for (final DatabaseMapping mapping : list(cd.getMappings())) {

            final DatabaseField dbField = mapping.getField();

            if (dbField == null) {
                // 1-1や1-N関連で外部キーカラムが自テーブル側に無いケースなので無視
                continue;
            }

            // エンティティの属性名
            final String fieldName = mapping.getAttributeName();
            // テーブルのカラム名
            final String columnName = dbField.getName();

            // エンティティの属性
            final Field field = FieldUtil.getField(cd.getJavaClass(), fieldName);

            // OptionalColumnアノテーションがあり、テーブルにカラムが宣言されていない場合はマッピングを削除する
            if ((field.getAnnotation(OptionalColumn.class) != null) && columns.none(columnName::equalsIgnoreCase)) {
                this.removeField(cd, dbField, fieldName, columnName);
            }
        }
    }

    // 指定したカラムに関係するマッピングを全て削除
    private void removeField(final ClassDescriptor cd, final DatabaseField dbField, final String fieldName,
        final String columnName) {

        cd.removeMappingForAttributeName(fieldName);
        this.removeField(columnName, cd.getFields());
        this.removeField(columnName, cd.getAllFields());
        this.removeField(columnName, cd.getSelectionFields());

        cd.getObjectBuilder().getFieldsMap().remove(dbField);

        // protectedフィールドなので無理やり取得
        final Map<String, DatabaseField> mappingsByAttr =
            cast(FieldUtil.getFieldValue(cd.getObjectBuilder(), "mappingsByAttribute"));

        mappingsByAttr.remove(fieldName);

        System.out.println(String.format("remove: %s.%s -> %s.%s", cd.getJavaClass().getSimpleName(), fieldName, cd.getTableName(),
            columnName));
    }

    private void removeField(final String columnName, final List<? extends DatabaseField> fields) {

        fields.removeAll(list(fields).filter(x -> x.getName().equalsIgnoreCase(columnName)));
    }

    // テーブルのカラム名一覧を取得する
    private SSet<String> getColumnNames(final Connection conn, final String tableName) throws SQLException {

        final SSet<String> columns = set();

        try (ResultSet rs = conn.getMetaData().getColumns(null, null, tableName, null)) {
            while (rs.next()) {
                columns.add(rs.getString("COLUMN_NAME"));
            }
        }

        return columns;
    }
}

恐ろしく不親切な解説ですが、まあわかる人がわかれば良いということで。