728x90
반응형
SMALL

거의 모든 애플리케이션은 데이터가 필요하다. 데이터를 저장하고, 읽고, 쓰는 작업이 안들어가는 애플리케이션은 없을것이다.

JIRA DC 플러그인도 마찬가지로 데이터가 필요한데 이 데이터를 다루기 위해 JIRA 플러그인에서는 AO(ActiveObjects)를 공식적으로 사용한다.

 

그래서 이 AO를 사용하는 방법을 알아본다. 우선, 라이브러리를 내려받아야 한다.

<dependency>
    <groupId>com.atlassian.activeobjects</groupId>
    <artifactId>activeobjects-plugin</artifactId>
    <version>${ao.version}</version>
    <scope>provided</scope>
</dependency>

 

내가 지정한 ao.version은 다음과 같다.

<properties>
    ...
    <ao.version>6.0.0-m03</ao.version>
    ...
</properties>

 

이렇게 라이브러리를 내려받았으면 이제 Entity를 만들어야 한다. 이 AO는 방식이 좀 신기하게 되어있다.

 

KapprovalUserEntity

package kr.osci.kapproval.com.entity;

import net.java.ao.Entity;
import net.java.ao.schema.Indexed;
import net.java.ao.schema.NotNull;
import net.java.ao.schema.StringLength;
import net.java.ao.schema.Table;

@Table("KAPP_USER")
public interface KapprovalUserEntity extends Entity {

      @NotNull
      @Indexed
      Long getUserId();
      void setUserId(Long userId);

      Integer getOrgId();
      void setOrgId(Integer orgId);

      Integer getPositionId();
      void setPositionId(Integer positionId);

      @StringLength(1)
      String getSuperUserYn();
      void setSuperUserYn(String superUserYn);

}

우선 인터페이스를 하나 만들고 net.java.ao.Entity를 상속받아야 한다. 그리고 각 엔티티에 필요한 필드들은 Getter, Setter를 만듦으로써 생성된다. 그리고 Primary Keynet.java.ao.Entity 안으로 들어가보면 Primary Key가 이미 선언이 되어있다. 

 

net.java.ao.Entity

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package net.java.ao;

import net.java.ao.schema.AutoIncrement;
import net.java.ao.schema.NotNull;
import net.java.ao.schema.PrimaryKey;

public interface Entity extends RawEntity<Integer> {
    @AutoIncrement
    @NotNull
    @PrimaryKey("ID")
    int getID();
}

그래서 기본키는 따로 만들지 않아도 된다. Entity를 상속받기 때문에!

엔티티끼리 Relationship도 생성할 수 있다. 그 부분은 이 공식 문서를 참조하자.

 

Developing your plugin with Active Objects

Last updated Jul 12, 2024

developer.atlassian.com

저기서 @Table 애노테이션은 이 엔티티에 대한 테이블명을 명시해주는 방법이다.

저렇게 @Table("KAPP_USER")로 애노테이션을 달면 데이터베이스에서 테이블 명은 이렇게 된다.

AO_28BE2D_KAPP_USER

그리고 @Table 애노테이션으로 테이블 명을 명시하지 않으면 테이블 명은 기본이 클래스명을 따라간다.

 

그래서 이렇게 테이블을 만들면, 이 AO 인터페이스로 만든 테이블을 atlassian-plugin.xml 파일에 등록해야 한다.

<ao key="ao-module">
    <description>The module configuring the Active Objects service used by this plugin</description>
    ...
    <entity>kr.osci.kapproval.com.entity.KapprovalUserEntity</entity>
    ...
</ao>

이렇게 등록을 하고 나서 다음 명령어로 서버를 실행해보자.

atlas-run

 

띄워진 서버에서 알려준대로 `localhost:2990/jira`로 접속해보면 띄워진 지라 서버가 보여질텐데 거기에 DbConsole 버튼을 클릭해보자.

그럼 기본으로 연동된 H2 데이터베이스의 콘솔이 노출된다.

테이블 목록을 쭉 보면 내가 등록한 테이블이 보여진다.

그럼 테이블은 정상적으로 만들어졌으니 이제 CRUD에 대한 작업을 해보자.

Create

기본적으로 ORM이면 CRUD에 대한 메서드가 이미 있다. 솔직히 AO는 너무 불편하고 부실하지만, 이게 Atlassian에서 제공하는 ORM이기 때문에 사용해야 한다. 다른 ORM을 사용할 수 있는지 계속해서 알아보는 중인데 쉽지 않다. 새로운 사실을 알게 되면 업데이트 해야겠다! 

 

우선 Create 작업이 필요한 서비스 내에서 ActiveObjects 객체를 주입받자.

KapprovalSettingsServiceImpl

package kr.osci.kapproval.admin.service.impl;

import com.atlassian.activeobjects.external.ActiveObjects;
import com.atlassian.plugin.spring.scanner.annotation.imports.ComponentImport;
import kr.osci.kapproval.admin.service.KapprovalSettingsService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class KapprovalSettingsServiceImpl implements KapprovalSettingsService {

    @ComponentImport
    private final ActiveObjects activeObjects;

}

 

그리고 이 activeObjects가 가지고 있는 메서드를 쭉 살펴보면 다음과 같이 create()가 있다.

첫번째 메서드 타입을 선택하면 된다. DBParam 객체를 받아 생성하는 메서드.

KapprovalSettingsEntity entity = activeObjects.create(KapprovalSettingsEntity.class
                    ,new DBParam("LINE_TYPE", "1")
                    ,new DBParam("PASSWORD_USE_YN", "Y")

이런식으로 작성하면 된다. 보면 2번째 파라미터의 타입이 DBParam... 이기 때문에 계속해서 추가적으로 넣어주는게 가능하다.

그리고 DBParam()에는 컬럼명과 값이 들어간다. 여기서 컬럼명이 어떻게 저렇게 되냐? 만약 아래와 같은 엔티티가 있다면,

@Table("KAPP_USER")
public interface KapprovalUserEntity extends Entity {

      @NotNull
      @Indexed
      Long getUserId();
      void setUserId(Long userId);

}

각 대문자 사이에 `_`가 들어간다고 보면 된다. `getUserId` -> `USER_ID`

그래서 각 컬럼에 원하는 값을 DBParam 객체로 하나씩 넣어주면 된다. 물론 위에 오버로딩된 메서드 시그니쳐를 보면 알 수 있듯 Map을 사용하거나 MapList를 사용해도 된다. 

 

Read

한개의 레코드를 찾을땐 get() 메서드를 사용하면 된다.

KapprovalOrgEntity kapprovalOrgEntity = activeObjects.get(KapprovalOrgEntity.class,id);

 

두번째 인자엔 해당 엔티티에 대한 PK를 집어넣으면 된다. 그리고 이 get()메서드도 여러 PK를 넣으면 그 PK에 해당하는 모든 레코드를 가져온다. 다음 사진을 참고해보자. 넣은 PK만큼 배열로 리턴하고 있는것을 확인할 수 있다.

 

그리고 여러개의 레코드를 찾을땐 위 사진과 같이 get()을 사용해도 되지만, 일반적으로 find()를 사용한다.

다음 코드를 보자.

KapprovalUserEntity[] arrKapprovalUserEntity =
                activeObjects.find(KapprovalUserEntity.class, "USER_ID = ? ", jiraUser.getId());

첫번째 인자는 엔티티 타입이다. 어떤 엔티티로부터 조회할건지에 대한 정보.

두번째 인자는 특정 조건이다. 위 코드와 같이 특정 컬럼(USER_ID)값에 대한 조건을 건다.

세번째 인자는 두번째 인자에서 사용되는 Variable에 대한 값이다.

 

다른 방식을 사용할수도 있다. 아예 Query 라는 객체가 있는데, 생김새가 QueryDSL과 유사하게 생겼다.

activeObjects.find(KapprovalApprovalPathMappingEntity.class,
                Query
                        .select()
                        .where("APPROVAL_PATH_ID = ? AND PROJECT_ID = ? AND ISSUE_TYPE_ID = ? AND APPLY_STATUS_ID = ? AND PROCESS_ACTION_ID = ? AND REJECTED_ACTION_ID = ?",
                                approvalPathId,
                                projectId,
                                issueTypeId,
                                applyStatusId,
                                processActionId,
                                rejectedActionId))

그래서 조금 더 복잡한 쿼리에 대해서 아예 객체로 쿼리를 만들어낸다. 

어떤 방법을 사용해도 상관없다. 그리고 어떠한 조건도 넣지 않는다면 그냥 `SELECT * FROM TABLE_NAME`이랑 똑같다.

activeObjects.find(KapprovalApprovalPathMappingEntity.class)
참고로, 아주 많은 양의 데이터를 읽어 들일땐 find(), get() 말고 stream()을 사용하라고 나와있다. 일단 아주 많은 양의 데이터를 읽는다는 것은 업데이트와 거리가 멀다. 정말 읽어들이기 위한 (Read-Only) 데이터를 찾는 경우가 대다수이다. 이럴땐 Stream API를 사용하라고 공식 문서에 표기되어 있다. 

 

만약, 간단한 쿼리가 아닌 복잡한 쿼리가 필요하다면, findWithSQL()을 사용하자.

String sql = "SELECT id, name, value FROM my_table WHERE column1 = ? AND column2 = ?";
Object[] params = new Object[] {value1, value2};
String keyField = "id"; // 쿼리 결과의 'id' 컬럼을 키로 사용

MyEntity[] results = ao.findWithSQL(MyEntity.class, keyField, sql, params);
return results;

여기서 keyField라는 파라미터가 사용되는데 이는 SELECT 절에 사용되는 컬럼 중 임의의 것을 사용해도 되지만 일반적으로 그리고 권장되는 것은 PK를 사용하는 것이다. 이 keyField를 통해 AO가 쿼리로부터 받아오는 결과를 적절하게 매핑할 수 있기 때문에 사용한다고 한다. 그리고 keyField로 사용되는 값은 반드시 SELECT절로 받아오는 컬럼 중 하나여야 한다.

Update

업데이트는 읽어서 쓴다. 즉, 먼저 업데이트 할 객체를 가져와서 그 객체를 변경한 후에 저장하는 방식으로 수행한다.

예를 들어서 이렇게 사용하면 된다.

KapprovalPositionEntity entity = activeObjects.get(KapprovalPositionEntity.class, id);

entity.setXxx(...);

entity.save();

읽고, 변경하고, 저장한다. 오히려 이 방식이 더 좋을수도 있다. 딱 필요한 것만 업데이트 친 후에 변경하기 때문에.

JPA를 써봤던 사람들은 변경감지 개념과 유사하다고 볼 수 있다. 물론 save()라는 메서드를 따로 호출하지 않아도 영속시켰다면 저절로 변경 감지가 일어나서 쿼리가 날라가지만! 

 

Delete

이제 AO는 Delete가 불편하다. 왜냐하면, AO는 Cascade를 지원하지 않는다.

그에 대한 내용은 다음 문서에 있다.

 

Best practices for developing with Active Objects

Best practices for developing with Active Objects This page contains some guidelines for best practices when developing a plugin that uses the Atlassian Active Objects (AO) plugin to store and retrieve data. The information takes the form of a quick refere

developer.atlassian.com

그래서 연관 레코드가 있다면 적절한 순서에 맞춰 먼저 그들을 삭제해 준 후에 원하는 레코드를 삭제해야 한다.

메서드는 간단하다.

activeObjects.delete(Your_Entity);

이는 단일 객체를 삭제하는 방식인데 여러 객체를 삭제해야 한다면 또는 복잡한 쿼리가 필요하다면 deleteWithSQL()을 사용하자.

activeObjects.deleteWithSQL(YourEntity.class, "SPECIFIC_COLUMN_ID = ?", id);

 

 

참고 자료

다른 데이터베이스로 연결하는 방법에 대한 참조 3개:

https://community.developer.atlassian.com/t/how-to-config-atlassian-sdks-jira-to-use-custom-jdbc-database-instead-of-h2/25457/2

 

How to config Atlassian-SDK's Jira to use custom JDBC database instead of H2

Hello @hy.duc.nguyen, The way I did it is to use maven-amps-plugin in my POM and declaring the datasource there. In a nutshell, I did it like this (for mySql but should be similar with Postgre) <plugin> <groupId>com.atlassian.maven.plugins</groupId> <artif

community.developer.atlassian.com

https://developer.atlassian.com/server/framework/atlassian-sdk/declaring-jndi-datasources-in-amps/#declaring-jndi-datasources-in-amps

 

Declaring JNDI datasources in AMPS

Last updated Jul 12, 2024

developer.atlassian.com

https://bitbucket.org/aragot/amps-examples/src/master/start-jira-with-datasource/

 

Bitbucket

 

bitbucket.org

 

728x90
반응형
LIST

+ Recent posts