Reddit投稿の履歴を保存する

1概要

今回のリンクでは、/case-study-a-reddit-app-with-spring[Reddit Appのケーススタディ]で、投稿に対する投稿試行の履歴** を追跡し始めます。ステータスはより説明的でわかりやすい。

2 Post エンティティ の改善

まず、 Post エンティティの古いStringステータスを送信応答のより完全なリストに置き換えて、より多くの情報を追跡します。

public class Post {
    ...
    @OneToMany(fetch = FetchType.EAGER, mappedBy = "post")
    private List<SubmissionResponse> submissionsResponse;
}

次に、この新しい送信応答エンティティで実際に追跡している内容を見てみましょう。

@Entity
public class SubmissionResponse implements IEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private int attemptNumber;

    private String content;

    private Date submissionDate;

    private Date scoreCheckDate;

    @JsonIgnore
    @ManyToOne
    @JoinColumn(name = "post__id", nullable = false)
    private Post post;

    public SubmissionResponse(int attemptNumber, String content, Post post) {
        super();
        this.attemptNumber = attemptNumber;
        this.content = content;
        this.submissionDate = new Date();
        this.post = post;
    }

    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder();
        builder.append("Attempt No ").append(attemptNumber).append(" : ").append(content);
        return builder.toString();
    }
}

消費された送信試行 SubmissionResponse を持っているので注意してください。

  • attemptNumber :この試みの数

  • content :この試みの詳細な回答

  • submissionDate :この試みの提出日

  • scoreCheckDate :Redditのスコアを確認した日付

この試みでは

そして、これが単純なSpring Data JPAリポジトリです。

public interface SubmissionResponseRepository extends JpaRepository<SubmissionResponse, Long> {

    SubmissionResponse findOneByPostAndAttemptNumber(Post post, int attemptNumber);
}

3スケジューリングサービス

この追加情報を追跡するために、サービス層の修正を始める必要があります。

まず、Postが成功または失敗と見なされた理由について、うまくフォーマットされた成功または失敗の理由があることを確認します。

private final static String SCORE__TEMPLATE = "score %d %s minimum score %d";
private final static String TOTAL__VOTES__TEMPLATE = "total votes %d %s minimum total votes %d";

protected String getFailReason(Post post, PostScores postScores) {
    StringBuilder builder = new StringBuilder();
    builder.append("Failed because ");
    builder.append(String.format(
      SCORE__TEMPLATE, postScores.getScore(), "<", post.getMinScoreRequired()));

    if (post.getMinTotalVotes() > 0) {
        builder.append(" and ");
        builder.append(String.format(TOTAL__VOTES__TEMPLATE,
          postScores.getTotalVotes(), "<", post.getMinTotalVotes()));
    }
    if (post.isKeepIfHasComments()) {
        builder.append(" and has no comments");
    }
    return builder.toString();
}

protected String getSuccessReason(Post post, PostScores postScores) {
    StringBuilder builder = new StringBuilder();
    if (postScores.getScore() >= post.getMinScoreRequired()) {
        builder.append("Succeed because ");
        builder.append(String.format(SCORE__TEMPLATE,
          postScores.getScore(), ">=", post.getMinScoreRequired()));
        return builder.toString();
    }
    if (
      (post.getMinTotalVotes() > 0) &&
      (postScores.getTotalVotes() >= post.getMinTotalVotes())
    ) {
        builder.append("Succeed because ");
        builder.append(String.format(TOTAL__VOTES__TEMPLATE,
          postScores.getTotalVotes(), ">=", post.getMinTotalVotes()));
        return builder.toString();
    }
    return "Succeed because has comments";
}

それでは、古いロジックを改善し、 この追加の履歴情報を追跡します

private void submitPost(...) {
    ...
    if (errorNode == null) {
        post.setSubmissionsResponse(addAttemptResponse(post, "Submitted to Reddit"));
        ...
    } else {
        post.setSubmissionsResponse(addAttemptResponse(post, errorNode.toString()));
        ...
    }
}
private void checkAndReSubmit(Post post) {
    if (didIntervalPass(...)) {
        PostScores postScores = getPostScores(post);
        if (didPostGoalFail(post, postScores)) {
            ...
            resetPost(post, getFailReason(post, postScores));
        } else {
            ...
            updateLastAttemptResponse(
              post, "Post reached target score successfully " +
                getSuccessReason(post, postScores));
        }
    }
}
private void checkAndDeleteInternal(Post post) {
    if (didIntervalPass(...)) {
        PostScores postScores = getPostScores(post);
        if (didPostGoalFail(post, postScores)) {
            updateLastAttemptResponse(post,
              "Deleted from reddit, consumed all attempts without reaching score " +
                getFailReason(post, postScores));
            ...
        } else {
            updateLastAttemptResponse(post,
              "Post reached target score successfully " +
                getSuccessReason(post, postScores));
            ...
        }
    }
}
private void resetPost(Post post, String failReason) {
    ...
    updateLastAttemptResponse(post, "Deleted from Reddit, to be resubmitted " + failReason);
    ...
}

低レベルのメソッドが実際に行っていることに注意してください。

  • addAttemptResponse() :新しい SubmissionResponse レコードを作成し、

投稿に追加します(投稿のたびに呼び出されます)。 ** updateLastAttemptResponse() :最後の試行応答を更新する

(投稿のスコアを確認しながら呼び出されます)

4スケジュールされた投稿DTO

次に、この新しい情報がクライアントに公開されるようにDTOを変更します。

public class ScheduledPostDto {
    ...

    private String status;

    private List<SubmissionResponseDto> detailedStatus;
}

そして、これがシンプルな SubmissionResponseDto です。

public class SubmissionResponseDto {

    private int attemptNumber;

    private String content;

    private String localSubmissionDate;

    private String localScoreCheckDate;
}

ScheduledPostRestController の変換方法も変更します。

private ScheduledPostDto convertToDto(Post post) {
    ...
    List<SubmissionResponse> response = post.getSubmissionsResponse();
    if ((response != null) && (response.size() > 0)) {
        postDto.setStatus(response.get(response.size() - 1).toString().substring(0, 30));
        List<SubmissionResponseDto> responsedto =
          post.getSubmissionsResponse().stream().
            map(res -> generateResponseDto(res)).collect(Collectors.toList());
        postDto.setDetailedStatus(responsedto);
    } else {
        postDto.setStatus("Not sent yet");
        postDto.setDetailedStatus(Collections.emptyList());
    }
    return postDto;
}

private SubmissionResponseDto generateResponseDto(SubmissionResponse responseEntity) {
    SubmissionResponseDto dto = modelMapper.map(responseEntity, SubmissionResponseDto.class);
    String timezone = userService.getCurrentUser().getPreference().getTimezone();
    dto.setLocalSubmissionDate(responseEntity.getSubmissionDate(), timezone);
    if (responseEntity.getScoreCheckDate() != null) {
        dto.setLocalScoreCheckDate(responseEntity.getScoreCheckDate(), timezone);
    }
    return dto;
}

5フロントエンド

次に、新しいレスポンスを処理するためにフロントエンド scheduledPosts.jsp を変更します。

<div class="modal">
    <h4 class="modal-title">Detailed Status</h4>
    <table id="res"></table>
</div>

<script >
var loadedData =[];
var detailedResTable = $('#res').DataTable( {
    "searching":false,
    "paging": false,
    columns:[        { title: "Attempt Number", data: "attemptNumber" },
        { title: "Detailed Status", data: "content" },
        { title: "Attempt Submitted At", data: "localSubmissionDate" },
        { title: "Attempt Score Checked At", data: "localScoreCheckDate" }
]} );

$(document).ready(function() {
    $('#myposts').dataTable( {
        ...
        "columnDefs":[            { "targets": 2, "data": "status",
              "render": function ( data, type, full, meta ) {
                  return data +
                    ' <a href="#" onclick="showDetailedStatus('+meta.row+' )">More Details</a>';
              }
            },
            ....
       ],
        ...
    });
});

function showDetailedStatus(row){
    detailedResTable.clear().rows.add(loadedData[row].detailedStatus).draw();
    $('.modal').modal();
}

</script>

6. テスト

最後に、新しいメソッドに対して簡単な単体テストを行います。

まず、 getSuccessReason() 実装をテストします。

@Test
public void whenHasEnoughScore__thenSucceed() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    PostScores postScores = new PostScores(6, 10, 1);

    assertTrue(getSuccessReason(post, postScores).contains("Succeed because score"));
}

@Test
public void whenHasEnoughTotalVotes__thenSucceed() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setMinTotalVotes(8);
    PostScores postScores = new PostScores(2, 10, 1);

    assertTrue(getSuccessReason(post, postScores).contains("Succeed because total votes"));
}

@Test
public void givenKeepPostIfHasComments__whenHasComments__thenSucceed() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setKeepIfHasComments(true);
    final PostScores postScores = new PostScores(2, 10, 1);

    assertTrue(getSuccessReason(post, postScores).contains("Succeed because has comments"));
}

次に、 getFailReason() 実装をテストします。

@Test
public void whenNotEnoughScore__thenFail() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    PostScores postScores = new PostScores(2, 10, 1);

    assertTrue(getFailReason(post, postScores).contains("Failed because score"));
}

@Test
public void whenNotEnoughTotalVotes__thenFail() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setMinTotalVotes(15);
    PostScores postScores = new PostScores(2, 10, 1);

    String reason = getFailReason(post, postScores);
    assertTrue(reason.contains("Failed because score"));
    assertTrue(reason.contains("and total votes"));
}

@Test
public void givenKeepPostIfHasComments__whenNotHasComments__thenFail() {
    Post post = new Post();
    post.setMinScoreRequired(5);
    post.setKeepIfHasComments(true);
    final PostScores postScores = new PostScores(2, 10, 0);

    String reason = getFailReason(post, postScores);
    assertTrue(reason.contains("Failed because score"));
    assertTrue(reason.contains("and has no comments"));
}

7. 結論

今回の記事では、Reddit投稿のライフサイクルに非常に役立つ可視性を紹介しました。毎回投稿が送信され削除された日時と、各操作の正確な理由を正確に確認できます。