wiki:AdvancedApplicationDevelopment

Version 6 (modified by 231175, 10 hours ago) ( diff )

--

Напреден Апликативен Развој

Трансакции

Креирање на курс

    @Transactional
    @Override
    public CourseVersion createCourse(CourseDtoFull courseDtoFull) {
        Language language = courseDtoFull.getLanguage();

        Course course = buildCourse(courseDtoFull, language);
        course = courseRepository.save(course);

        CourseVersion oldCourseVersion = courseVersionRepository
                .findByIsActiveTrueAndCourse_CourseId(course.getCourseId());

        int newVersionNumber = 1;
        if (oldCourseVersion != null) {
            newVersionNumber = oldCourseVersion.getVersionNumber() + 1;
            oldCourseVersion.setIsActive(false);
            courseVersionRepository.save(oldCourseVersion);
        }

        CourseVersion courseVersion = CourseVersion.builder()
                .versionNumber(newVersionNumber)
                .isActive(true)
                .createdAt(java.time.LocalDate.now())
                .course(course)
                .build();

        List<CourseContent> contentList = courseDtoFull.getCourseContents().stream()
                .map(content -> courseContentService.buildCourseContent(content, courseVersion, language))
                .toList();

        courseVersion.setCourseContents(contentList);

        List<Tag> skillTags =  tagService.processRawTags(
                courseDtoFull.getSkillsGained(), TagType.SKILL, language
        );
        List<Tag> topicTags = tagService.processRawTags(
                courseDtoFull.getTopicsCovered(), TagType.TOPIC, language
        );

        List<Tag> tagList = new ArrayList<>(skillTags);
        tagList.addAll(topicTags);

        course.setTags(tagList);

        return courseVersionRepository.save(courseVersion);
    }

    private Course buildCourse(CourseDtoFull courseDtoFull, Language language) {

        int durationMinutes = courseDtoFull.getCourseContents().stream()
                .flatMap(content -> content.getCourseLectures().stream())
                .mapToInt(CourseLectureDtoFull::getDurationMinutes)
                .sum();

        Course course = Course.builder()
                .imageUrl(courseDtoFull.getImageUrl())
                .color(courseDtoFull.getColor())
                .difficulty(courseDtoFull.getDifficulty())
                .durationMinutes(durationMinutes)
                .price(courseDtoFull.getPrice())
                .build();

        CourseTranslate courseTranslate = CourseTranslate.builder()
                .language(language)
                .titleShort(courseDtoFull.getTitleShort())
                .title(courseDtoFull.getTitle())
                .descriptionShort(courseDtoFull.getDescriptionShort())
                .description(courseDtoFull.getDescription())
                .descriptionLong(courseDtoFull.getDescriptionLong())
                .whatWillBeLearned(courseDtoFull.getWhatWillBeLearned())
                .course(course)
                .build();

        course.setTranslations(List.of(courseTranslate));
        return course;
    }

Преведување на постоечки курс

    @Transactional
    @Override
    public Course translateCourse(CourseTranslateReq courseTranslateReq) {
        validate.validateCourseExists(courseTranslateReq.getId());
        validate.validateCourseTranslation(courseTranslateReq.getId(), courseTranslateReq.getLanguage());

        CourseVersion courseVersion = courseVersionRepository.findByIsActiveTrueAndCourse_CourseId(courseTranslateReq.getId());
        Course course = courseRepository.findById(courseTranslateReq.getId()).orElseThrow();
        addCourseTranslation(course, courseTranslateReq);
        translateContents(courseVersion, courseTranslateReq.getCourseContents(), courseTranslateReq.getLanguage());

        tagService.processExistingTags(
                courseTranslateReq.getSkillsGained(), TagType.SKILL, courseTranslateReq.getLanguage()
        );
        tagService.processExistingTags(
                courseTranslateReq.getTopicsCovered(), TagType.TOPIC, courseTranslateReq.getLanguage()
        );

        return courseRepository.save(course);
    }

    private void addCourseTranslation(Course course, CourseTranslateReq dto) {
        CourseTranslate translation = CourseTranslate.builder()
                .language(dto.getLanguage())
                .titleShort(dto.getTitleShort())
                .title(dto.getTitle())
                .descriptionShort(dto.getDescriptionShort())
                .description(dto.getDescription())
                .descriptionLong(dto.getDescriptionLong())
                .whatWillBeLearned(dto.getWhatWillBeLearned())
                .course(course)
                .build();

        course.getTranslations().add(translation);
    }

    private void translateContents(CourseVersion courseVersion, List<CourseContentTranslateReq> contentDtos, Language language) {
        for (CourseContent content : courseVersion.getCourseContents()) {
            CourseContentTranslateReq contentDto = contentDtos.stream()
                    .filter(ct -> ct.getId().equals(content.getCourseContentId()))
                    .findFirst()
                    .orElseThrow(() -> new RuntimeException(
                            "Missing translation DTO for CourseContent id=" + content.getCourseContentId()
                    ));

            addContentTranslation(content, contentDto, language);
        }
    }

    private void addContentTranslation(CourseContent content, CourseContentTranslateReq contentDto, Language language) {
        CourseContentTranslate contentTranslate = CourseContentTranslate.builder()
                .title(contentDto.getTitle())
                .language(language)
                .courseContent(content)
                .build();

        content.getTranslations().add(contentTranslate);
        translateLectures(content, contentDto.getCourseLectures(), language);
    }

    private void translateLectures(CourseContent content, List<CourseLectureTranslateReq> lectureDtos, Language language) {
        for (CourseLecture lecture : content.getCourseLectures()) {
            CourseLectureTranslateReq lectureDto = lectureDtos.stream()
                    .filter(ld -> ld.getId().equals(lecture.getCourseLectureId()))
                    .findFirst()
                    .orElseThrow(() -> new RuntimeException(
                            "Missing translation DTO for CourseLecture id=" + lecture.getCourseLectureId()
                    ));

            CourseLectureTranslate lectureTranslate = CourseLectureTranslate.builder()
                    .language(language)
                    .title(lectureDto.getTitle())
                    .description(lectureDto.getDescription())
                    .contentFileName(lectureDto.getContentFileName())
                    .contentText(lectureDto.getContentText())
                    .courseLecture(lecture)
                    .build();

            lecture.getTranslations().add(lectureTranslate);
        }
    }

Мапирање на ентитети кон новата шема

Новата шема користи PostgreSQL enum типови и text[] колона, кои Hibernate не ги мапира автоматски.

Enum вредностите во базата се со мали букви (skill, topic, beginner, pending, ...), затоа Java enum константите мора да ги носат истите имиња преку @JdbcTypeCode(SqlTypes.NAMED_ENUM).

@Entity
@Table(name = "tag")
public class Tag {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "tag_id")
    private Long tagId;

    @Enumerated(EnumType.STRING)
    @JdbcTypeCode(SqlTypes.NAMED_ENUM)
    @Column(name = "tag_type", columnDefinition = "tag_type")
    private TagType tagType;
}

public enum TagType {
    skill,
    topic
}

Колоната what_will_be_learned е text[], па се мапира како List<String>.

@Entity
@Table(name = "course_translate")
public class CourseTranslate {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "course_translate_id")
    private Long courseTranslateId;

    @JdbcTypeCode(SqlTypes.ARRAY)
    @Column(name = "what_will_be_learned", columnDefinition = "text[]")
    private List<String> whatWillBeLearned;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "language_id")
    private Language language;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "course_id")
    private Course course;
}

Табелата user е резервиран збор во PostgreSQL, па името мора да биде во наводници.

@Entity
@Table(name = "\"user\"")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "user_id")
    private Long userId;

    @Column(name = "is_verified")
    private Boolean isVerified;

    @Column(name = "is_profile_complete")
    private Boolean isProfileComplete;

    @Column(name = "has_used_free_consultation")
    private Boolean hasUsedFreeConsultation;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "account_id")
    private Account account;
}

Pooling

Pooling претставува процес каде што не се отвара нова конекција за секој request, туку има константен број на отворени конекции кои се доделуваат на различни requests. Со ова значително се намалува времето по request бидејќи не се губи време за отварање на нова и затворање на стара конекција.

Во мојот проект ова го правам со Hikari Pool во Java Spring Boot. Hikari е автоматски вклучено во Spring Boot (не треба дополнителна конфигурација за користење) преку spring-boot-starter-data-jpa библиотеката.

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Дополнително во application.properties фајлот за конфигурација може да се конфигурираат параметри за pooling

# Име на базенот
spring.datasource.hikari.pool-name=ShifterHikariPool

# Максимален број на конекции
spring.datasource.hikari.maximum-pool-size=20

# Минимум конекции кои секогаш ќе се спремни за да преземат нови requests
spring.datasource.hikari.minimum-idle=5

# Колку долго да се чека за конекција пред requestot да е одбиен (ако сите се зафатени)
spring.datasource.hikari.connection-timeout=30000

# Колку долго може да седи една конекција слободна (пред да биде затворена)
spring.datasource.hikari.idle-timeout=600000

# Максимално времетраење на една конекција (пред да биде заменета со нова)
spring.datasource.hikari.max-lifetime=1800000

# Дозволува драјверот да праќа стрингови без експлицитен тип, за PostgreSQL enum колоните
spring.datasource.hikari.data-source-properties.stringtype=unspecified
Note: See TracWiki for help on using the wiki.