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フレームワークでも十分利用できる例が確立しています。