Skip to content

Instantly share code, notes, and snippets.

@gakuzzzz
Last active December 10, 2015 03:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gakuzzzz/4372720 to your computer and use it in GitHub Desktop.
Save gakuzzzz/4372720 to your computer and use it in GitHub Desktop.
汎用的なDelegateのリフレクション使わない版
import org.joda.time.DateTime;
import org.seasar.doma.Column;
import org.seasar.doma.Entity;
@Entity
public abstract class LogicalDeletable {
@Column
protected DateTime deletedTime;
}
import javax.annotation.Nonnull;
import org.seasar.doma.Column;
import org.seasar.doma.Entity;
import org.seasar.doma.GeneratedValue;
import org.seasar.doma.GenerationType;
import org.seasar.doma.Id;
import org.seasar.doma.SequenceGenerator;
import org.seasar.doma.jdbc.entity.NamingType;
@Entity(naming = NamingType.SNAKE_UPPER_CASE)
public class Employee extends LogicalDeletable {
@Id
// @GeneratedValue(strategy = GenerationType.SEQUENCE) // Domainオブジェクトに@GeneratedValueつけようとすると怒られる
// @SequenceGenerator(sequence = "EMPLOYEE_SEQ")
public EmployeeId id;
@Column
public String name;
}
import org.seasar.doma.Dao;
import org.seasar.doma.Delegate;
import org.seasar.doma.Update;
@Dao
public interface LogicalDeletableDao {
@Update
int update(LogicalDeletable entity);
@Delegate(to = LogicalDeletableDaoDelegate.class)
int delete(LogicalDeletable entity);
}
import org.joda.time.DateTime;
import org.seasar.doma.jdbc.Config;
public class LogicalDeletableDaoDelegate {
private final Config config;
private final LogicalDeletableDao dao;
public LogicalDeletableDaoDelegate(final Config config, final LogicalDeletableDao dao) {
this.config = config;
this.dao = dao;
}
public int delete(final LogicalDeletable entity) {
entity.deletedTime = DateTime.now();
return dao.update(entity);
}
}
import org.seasar.doma.Dao;
import org.seasar.doma.Delegate;
import org.seasar.doma.Insert;
import org.seasar.doma.Select;
import org.seasar.doma.Update;
@Dao
public interface EmployeeDao extends LogicalDeletableDao {
@Update
int update(Employee entity);
@Insert
int insert(Employee entity);
@Delegate(to = LogicalDeletableDaoDelegate.class)
int delete(Employee entity);
}

org.seasar.doma.jdbc.JdbcException: [DOMA2022] IDプロパティのないエンティティ[LogicalDeletable]の更新や削除はできません

LogicalDeletableDao を 以下のようにすればいけるかと思ったが、

@Dao
public interface LogicalDeletableDao<T extends LogicalDeletableEntity> {

	@Update
	int update(T entity);
	
	@Delegate(to = LogicalDeletableDaoDelegate.class)
	int delete(T entity);

}
[DOMA4059] Daoインタフェースには型パラメータを定義できません。

というエラーで怒られる。

かといって@Daoを付けないと

[DOMA4188] @Daoが注釈されたインタフェースは@Daoが注釈されてないインタフェース[models.shared.LogicalDeletableDao]をextendsできません。

となる。

余談だが、Domain Object型のプロパティに @GeneratedValue を付けようとすると怒られる。 @Id に Domain Object型が使えなくて非常につらいです……。

@nakamura-to
Copy link

DOMA4059の件ですが、SQLファイルとの対応付けが難しくなるのでDaoをジェネリックにすることは意図的に禁止しています。
Domaのほうで修正するとして、こうできるようにするのはどうでしょうか?

@Dao
public interface EmployeeDao extends LogicalDeletableDao {

    @Update()
    int update(Employee entity);

    @Insert
    int insert(Employee entity);

    @Delegate(to = LogicalDeletableDaoDelegate.class, callback = "update")
    int delete(Employee entity);

}
public class LogicalDeletableDaoDelegate {

    public int delete(@Nonnull final LogicalDeletableEntity entity) {
        entity.deletedTime = DateTime.now();
    }
}

@DeleGateにcallback要素を新規追加して、デリゲートが終わった後に呼び出すメソッドを指定するというアイデアです。呼び出しはDomaが自動生成したコードの中で行います(つまりリフレクションは行いません)。

余談だが、Domain Object型のプロパティに @GeneratedValue を付けようとすると怒られる。 @Id に Domain Object型が使えなくて非常につらいです……。

これについては、対応したいと思います。

@gakuzzzz
Copy link
Author

SQLファイルとの対応付けが難しくなるのでDaoをジェネリックにすることは意図的に禁止しています。

なるほど、そういう意図があったんですね。

@DeleGateにcallback要素を新規追加して、デリゲートが終わった後に呼び出すメソッドを指定するというアイデアです。呼> び出しはDomaが自動生成したコードの中で行います(つまりリフレクションは行いません)。

ふむふむ、面白いですね。
気になるのは、 callback に指定できるメソッドは @Delegate が付くメソッドと同じシグネチャを持つ必要がある感じですかね?
すいません、ちょっともやもやした何かがあるので整理して言葉にします!

@nakamura-to
Copy link

気になるのは、 callback に指定できるメソッドは @DeleGate が付くメソッドと同じシグネチャを持つ必要がある感じですかね?

戻り値やパラメータは互換性のある型を認めるつもりです。ほとんどのケースで必要性は薄いと思いますが、念のため。

    @Update()
    int update(Parent entity)

    @Delegate(to = MyDelegate.class, callback = "update")
    int delete(Child entity);

@nakamura-to
Copy link

もっと別の方法も思いつきました。Domaの修正は必要ですが、@DeleGateではなく、EntityListenerを使うのはどうでしょうか?前後でフックしたいならこちらのほうがいいかもしれないです。欠点は、すべてのEntityにEntityListenerを指定する必要があることです。

public class LogicalDeletableListener<T extends LogicalDeletable> implements EntityListener<T> {

    @Override
    public void preUpdate(T entity, PreUpdateContext context) {
        // このようなメソッドは現行のDomaにはないが修正は可能(だと思われる)
        Method m = context.getDaoMethod();
        // 論理削除ならdeletedTimeを更新
        // ここでは命名規約によるチェックだが、独自アノテーションの有無でチェックしてもいい
        if (m.getName().startsWith("delete")) { 
            entity.deletedTime = DateTime.now();
        }
    }

    @Override
    public void postUpdate(T entity, PostUpdateContext context) {
    }
    ...
}
public class EmployeListener implements LogicalDeletableListener<Employe> {
    ...
}
@Entity(listener = EmployeListener.class)
public class Employe extends LogicalDeletable {
    ...
}

@gakuzzzz
Copy link
Author

なるほど。
現状って EntityListener は 1 Entity につき 1クラスのみ指定できる感じですよね?

実はこれとは別に updateBy を更新する EntityListener を用意してるんですよね。
その実装と混ぜてもいいんですが、論理削除じゃない Entity も存在していて、そいつらのupdatedByも更新してやる必要があるという……。

なんか厄介な設計で申し訳なくなってきました><

@nakamura-to
Copy link

現状って EntityListener は 1 Entity につき 1クラスのみ指定できる感じですよね?

そうですね。1 Entity につき 1クラスです。

1つ上の例で示したコードでは、メソッド名で条件分岐させていますが、論理削除用のアノテーションを独自に作ってDaoのメソッドに注釈すれば、論理削除する処理としない処理が混在していても明確に区別できるのではないかなーと思います。

上の例で言うところのLogicalDeletableListenerを継承するEntityListenerと、直接Domaのインタフェースを実装するEntityListenerで分けてしまうのもありだと思います。

@gakuzzzz
Copy link
Author

あれこれ考えてみましたが、ご提案頂いた PreUpdateContext がメソッドの情報を返してくれる形がやっぱり一番良さそうです。
EntityListener の継承に若干迷いがありましたが、よくよく考えるとそこまで強く否定する材料もなかったです。

お手すきの際にでも getDaoMethod() の実装を入れて頂けますでしょうか?
色々わがままを申してお手数をお掛けしますが、よろしくお願いしますm(_ _)m

@nakamura-to
Copy link

了解です。
EntityListener周辺の機能拡張で進めます。

色々わがままを

こちらとしては、いろいろ意見を聞けておもしろいのでお気になさらず。

@nakamura-to
Copy link

対応してみました。このSNAPSHOTでお試しください。

EntityListenerのcontextからメソッドやConfigを取れるようにしました。こんな感じで取得できます。

public class EmployeeListener implements EntityListener<Employee> {

    @Override
    public void preUpdate(Employee employee, PreUpdateContext context) {
        // Daoのメソッドを取得
        Method method = context.getMethod();
        // Daoに指定したConfigを取得
        Config config = context.getConfig();
        ...
    }
    ...
}

それから、Entityクラスにて、@GeneratedValueや@Versionを注釈したプロパティをドメインクラスの型で定義できるようにしました。

先日行った、@DeleGateで互換性のあるDaoやEntityを受け入れる修正(https://gist.github.com/4355837) は一旦取り除いてあります。

@gakuzzzz
Copy link
Author

ありがとうございます。早速試してみました。
一点はまりましたが、上手くいきました!!

ハマったのは以下のポイントです。

@Entity
public abstract class AbstractEntity {
    @Column     
    public DateTime createdAt;
    @Column     
    public DateTime updatedAt;
}
@Entity
public abstract class LogicalDeletable extends AbstractEntity {
    @Column
    protected DateTime deletedAt;
}
public class InitializeTimestampEntityListener implements EntityListener<AbstractEntity> {

    @Override
    public void preInsert(final AbstractEntity entity, final PreInsertContext context) {
        entity.createdAt = DateTime.now();
        entity.updatedAt = DateTime.now();
    }

    @Override
    public void preUpdate(final AbstractEntity entity, final PreUpdateContext context) {
        entity.updatedAt = DateTime.now();
    }
    ...
}
public class LogicalDeletableEntityListener extends InitializeTimestampEntityListener {

    @Override
    public void preUpdate(final AbstractEntity entity, final PreUpdateContext context) {
        super.preUpdate(entity, context);
        if (context.getMethod().isAnnotationPresent(LogicalDeletion.class)) {
            entity.deletedAt = DateTime.now(); // 型があわないので deletedAt にアクセスできない!
        }
    }

}

Entity の型があわずに deletedAt にアクセスできない感じです。

以下のようにすると Java Compiler がエラー吐いて落ちます^^;;;

public class InitializeTimestampEntityListener<T extends AbstractEntity> implements EntityListener<T> {
    ...
}
public class LogicalDeletableEntityListener extends InitializeTimestampEntityListener<LogicalDeletable> {
    ...
}

一応、以下のようにすることで対応は可能でした。

public abstract class AbstractInitializeTimestampEntityListener<T extends AbstractEntity> implements EntityListener<T> {

    @Override
    public void preInsert(final T entity, final PreInsertContext context) {
        entity.createdAt = DateTime.now();
        entity.updatedAt = DateTime.now();
    }

    @Override
    public void preUpdate(final T entity, final PreUpdateContext context) {
        entity.updatedAt = DateTime.now();
    }
}
public class InitializeTimestampEntityListener extends AbstractInitializeTimestampEntityListener<AbstractEntity> {}
public class LogicalDeletableEntityListener extends AbstractInitializeTimestampEntityListener<LogicalDeletableEntity> {

    @Override
    public void preUpdate(final LogicalDeletableEntity entity, final PreUpdateContext context) {
        super.preUpdate(entity, context);
        if (context.getMethod().isAnnotationPresent(LogicalDeletion.class)) {
            entity.deletedAt = DateTime.now();
        }
    }

}

取り急ぎご報告まで

@nakamura-to
Copy link

確認ありがとうございます。動いたと言うことでなりよりです。

以下のようにすると Java Compiler がエラー吐いて落ちます^^;;;

これは、もしかして、InitializeTimestampEntityListenerのような型パラメータをもつクラスを@entityのlistener要素に指定するとDomaのaptでエラーになるやつですか?これは、わかりやすいエラーメッセージを出すように修正したいと思います。

@entityのlistener要素に指定できるのはインスタンス化可能なクラス(具象でかつ型パラメータをもたない)だけになります。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment