技術者向けテクニカルtips
 / 

Liferay 7のServiceBuilderと外部データベース

Liferay 7のServiceBuilderにおける、外部データベースの利用方法をご紹介。

はじめに

こんにちは。日本ライフレイ、サポータビリティエンジニアの竹生(たけお)です。

今回は、Liferay 7のServiceBuilderで、外部データベースを使用する方法をまとめた記事をご紹介します。

Liferay 7のServiceBuilderと外部データベース


※本記事はLiferay Community Blogに投稿されている”Liferay 7 Notifications”を翻訳したものです。配信元または著者の許可を得て配信しています。
※オリジナルの英記事著者:David H Nebinger サウスカロライナ州のサマーヴィルに住むLiferay, Inc.のSoftware Architect/リードコンサルタント。Liferay Communityサイトにて、多くの技術情報を投稿するなど精力的に活動しています。

私は長年ServiceBuilderの利用を推奨してきました。Liferay バージョン4や5当時の、このツールの目的を見返したこともあり、本ブログで今一度紹介したいと思います。

Liferay7のリリースで、ServiceBuilderは主にOSGiモジュール化に関連するいくつかの変更を受けました。 ServiceBuilderはAPIモジュール(インタフェースを備えた古いサービスjarに相当する)と、サービスモジュール(ポートレットの一部であったサービス実装に匹敵する)の、2つのモジュールを作成するようになりました。

しかしその中核(コア)では、これまでと同じ点もたくさんあります。 service.xmlファイルでは、生成されたコードを再構築するための "buildService"(gradleを使用)のすべてのエンティティが定義され、コンシューマは引き続きAPIモジュールを使用し、実装はサービスモジュールにカプセル化されます。生成されたコードとLiferay ServiceBuilderフレームワークは、Hibernateの上に構築されるため、同じSpringおよびHibernateファセットのすべてが引き続き適用されます。カスタムSQL、DynamicQuery、カスタム・ファインダー、さらには外部データベースのサポートなど、過去に使用されたすべての機能もサポートされています。
外部データベースのサポートはServiceBuilderにはまだ含まれていますが、Liferay 7で動作させるために必要ないくつかの制限と、セットアップの条件があります。

以下の例は、プロセスを処理するのに適した方法です。Liferayデータベースとは別のHSQLデータベースのログインをトラッキングする単純なServiceBuilderコンポーネントを紹介します。実際にHSQLを本番環境では使わないため、最後の部分は調整していますが、プラ​​ットフォームでサポートされているDBに自由に置き換えることができます。

プロジェクト


今回は、Gradle、JDK 1.8、Liferay CE 7 GA2をプロジェクトに使用します。プロジェクトを作成するコマンドは次のとおりです。

blade create -t servicebuilder -p com.liferay.example.servicebuilder.extdb sb-extdb

これにより、2つのモジュールを持つServiceBuilderプロジェクトが作成されます。

  • sb-extdb-api:コンシューマーが依存するAPIモジュール
  • sb-extdb-service:サービス実装モジュール

まずエンティティを定義する必要があります。 service.xmlファイルはsb-extdb-serviceモジュールにあります。ここから始めてください:

<!--?xml version="1.0"?-->
 

  <service-builder package-path="com.liferay.example.servicebuilder.extdb">

    <!-- Define a namespace for our example -->
    <namespace>ExtDB</namespace>

    <!-- Define an entity for tracking login information. -->
    <entity data-source="extDataSource" local-service="true" name="UserLogin" remote-service="false" uuid="false">
    <!-- session-factory="extSessionFactory" tx-manager="extTransactionManager" -->

      <!-- userId is our primary key. -->
      <column name="userId" primary="true" type="long">

      <!-- We'll track the date of last login -->
      <column name="lastLogin" type="Date">

      <!-- We'll track the total number of individual logins for the user -->
      <column name="totalLogins" type="long">

      <!-- Let's also track the longest time between logins -->
      <column name="longestTimeBetweenLogins" type="long">

      <!-- And we'll also track the shortest time between logins -->
      <column name="shortestTimeBetweenLogins" type="long">
    </column></column></column></column></column></entity>
  </service-builder>

上記はユーザーログインをトラッキングするための、非常にシンプルなエンティティです。ユーザーIDがプライマリキーとなり、ログインの日時、ユーザーのログイン総数が追跡されます。

以前のバージョンのLiferayと同様に、エンティティの外部データソースを指定する必要があります。

ServiceBuilderは、Liferayデータベースのテーブルのみを作成および管理します。 ServiceBuilderは、外部データベースのテーブル、インデックスなどを管理しません。

今回の特定の例では、HSQLに配線するつもりですので、テーブル定義を使って、HSQLスクリプトファイルを作成する以下手順を実行しました:

CREATE MEMORY TABLE PUBLIC.EXTDB_USERLOGIN(
    USERID BIGINT NOT NULL PRIMARY KEY,
    LASTLOGIN TIMESTAMP,
    TOTALLOGINS BIGINT,
    LONGESTTIMEBETWEENLOGINS BIGINT,
    SHORTESTTIMEBETWEENLOGINS BIGINT);

サービス


次に行うべきことは、サービスの構築です。 sb-extdb-serviceディレクトリで、サービスをビルドする必要があります:

gradle buildService

最終的には、このトラッキングを管理するためにログイン後のフックを構築します。そこでログイン追跡を簡略化する方法を使用することができます。 UserLoginLocalServiceImpl.javaに追加するメソッドは次のとおりです。

public class UserLoginLocalServiceImpl extends UserLoginLocalServiceBaseImpl {
  private static final Log logger = LogFactoryUtil.getLog(UserLoginLocalServiceImpl.class);

  /**
  * updateUserLogin: Updates the user login record with the given info.
  * @param userId User who logged in.
  * @param loginDate Date when the user logged in.
  */
  public void updateUserLogin(final long userId, final Date loginDate) {
    UserLogin login;

    // first try to get the existing record for the user
    login = fetchUserLogin(userId);

    if (login == null) {
      // user has never logged in before, need a new record
      if (logger.isDebugEnabled()) logger.debug("User " + userId + " has never logged in before.");

      // create a new record
      login = createUserLogin(userId);

      // update the login date
      login.setLastLogin(loginDate);

      // initialize the values
      login.setTotalLogins(1);
      login.setShortestTimeBetweenLogins(Long.MAX_VALUE);
      login.setLongestTimeBetweenLogins(0);

      // add the login
      addUserLogin(login);
    } else {
      // user has logged in before, just need to update record.

      // increment the logins count
      login.setTotalLogins(login.getTotalLogins() + 1);

      // determine the duration time between the current and last login
      long duration = loginDate.getTime() - login.getLastLogin().getTime();

      // if this duration is longer than last, update the longest duration.
      if (duration > login.getLongestTimeBetweenLogins()) {
        login.setLongestTimeBetweenLogins(duration);
      }

      // if this duration is shorter than last, update the shortest duration.
      if (duration < login.getShortestTimeBetweenLogins()) {
        login.setShortestTimeBetweenLogins(duration);
      }

      // update the last login timestamp
      login.setLastLogin(loginDate);

      // update the record
      updateUserLogin(login);
    }
  }
}

メソッドを追加したら、メソッドをAPIに組み込むためにサービスを再構築する必要があります。

データソースBeanの定義


続いて、外部データソースのデータソースBeanを定義する必要があります。 sb-extdb-service / src / main / resources / META-INF / springディレクトリにXMLファイルext-db-spring.xmlを作成します。モジュールがロードされると、このディレクトリのSpringファイルはモジュールのSpringコンテキストに自動的に処理されます。

<!--?xml version="1.0"?-->

<beans default-destroy-method="destroy" default-init-method="afterPropertiesSet" xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  <!--
    NOTE: Current restriction in LR7's handling of external data sources requires us to redefine the
    liferayDataSource bean in our spring configuration.

    The following beans define a new liferayDataSource based on the jdbc.ext. prefix in portal-ext.properties.
   -->
  <bean class="com.liferay.portal.dao.jdbc.spring.DataSourceFactoryBean" id="liferayDataSourceImpl">
    <property name="propertyPrefix" value="jdbc.ext.">
  </property></bean>

  <bean class="org.springframework.jdbc.datasource.LazyConnectionDataSourceProxy" id="liferayDataSource">
    <property name="targetDataSource" ref="liferayDataSourceImpl">
  </property></bean>

  <!--
    So our entities are all appropriately tagged with the extDataSource, we'll alias the above
    liferayDataSource so it matches the entities.
   -->

  <alias alias="extDataSource" name="liferayDataSource">
</alias></beans>

これらのBean定義は、外部データソースを使用する従来の方法とは大きく異なります。これまではLiferayデータソースBeanから別々のデータソースBeanを定義していましたが、Liferay 7では、Liferayデータソースを外部データソースを指すよう、再定義する必要があります。

これにはいくつかの重要な副作用があります:

1つのServiceBuilderモジュールで使用できるデータソースは1つだけです。 3つの異なる外部データソースがある場合は、3つの異なるServiceBuilderモジュールを作成する必要があります(各データソースに1つずつ)。

通常のLiferayトランザクション管理は、トランザクションの範囲を現在のモジュールに制限します。 ServiceBuilderモジュールをまたいだトランザクションを管理するには、XAトランザクションを定義して使用する必要があります。

Service Builder / Spring Extender用のモジュールを構築する場合、Spring XMLファイルで参照されるクラスは、ClassNotFoundExceptionsにつながるOSGiインポート要件として宣言されません。 org.springframework.jdbc.datasourceやcom.liferay.portal.dao.jdbc.springなどのパッケージは、手動でbnd.bndファイルにインポートする必要があります。

最後のエイリアス行は、service.xmlファイル内の名前付きデータソースとしてliferayDataSourceのSpringエイリアスを定義しています。

例にもどりましょう。レコードをHSQLに書き込むつもりだったので、それには、外部データソース接続のためにportal-ext.propertiesにプロパティを追加する必要があります。

# Connection details for the HSQL database
jdbc.ext.driverClassName=org.hsqldb.jdbc.JDBCDriver
jdbc.ext.url=jdbc:hsqldb:${liferay.home}/data/hypersonic/logins;hsqldb.write_delay=false
jdbc.ext.username=sa
jdbc.ext.password=

ポストログインフック


次にブレードを使用してpost login hookを作成します。 sb-extdbメインディレクトリで、bladeを実行してモジュールを作成します。

blade create -p com.liferay.example.servicebuilder.extdb.event -t service -s com.liferay.portal.kernel.events.LifecycleAction sb-extdb-postlogin

ブレードは実際にサブモジュールが追加されていることはわからないので、完全なスタンドアロンのgradleプロジェクトを作成しました。ここでは記述していませんが、いくつかのgradleプロジェクトファイルを変更し、postloginモジュールをプロジェクトのサブモジュールにしています。

com.liferay.example.servicebuilder.extdb.event.UserLoginTrackerActionを次のように作成します。

/**
 * class UserLoginTrackerAction: This is the post login hook to track user logins.
 *
 * @author dnebinger
 */
@Component(
  immediate = true, property = {"key=login.events.post"},
  service = LifecycleAction.class
)
public class UserLoginTrackerAction implements LifecycleAction {

  private static final Log logger = LogFactoryUtil.getLog(UserLoginTrackerAction.class);

  /**
   * processLifecycleEvent: Invoked when the registered event is triggered.
   * @param lifecycleEvent
   * @throws ActionException
   */
  @Override
  public void processLifecycleEvent(LifecycleEvent lifecycleEvent) throws ActionException {

    // okay, we need the user login for the event
    User user = null;

    try {
      user = PortalUtil.getUser(lifecycleEvent.getRequest());
    } catch (PortalException e) {
      logger.error("Error accessing login user: " + e.getMessage(), e);
    }

    if (user == null) {
      logger.warn("Could not find the logged in user, nothing to track.");

      return;
    }

    // we have the user, let's invoke the service
    getService().updateUserLogin(user.getUserId(), new Date());

    // alternatively we could just use the local service util:
    // UserLoginLocalServiceUtil.updateUserLogin(user.getUserId(), new Date());
  }

  /**
   * getService: Returns the user tracker service instance.
   * @return UserLoginLocalService The instance to use.
   */
  public UserLoginLocalService getService() {
    return _serviceTracker.getService();
  }

  // use the OSGi service tracker to get an instance of the service when available.
  private ServiceTracker _serviceTracker =
    ServiceTrackerFactory.open(UserLoginLocalService.class);
}

チェックポイント:テスト


この時点で、apiモジュール、サービスモジュール、ポストログインフックモジュールをビルドして展開できるはずです。 gradleコマンドを使用します:

gradle build

各サブモジュールには、バンドルjarがあるbuild / libsディレクトリがあります。使用しているバージョンのLR7CEGA2を起動し(jdbc.extプロパティがportal-text.propertiesファイル内にあることを確認してください)、$ LIFERAY_HOME / deployフォルダにjarを配置します。 Liferayはそれらを認識し、デプロイします。

Gogo Shellからモジュールが起動していることを確認してください。

ポータルに数回ログインすると、データディレクトリ内のデータベースを検索し、レコードに含まれている内容を参照できるようになります。

まとめ


Liferay 7のServiceBuilderで外部データソースを使用することは引き続き推奨されています。これは、今後もDBベースのOSGiモジュールを構築するためのツールであり、APIの実装をカプセル化しながら、大量のDBアクセスコードを生成することができます。

ご紹介したLiferay 7によって課されたServiceBuilderの新しい制約を以下にまとめました。

  • Service Builderモジュールごとに1つの(外部の)データソースのみ
  • 外部データソースオブジェクト、テーブル、インデックスなどは、手動で管理する必要がある
  • トランザクションを複数のService Builderモジュールにまたがるようにするには、XAトランザクションを使用する必要がある

本ブログ記事のGitHubプロジェクトコードはこちらから参照いただけます:
https://github.com/dnebing/sb-extdb


本記事は以上です。
いかがでしたでしょうか?

記事に関するご意見、ご感想などございましたら、お気軽に[email protected]までご連絡くだい。


■ これまで発信してきた日本語による技術コンテンツを、Qiitaでお読みいただけます。よろしければそちらの記事も合わせてご役立てください。
https://qiita.com/yasuflatland-lf

■Doorkeeperのライフレイコミュニティメンバー大歓迎!
コーポレートブログの技術コンテンツ更新情報など定期的におとどけしています。
https://liferay.doorkeeper.jp/