wiki:dbdevelopment

Version 4 (modified by 231175, 29 hours ago) ( diff )

--

Напреден развој на базата

Правила за запишување на курс (Enrollment)

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Кога корисникот се запишува на курс по даден course_id, автоматски се избира и запишува моментално активната верзија на курсот (course_version.active = true) за тој курс.
  • Не е дозволено запишување на курс за кој воопшто нема активна верзија.
  • Секоја верзија се третира независно: корисникот може да има повеќе запишувања на различни верзии на истиот курс, без разлика дали претходните се завршени или не.

Имплементација

Тригери

BEFORE INSERT тригер на enrollment за автоматско доделување на активната верзија на curse (course version).

CREATE OR REPLACE FUNCTION set_active_course_version_on_enrollment()
RETURNS TRIGGER 
AS $$
DECLARE
    v_course_version_id INTEGER;
BEGIN
    SELECT cv.id
    INTO v_course_version_id
    FROM course_version cv
    WHERE cv.course_id = NEW.course_id
      AND cv.active = TRUE
    ORDER BY cv.version_number DESC
    LIMIT 1;
    
    IF v_course_version_id IS NULL THEN
        RAISE EXCEPTION 'No active course version found for course_id=%', NEW.course_id;
    END IF;
    
    NEW.course_version_id := v_course_version_id;
    
    RETURN NEW;
END;
$$ 
LANGUAGE plpgsql;

CREATE TRIGGER trg_set_active_course_version_on_enrollment
BEFORE INSERT ON enrollment
FOR EACH ROW
EXECUTE FUNCTION set_active_course_version_on_enrollment();

Функции / Stored Procedures

Функција за креирање на нов enrollment. При креирање се зема најновата активна верзија на курсот.

CREATE OR REPLACE FUNCTION create_enrollment_for_active_version(p_user_id INTEGER, p_course_id INTEGER)
RETURNS INTEGER 
AS $$
DECLARE
    v_course_version_id INTEGER;
    v_enrollment_id INTEGER;
BEGIN
    SELECT cv.id
    INTO v_course_version_id
    FROM course_version cv
    WHERE cv.course_id = p_course_id
      AND cv.active = TRUE
    ORDER BY cv.version_number DESC
    LIMIT 1;
    
    IF v_course_version_id IS NULL THEN
        RAISE EXCEPTION 'No active course version found for course_id=%', p_course_id;
    END IF;
    
    INSERT INTO enrollment (user_id, course_id, course_version_id, enrollment_purchase_date, status)
    VALUES (p_user_id, p_course_id, v_course_version_id, NOW(), 'PENDING')
    RETURNING id INTO v_enrollment_id;
    
    RETURN v_enrollment_id;
END;
$$ 
LANGUAGE plpgsql;

Погледи (Views)

Поглед за генерирање на основни информации за кои enrollments имаат застарена верзија од курсот, а кои најнова.

CREATE OR REPLACE VIEW enrollments_with_active_version AS
SELECT
    e.id AS enrollment_id,
    u.id AS user_id,
    u.name AS user_name,
    c.id AS course_id,
    ct.title_short AS course_title,
    cv.id AS course_version_id,
    cv.version_number AS course_version_number,
    cv.active AS is_version_active,
    e.enrollment_purchase_date AS purchase_date,
    e.status AS enrollment_status
FROM enrollment e
JOIN user u ON e.user_id = u.id
JOIN course c ON e.course_id = c.id
JOIN course_version cv ON e.course_version_id = cv.id
JOIN course_translate ct ON c.id = ct.course_id AND ct.language = 'mk';

Менаџирање на верзии на курс (Course Version Management)

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Само една верзија може да биде активна (active = true) по курс во исто време
  • Кога се активира нова верзија, претходните активни верзии на истиот курс автоматски се деактивираат
  • Не смее да се избрише верзија која има активни (незавршени) запишувања (enrollments)

Имплементација

Тригери

BEFORE INSERT тригер во course_version, каде само таа верзија што се креира е active, останатите не се. Дополнително се ажурираат и уште некои атрибути.

CREATE OR REPLACE FUNCTION ensure_single_active_version_when_created()
RETURNS TRIGGER 
AS $$
DECLARE
    v_num_new_version INTEGER;
BEGIN
    UPDATE course_version
    SET active = FALSE
    WHERE course_id = NEW.course_id;

    SELECT COALESCE(MAX(version_number), 0) + 1 INTO v_num_new_version
    FROM course_version
    WHERE course_id = NEW.course_id;

    NEW.active := TRUE;
    NEW.creation_date := CURRENT_DATE;
    NEW.version_number := v_num_new_version;
    
    RETURN NEW;
END;
$$ 
LANGUAGE plpgsql;

CREATE TRIGGER trg_ensure_single_active_version_when_created
BEFORE INSERT ON course_version
FOR EACH ROW
EXECUTE FUNCTION ensure_single_active_version_when_created();

BEFORE UPDATE тригер за атрибутот active во course_version, со цел осигурување дека постои само една активна верзија од курсот.

CREATE OR REPLACE FUNCTION ensure_single_active_version()
RETURNS TRIGGER 
AS $$
BEGIN
    IF NEW.active = TRUE THEN
        UPDATE course_version
        SET active = FALSE
        WHERE course_id = NEW.course_id
          AND id != NEW.id
          AND active = TRUE;
    END IF;
    
    RETURN NEW;
END;
$$ 
LANGUAGE plpgsql;

CREATE TRIGGER trg_ensure_single_active_version
BEFORE UPDATE OF active ON course_version
FOR EACH ROW
WHEN (NEW.active = TRUE)
EXECUTE FUNCTION ensure_single_active_version();

BEFORE DELETE тригер за course version, со цел осигурување дека course version не може да биде избришано доколку веќе постојат enrollments за него.

CREATE OR REPLACE FUNCTION prevent_version_deletion_with_active_enrollments()
RETURNS TRIGGER 
AS $$
DECLARE
    v_active_enrollments INTEGER;
BEGIN
    SELECT COUNT(*) INTO v_active_enrollments
    FROM enrollment
    WHERE course_version_id = OLD.id;
    
    IF v_active_enrollments > 0 THEN
        RAISE EXCEPTION 'Cannot delete course version with % active enrollments', v_active_enrollments;
    END IF;
    
    RETURN OLD;
END;
$$ 
LANGUAGE plpgsql;

CREATE TRIGGER trg_prevent_version_deletion
BEFORE DELETE ON course_version
FOR EACH ROW
EXECUTE FUNCTION prevent_version_deletion_with_active_enrollments();

Погледи (Views)

Поглед за преглед на курсевите со нивните најнови верзии и enrollments.

CREATE OR REPLACE VIEW course_latest_versions AS
SELECT
    c.id AS course_id,
    ct.title_short AS course_title,
    cv.id AS version_id,
    cv.version_number,
    cv.active,
    cv.version_creation_date,
    COUNT(DISTINCT e.id) FILTER (WHERE e.completion_date IS NULL) AS active_enrollments,
    COUNT(DISTINCT e.id) AS total_enrollments
FROM course c
JOIN course_version cv ON c.id = cv.course_id
JOIN course_translate ct ON c.id = ct.course_id AND ct.language = 'mk'
LEFT JOIN enrollment e ON cv.id = e.course_version_id
WHERE cv.version_number = (
    SELECT MAX(version_number)
    FROM course_version
    WHERE course_id = c.id
)
GROUP BY c.id, ct.title_short, cv.id, cv.version_number, cv.active, cv.version_creation_date;

Валидација на рецензии и рејтинг (Review Validation & Rating)

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Корисникот може да остави рецензија само ако enrollment е завршен (completion_date IS NOT NULL)
  • Еден корисник може да остави само една рецензија по enrollment
  • Рејтингот мора да биде валиден број од 1 до 5
  • Сите промени на рецензии се евидентираат во audit табела

Имплементација

Прилагодени домени (Custom Domains)

Ограничување дека рејтинг мора да биде во доменот [1, 5]

CREATE DOMAIN rating_scale AS INTEGER
CHECK (VALUE >= 1 AND VALUE <= 5);

ALTER TABLE review ALTER COLUMN rating TYPE rating_scale;

Тригери

BEFORE INSERT тригер за review, каде се прекинува внесувањето на review доколку курсот (enrollment) не е завршен.

CREATE OR REPLACE FUNCTION validate_review_before_insert()
RETURNS TRIGGER AS $$
DECLARE
    v_completion_date TIMESTAMP;
    v_existing_review_count INTEGER;
BEGIN
    SELECT completion_date INTO v_completion_date
    FROM enrollment
    WHERE id = NEW.enrollment_id;
    
    IF v_completion_date IS NULL THEN
        RAISE EXCEPTION 'Cannot create review for incomplete enrollment';
    END IF;
    
    SELECT COUNT(*) INTO v_existing_review_count
    FROM review
    WHERE enrollment_id = NEW.enrollment_id;
    
    IF v_existing_review_count > 0 THEN
        RAISE EXCEPTION 'Review already exists for this enrollment';
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_validate_review
BEFORE INSERT ON review
FOR EACH ROW
EXECUTE FUNCTION validate_review_before_insert();

Погледи (Views)

Поглед за преглед на секој курс со неговата просечна оценка и дистрибуција на оценки.

CREATE OR REPLACE VIEW course_average_ratings AS
SELECT
    c.id AS course_id,
    ct.title_short AS course_title,
    COUNT(r.id) AS total_reviews,
    AVG(r.rating)::NUMERIC(3,2) AS average_rating,
    COUNT(r.id) FILTER (WHERE r.rating = 5) AS five_star_count,
    COUNT(r.id) FILTER (WHERE r.rating = 4) AS four_star_count,
    COUNT(r.id) FILTER (WHERE r.rating = 3) AS three_star_count,
    COUNT(r.id) FILTER (WHERE r.rating = 2) AS two_star_count,
    COUNT(r.id) FILTER (WHERE r.rating = 1) AS one_star_count
FROM course c
JOIN course_translate ct ON c.id = ct.course_id AND ct.language = 'mk'
LEFT JOIN course_version cv ON c.id = cv.course_id
LEFT JOIN enrollment e ON cv.id = e.course_version_id
LEFT JOIN review r ON e.id = r.enrollment_id
GROUP BY c.id, ct.title_short;

Следење и ограничување на бесплатни консултации

Опис на барањата за податочни ограничувања

Системот мора да обезбеди дека:

  • Корисникот може да користи само една бесплатна консултација
  • Може да се креира meeting_reminder за бесплатна консултација само ако корисникот сè уште не ја искористил
  • После креирање на meeting_reminder за бесплатна консултација, флагот used_free_consultation автоматски се поставува на TRUE

Имплементација

Тригери

BEFORE INSERT тригер на meeting reminder, каде не смее да се креира нов митинг и потсетување по емаил доколку корисникот веќе имал бесплатна консултација.

CREATE OR REPLACE FUNCTION check_free_consultation_eligibility()
RETURNS TRIGGER AS $$
DECLARE
    v_used_free_consultation BOOLEAN;
BEGIN
    SELECT used_free_consultation INTO v_used_free_consultation
    FROM _user
    WHERE id = NEW.user_id;
    
    IF v_used_free_consultation = TRUE THEN
        RAISE EXCEPTION 'User has already used their free consultation';
    END IF;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_check_free_consultation_before_meeting
BEFORE INSERT ON meeting_reminder
FOR EACH ROW
EXECUTE FUNCTION check_free_consultation_eligibility();

AFTER INSERT тригер на meeting reminder, каде одкако ќе се закаже состанокот да се маркира дека тој корисник го има искористено своето право за бесплатна консултативна сесија со експерт.

CREATE OR REPLACE FUNCTION mark_free_consultation_as_used()
RETURNS TRIGGER AS $$
BEGIN
    UPDATE _user
    SET used_free_consultation = TRUE
    WHERE id = NEW.user_id
      AND used_free_consultation = FALSE;
    
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_mark_free_consultation_used
AFTER INSERT ON meeting_reminder
FOR EACH ROW
EXECUTE FUNCTION mark_free_consultation_as_used();

Функции / Stored Procedures

Функција која кажува дали корисникот може да закаже бесплатна консултативна сесија со експерт.

CREATE OR REPLACE FUNCTION can_user_schedule_free_consultation(p_user_id INTEGER)
RETURNS TABLE(
    can_schedule BOOLEAN,
    reason TEXT
) 
AS $$
DECLARE
    v_used_free_consultation BOOLEAN;
BEGIN
    SELECT used_free_consultation INTO v_used_free_consultation
    FROM _user
    WHERE id = p_user_id;
    
    IF v_used_free_consultation = TRUE THEN
        RETURN QUERY SELECT FALSE, 'Free consultation already used';
        RETURN;
    END IF;
    
    RETURN QUERY SELECT TRUE, 'Eligible for free consultation';
END;
$$ 
LANGUAGE plpgsql;
Note: See TracWiki for help on using the wiki.