Batch hay trong EzyPlatform gọi là DataAppender là một trong những tính năng thường xuyên được sử dụng để tổng hợp dữ liệu cho các nghiệp vụ nặng nề, trong bài này chúng ta sẽ cùng nhau tạo một DataAppender để lưu số từ có trong bài viết vào bảng personal_post_word_counts đã tạo trước đó nhé.

Mục tiêu

Của bài này là giúp các bạn:

  • Tạo được lớp DataAppender để lưu số từ có trong bài viết vào bảng personal_post_word_counts.
  • Tắt bật DataAppender.

Tạo lớp DataAppender

Đầu tiên chúng ta hãy tạo lớp AdminPersonalPostHistoryRepository với nội dung như sau:

package org.youngmonkeys.personal.admin.repo;

import com.tvd12.ezydata.database.EzyDatabaseRepository;
import com.tvd12.ezyfox.database.annotation.EzyQuery;
import com.tvd12.ezyfox.database.annotation.EzyRepository;
import com.tvd12.ezyfox.util.Next;
import org.youngmonkeys.ezyarticle.sdk.entity.PostHistory;

import java.util.List;

@EzyRepository
public interface AdminPersonalPostHistoryRepository
    extends EzyDatabaseRepository<Long, PostHistory> {

    @EzyQuery(
        "SELECT e FROM PostHistory e " +
            "WHERE e.id > ?0 " +
            "ORDER BY e.id ASC"
    )
    List<PostHistory> findByIdGt(long idGt, Next next);
}

Lớp này sẽ cung cấp hàm findByIdGt để lấy được danh sách các bài viết theo chiều id tăng dần đến id lớn nhất, việc này đảm bảo chúng ta sẽ duyệt qua được tất cả các lịch sử bài viết và không lặp lại.

Tiếp theo chúng ta hãy tạo lớp AdminPersonalPostWordCountDataAppender với nội dung như sau:

package org.youngmonkeys.personal.admin.appender;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tvd12.ezyfox.bean.annotation.EzySingleton;
import com.tvd12.ezyfox.util.Next;
import org.youngmonkeys.ezyarticle.sdk.entity.PostHistory;
import org.youngmonkeys.ezyplatform.admin.appender.AdminDataAppender;
import org.youngmonkeys.ezyplatform.admin.service.AdminSettingService;
import org.youngmonkeys.ezyplatform.time.ClockProxy;
import org.youngmonkeys.personal.admin.repo.AdminPersonalPostHistoryRepository;
import org.youngmonkeys.personal.admin.repo.AdminPersonalPostWordCountRepository;
import org.youngmonkeys.personal.entity.PersonalPostWordCount;

import java.util.Arrays;
import java.util.List;

import static com.tvd12.ezyfox.io.EzyLists.last;
import static org.youngmonkeys.ezyplatform.constant.CommonConstants.LIMIT_100_RECORDS;

@EzySingleton
public class AdminPersonalPostWordCountDataAppender
    extends AdminDataAppender<PostHistory, PersonalPostWordCount, Long> {

    private final ClockProxy clock;
    private final AdminPersonalPostHistoryRepository postHistoryRepository;
    private final AdminPersonalPostWordCountRepository postWordCountRepository;

    public AdminPersonalPostWordCountDataAppender(
        ClockProxy clock,
        ObjectMapper objectMapper,
        AdminSettingService settingService,
        AdminPersonalPostHistoryRepository postHistoryRepository,
        AdminPersonalPostWordCountRepository postWordCountRepository
    ) {
        super(objectMapper, settingService);
        this.clock = clock;
        this.postHistoryRepository = postHistoryRepository;
        this.postWordCountRepository = postWordCountRepository;
    }

    @Override
    protected boolean defaultStarted() {
        return true;
    }

    @Override
    protected void addDataRecord(PersonalPostWordCount dataRecord) {
        postWordCountRepository.save(dataRecord);
    }

    @Override
    protected PersonalPostWordCount toDataRecord(
        PostHistory value
    ) {
        long postId = value.getParentId();
        String content = value.getContent();
        long wordCount = Arrays
            .stream(content.trim().split("\s+"))
           .filter(s -> !s.isEmpty())
           .count();
        logger.info(
            "post history id: {}, post id: {}, word count: {}",
            value.getId(),
            postId,
            wordCount
        );
        PersonalPostWordCount entity = new PersonalPostWordCount();
        entity.setPostId(postId);
        entity.setWordCount(wordCount);
        entity.setUpdatedAt(clock.nowDateTime());
        return entity;
    }

    @Override
    protected List<PostHistory> filterValueList(
        List<PostHistory> valueList
    ) {
        return valueList;
    }

    @Override
    protected List<PostHistory> getValueList(Long pageToken) {
        return postHistoryRepository.findByIdGt(
            pageToken,
            Next.limit(LIMIT_100_RECORDS)
        );
    }

    @Override
    protected Long extractNewLastPageToken(
        List<PostHistory> valueList,
        Long currentLastPageToken
    ) {
         return valueList.isEmpty()
            ? currentLastPageToken
            : last(valueList).getId();
    }

    @Override
    protected Long defaultPageToken() {
        return 0L;
    }

    @Override
    protected Class<Long> pageTokenType() {
        return Long.class;
    }

    @Override
    protected String getAppenderNamePrefix() {
        return "personal_post_word_count";
    }
}

Lớp này tương đối phức tạp, nó sẽ làm một số việc sau:

1. Thừa kế lớp AdminDataAppender cơ sở, trong lớp cơ sở này sẽ cài đặt sẵn các nghiệp vụ để lưu lại các id đã được duyệt qua mà EzyPlatform gọi nó là pageToken. Các tham số generics bao gồm

  • PostHistory: Là Entity nguồn, tức là chúng ta sẽ lấy dữ liệu từ PostHistory.
  • PersonalPostWordCount: Là Entity đích, tức là chúng ta sẽ chuyển đồi dữ liệu từ PostHistory sang PersonalPostWordCount để lưu vào cơ sở dữ liệu.
  • Long: Là kiểu giá trị của pageToken, thực tế khi lưu xuống cơ sở dữ liệu (vào bảng ezy_settings) page token sẽ được chuyển sang dạng text.

2. Cài đặt các hàm thao tác với page token:

  • extractNewLastPageToken: Để lấy ra page token từ danh sách dữ liệu đã được lấy ra, ở đây chúng ta lấy ra id của phần tử cuối cùng trong danh sách post history được lấy ra.
  • defaultPageToken: Ở lần đầu tiên appender được chạy thì nó sẽ chưa có page token, ở đây chúng ta đặt bằng 0, nghĩa là appender sẽ lấy từ bản ghi đầu tiên của post history.
  • pageTokenType: Trả về kiểu của page token, ở đây là Long.

3. Hàm getAppenderNamePrefix trả về tên của appender, lát nữa chúng ta sẽ thấy tác dụng của nó.

4. Hàm getValueList trả về danh sách các post history cần được duyệt. Lưu ý rằng tất cả các appender của EzyPlatform đều sử dụng chung 1 luồng duy nhất, vậy hãy lấy một lượng vừa phải số bản ghi cần xử lý để nhường tài nguyên cho các appender khác.

5. Hàm filterValueList dùng để lọc ra các post history thoả mãn điều kiện nào đó, ở đây chúng ta sẽ đếm từ của mọi bài viết.

  1. Hàm toDataRecord dùng để chuyển đổi từ PostHistory sang PersonalPostWordCount. Ở đây chúng ta chỉ đơn giản là đếm số lượng từ có trong nội dung của post history và tạo thành đối tượng PersonalPostWordCount. Lưu ý rằng nghiệp vụ đếm từ rất phức tạp tuỳ theo ngôn ngữ mà được coi thế nào là "một từ", ví dụ Việt Nam thì ngăn cách nhau bởi dấu phẩy, nhưng Trung Quốc có khi lại khác.
  2. Hàm addDataRecord: Dùng để lưu PersonalPostWordCount được tạo ra vào cơ sở dữ liệu thông qua postWordCountRepository.

8. Hàm defaultStarted chỉ định rằng AdminPersonalPostWordCountDataAppender sẽ được chạy ngay khi EzyPlatform khởi động.

Bây giờ hãy chạy lại PersonalAdminPluginStartupTest và bạn sẽ thấy log hiện ra:

Screenshot 2025-10-12 at 11.58.33.png

Và khi truy vấn cơ sở dữ liệu bạn sẽ nhận được kết quả:

Screenshot 2025-10-12 at 12.00.34.png

Bật tắt DataAppender

EzyPlatform đã cung cấp sẵn tính năng này, bạn chỉ cần truy cập vào phần Cài đặt - Thông thường sau đó kéo xuống phần Các trình thêm dữ liệu và nhấn vào nút hiện, lú này bạn có thể thấy appender personal_post_word_count mà chúng ta đã đặt tên:

Screenshot 2025-10-12 at 13.03.06.png

Bạn có thể nhấn vào:

  • Stop (Dừng): để dừng lại appender.
  • Restart (Khởi động lại): Để khởi động lại appender, trên thực tế nó làm 2 thao tác là dừng vào chạy appender.
  • Reload (Tải lại): Để chạy lại appender từ đầu, về bản chất nó cài lại page token về null và chúng ta sẽ chạy từ đầu, nó có thể gây trùng lặp dữ liệu khi chạy lại nên bạn hãy cân nhắc cần thận.

Tài liệu tham khảo