1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MicronautでActiveObjectsを利用した軽量ORM実装

Last updated at Posted at 2025-04-13

ActiveObjectsとは?

ActiveObjectsは、Atlassian社が提供するJava向けの軽量なオブジェクト関係マッピング(ORM)ライブラリです。主にJiraやConfluenceなどのプラグイン開発向けに設計されており、次のような特徴があります:

  • Entityはインターフェースで定義し、コードが簡潔
  • **DDLの自動生成(migrate)**により、テーブル作成が容易
  • クラス定義ベースでリレーションが構築でき、@OneToMany@ManyToMany もサポート
  • シンプルなAPIでデータアクセスやトランザクションが可能

利点

  • JPAやHibernateに比べて構成が軽量で、学習コストも低い
  • JAR単体で動作可能なため、外部依存を極力抑えたいアプリに向いている
  • MicronautやSpringと併用することで、モダンなDIとも親和性を持たせられる

概要

Atlassian製品(JiraやConfluence)のプラグイン開発で知られる軽量ORM「ActiveObjects」を、Micronautと連携させた例はほぼ例を見ません。ここでは、MicronautのDI、サービス検索機能に、ActiveObjectsのEntityManagerを利用した構成を簡単に紹介します。

環境

  • Java 17
  • Micronaut 4.8.0
  • MariaDB
  • ActiveObjects 6.2.0-1ca9fca38
  • HikariCP

Maven設定(pom.xml 抜粋)

<repositories>
    <repository>
        <id>atlassian-public</id>
        <url>https://packages.atlassian.com/mvn/maven-atlassian-external/</url>
    </repository>
</repositories>

<dependencies>
    <!-- Micronaut + Servlet環境 -->
    <dependency>
        <groupId>io.micronaut</groupId>
        <artifactId>micronaut-http-server</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micronaut.servlet</groupId>
        <artifactId>micronaut-http-server-tomcat</artifactId>
    </dependency>
    <dependency>
        <groupId>io.micronaut.sql</groupId>
        <artifactId>micronaut-jdbc-hikari</artifactId>
    </dependency>
    <dependency>
        <groupId>org.mariadb.jdbc</groupId>
        <artifactId>mariadb-java-client</artifactId>
    </dependency>

    <!-- ActiveObjects本体 -->
    <dependency>
        <groupId>com.atlassian.activeobjects</groupId>
        <artifactId>activeobjects-core</artifactId>
        <version>6.2.0-1ca9fca38</version>
    </dependency>

    <!-- 依存する補助ライブラリ -->
    <dependency>
        <groupId>com.atlassian.plugins</groupId>
        <artifactId>atlassian-plugins-core</artifactId>
        <version>9.0.0-jakarta-m002</version>
    </dependency>
    <dependency>
        <groupId>com.atlassian.sal</groupId>
        <artifactId>sal-api</artifactId>
        <version>7.0.0-jakarta-001</version>
        <scope>provided</scope>
    </dependency>
    <dependency>
        <groupId>com.atlassian.profiling</groupId>
        <artifactId>atlassian-profiling</artifactId>
        <version>4.8.5-f8c7987</version>
    </dependency>
</dependencies>

構成概要

1. EntityManagerをラップした ActiveObjects の実装

package jp.livlog.kotonoba.ao;

import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.activeobjects.external.ActiveObjectsModuleMetaData;
import com.atlassian.activeobjects.external.FailedFastCountException;
import com.atlassian.sal.api.transaction.TransactionCallback;

import net.java.ao.DBParam;
import net.java.ao.EntityManager;
import net.java.ao.EntityStreamCallback;
import net.java.ao.Query;
import net.java.ao.RawEntity;

public class SimpleActiveObjects implements ActiveObjects {

    private final EntityManager em;

    public SimpleActiveObjects(final EntityManager em) {

        this.em = em;
    }


    @SuppressWarnings ("unchecked")
    @Override
    public void migrate(final Class <? extends RawEntity <?>>... entities) {

        try {
            this.em.migrate(entities);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @SuppressWarnings ("unchecked")
    @Override
    public void migrateDestructively(final Class <? extends RawEntity <?>>... entities) {

        try {
            this.em.migrateDestructively(entities);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @SuppressWarnings ("deprecation")
    @Override
    public void flushAll() {

        this.em.flushAll();
    }


    @SuppressWarnings ("deprecation")
    @Override
    public void flush(final RawEntity <?>... entities) {

        this.em.flush(entities);
    }


    @SuppressWarnings ("unchecked")
    @Override
    public <T extends RawEntity <K>, K> T[] get(final Class <T> type, final K... keys) {

        try {
            return this.em.get(type, keys);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T get(final Class <T> type, final K key) {

        try {
            return this.em.get(type, key);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T create(final Class <T> type, final DBParam... params) {

        try {
            return this.em.create(type, params);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T create(final Class <T> type, final Map <String, Object> params) {

        try {
            return this.em.create(type, params);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> void create(final Class <T> type, final List <Map <String, Object>> rows) {

        try {
            this.em.create(type, rows);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public void delete(final RawEntity <?>... entities) {

        try {
            this.em.delete(entities);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <K> int deleteWithSQL(final Class <? extends RawEntity <K>> type, final String criteria, final Object... parameters) {

        try {
            return this.em.deleteWithSQL(type, criteria, parameters);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T[] find(final Class <T> type) {

        try {
            return this.em.find(type);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T[] find(final Class <T> type, final String criteria, final Object... parameters) {

        try {
            return this.em.find(type, criteria, parameters);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T[] find(final Class <T> type, final Query query) {

        try {
            return this.em.find(type, query);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T[] find(final Class <T> type, final String field, final Query query) {

        try {
            return this.em.find(type, field, query);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> T[] findWithSQL(final Class <T> type, final String keyField, final String sql, final Object... parameters) {

        try {
            return this.em.findWithSQL(type, keyField, sql, parameters);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> void stream(final Class <T> type, final EntityStreamCallback <T, K> streamCallback) {

        try {
            this.em.stream(type, streamCallback);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T extends RawEntity <K>, K> void stream(final Class <T> type, final Query query, final EntityStreamCallback <T, K> streamCallback) {

        try {
            this.em.stream(type, query, streamCallback);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <K> int count(final Class <? extends RawEntity <K>> type) {

        try {
            return this.em.count(type);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <K> int count(final Class <? extends RawEntity <K>> type, final String criteria, final Object... parameters) {

        try {
            return this.em.count(type, criteria, parameters);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <K> int count(final Class <? extends RawEntity <K>> type, final Query query) {

        try {
            return this.em.count(type, query);
        } catch (final SQLException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <K> int getFastCountEstimate(final Class <? extends RawEntity <K>> type) throws SQLException, FailedFastCountException {

        try {
            return this.em.getFastCountEstimate(type);
        } catch (final SQLException | net.java.ao.FailedFastCountException e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public <T> T executeInTransaction(final TransactionCallback <T> callback) {

        return ((ActiveObjects) this.em).executeInTransaction(callback);
    }


    @Override
    public ActiveObjectsModuleMetaData moduleMetaData() {

        throw new UnsupportedOperationException("moduleMetaData() is not supported in SimpleActiveObjects");
    }
}

2. MicronautのファクトリーでDI用Bean生成

package jp.livlog.kotonoba.config;

import javax.sql.DataSource;

import jakarta.inject.Singleton;

import com.zaxxer.hikari.HikariDataSource;

import io.micronaut.context.annotation.Factory;
import jp.livlog.kotonoba.ao.SimpleActiveObjects;
import net.java.ao.builder.EntityManagerBuilderWithUrlAndUsernameAndPassword;

@Factory
public class  ActiveObjectsFactory {

    @Singleton
    public SimpleActiveObjects activeObjects(final DataSource dataSource) {

        final var hikariDataSource = (HikariDataSource) dataSource;
        final var entityManager = new EntityManagerBuilderWithUrlAndUsernameAndPassword(
                hikariDataSource.getJdbcUrl(),
                hikariDataSource.getUsername(),
                hikariDataSource.getPassword())
                        .auto() // 利用可能なConnectionPoolを自動選択
                        .build(); // EntityManagerを構築

        return new SimpleActiveObjects(entityManager);
    }
}

3. Entityの定義

package jp.livlog.kotonoba.ao.entity;

import net.java.ao.Accessor;
import net.java.ao.Entity;
import net.java.ao.Mutator;
import net.java.ao.Preload;
import net.java.ao.schema.Table;

@Preload
@Table("custom_address_view")
public interface CustomAddressViewEntity extends Entity {

    @Accessor("NAME")
    String getName();
    @Mutator("NAME")
    void setName(String name);

    @Accessor("KANA")
    String getKana();
    @Mutator("KANA")
    void setKana(String kana);

    @Accessor("ROMAN")
    String getRoman();
    @Mutator("ROMAN")
    void setRoman(String roman);

    @Accessor("LONGITUDE")
    double getLongitude();
    @Mutator("LONGITUDE")
    void setLongitude(double longitude);

    @Accessor("LATITUDE")
    double getLatitude();
    @Mutator("LATITUDE")
    void setLatitude(double latitude);

    @Accessor("NEW_OLD_FLG")
    String getNewOldFlg();
    @Mutator("NEW_OLD_FLG")
    void setNewOldFlg(String newOldFlg);

    @Accessor("PREF_CD")
    String getPrefCd();
    @Mutator("PREF_CD")
    void setPrefCd(String prefCd);

    @Accessor("PREF_NAME")
    String getPrefName();
    @Mutator("PREF_NAME")
    void setPrefName(String prefName);

    @Accessor("PREF_KANA")
    String getPrefKana();
    @Mutator("PREF_KANA")
    void setPrefKana(String prefKana);

    @Accessor("CITY_CD")
    String getCityCd();
    @Mutator("CITY_CD")
    void setCityCd(String cityCd);

    @Accessor("CITY_NAME")
    String getCityName();
    @Mutator("CITY_NAME")
    void setCityName(String cityName);

    @Accessor("CITY_KANA")
    String getCityKana();
    @Mutator("CITY_KANA")
    void setCityKana(String cityKana);

    @Accessor("TOWN_CD")
    String getTownCd();
    @Mutator("TOWN_CD")
    void setTownCd(String townCd);

    @Accessor("TOWN_NAME")
    String getTownName();
    @Mutator("TOWN_NAME")
    void setTownName(String townName);

    @Accessor("TOWN_KANA")
    String getTownKana();
    @Mutator("TOWN_KANA")
    void setTownKana(String townKana);

    @Accessor("STREET_CD")
    String getStreetCd();
    @Mutator("STREET_CD")
    void setStreetCd(String streetCd);

    @Accessor("STREET_NAME")
    String getStreetName();
    @Mutator("STREET_NAME")
    void setStreetName(String streetName);

    @Accessor("STREET_KANA")
    String getStreetKana();
    @Mutator("STREET_KANA")
    void setStreetKana(String streetKana);

    @Accessor("LATITUDE2")
    double getLatitude2();
    @Mutator("LATITUDE2")
    void setLatitude2(double latitude2);

    @Accessor("LONGITUDE2")
    double getLongitude2();
    @Mutator("LONGITUDE2")
    void setLongitude2(double longitude2);
}

4. 検索用サービス

package jp.livlog.kotonoba.ao.service;

import java.util.List;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import com.atlassian.activeobjects.external.ActiveObjects;

import jp.livlog.kotonoba.ao.entity.CustomAddressViewEntity;
import net.java.ao.Query;

@Singleton
public class CustomAddressService {

    private final ActiveObjects ao;

    @Inject
    public CustomAddressService(final ActiveObjects ao) {

        this.ao = ao;
    }


    public List <CustomAddressViewEntity> findByWord(final String prefCd, String word, final int search) {

        String condition;
        word = switch (search) {
            case 1: // 前方一致
            {
                condition = "NAME LIKE ? COLLATE utf8mb4_unicode_ci";
                yield word + "%";
            }
            case 2: // 後方一致
            {
                condition = "NAME LIKE ? COLLATE utf8mb4_unicode_ci";
                yield "%" + word;
            }
            default: // 部分一致
            {
                condition = "NAME LIKE ? COLLATE utf8mb4_unicode_ci";
                yield "%" + word + "%";
            }
        };

        if (prefCd != null && !prefCd.trim().isEmpty()) {
            condition += " AND PREF_CD = ?";
            return List.of(this.ao.find(CustomAddressViewEntity.class, Query.select().where(condition, word, prefCd)));
        }
        return List.of(this.ao.find(CustomAddressViewEntity.class, Query.select().where(condition, word)));
    }


    public List <CustomAddressViewEntity> findByYomi(final String prefCd, String yomi, final int search) {

        String condition;
        yomi = switch (search) {
            case 1 -> {
                condition = "KANA LIKE ? COLLATE utf8mb4_unicode_ci";
                yield yomi + "%";
            }
            case 2 -> {
                condition = "KANA LIKE ? COLLATE utf8mb4_unicode_ci";
                yield "%" + yomi;
            }
            default -> {
                condition = "KANA LIKE ? COLLATE utf8mb4_unicode_ci";
                yield "%" + yomi + "%";
            }
        };

        if (prefCd != null && !prefCd.trim().isEmpty()) {
            condition += " AND PREF_CD = ?";
            return List.of(this.ao.find(CustomAddressViewEntity.class, Query.select().where(condition, yomi, prefCd)));
        }
        return List.of(this.ao.find(CustomAddressViewEntity.class, Query.select().where(condition, yomi)));
    }


    public List <CustomAddressViewEntity> findByWordZenKoku(final String word) {

        final var condition = "MATCH(NAME) AGAINST(? IN BOOLEAN MODE)";
        return List.of(this.ao.find(CustomAddressViewEntity.class, Query.select().where(condition, "+" + word)));
    }


    public List <CustomAddressViewEntity> findByYomiZenKoku(final String yomi) {

        final var condition = "MATCH(KANA) AGAINST(? IN BOOLEAN MODE)";
        return List.of(this.ao.find(CustomAddressViewEntity.class, Query.select().where(condition, "+" + yomi)));
    }
}

特徴

  • 非Atlassian環境でActiveObjectsを利用
  • Entity定義はインターフェース形式
  • MariaDBの全文検索を直接利用
  • Micronaut DI/ファクトリーと自然に連携

ActiveObjectsは本来 Atlassian製品向けのニッチなORMですが、このように現代的なJavaフレームワークでも十分利用できる例が確立しています。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?