انتقل إلى المحتوى الرئيسي

TP3 - ترحيل المخطط باستخدام Flyway

أهداف التمرين​

في هذا التمرين، سوف تقوم بـ:

  1. ✅ تكوين Flyway في مشروع Spring Boot
  2. ✅ إنشاء سكربتات ترحيل مرفقة بإصدارات
  3. ✅ محاكاة تطور مخطط قاعدة البيانات
  4. ✅ إدارة البيانات الأولية باستخدام Flyway
  5. ✅ استخدام الترحيلات المتكررة

المدة المقدرة: 1 ساعة و30 دقيقة


السياق​

أنت تطور تطبيق تجارة إلكترونية. مع مرور الوقت، يتطور المخطط:

  1. V1: إنشاء الجداول الأساسية (users، products)
  2. V2: إضافة أعمدة (email إلى users)
  3. V3: إنشاء جدول orders
  4. V4: إضافة فهارس للأداء
  5. V5: إدخال بيانات مرجعية

الجزء 1: تكوين المشروع​

1.1 إنشاء المشروع​

قم بإنشاء مشروع Spring Boot جديد مع:

  • Artifact: flyway-demo
  • Dependencies:
    • Spring Web
    • Spring Data JPA
    • H2 Database
    • PostgreSQL Driver
    • Flyway Migration
    • Lombok

1.2 تبعيات Maven​

<!-- pom.xml - تحقق من أن Flyway موجود -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>

<!-- لـ PostgreSQL (اختياري) -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-database-postgresql</artifactId>
</dependency>

1.3 هيكل المجلدات​

src/main/resources/
├── application.properties
├── application-dev.properties
├── application-prod.properties
└── db/
└── migration/
├── V1__create_users_table.sql
├── V2__add_email_to_users.sql
├── V3__create_products_table.sql
├── V4__create_orders_table.sql
├── V5__add_indexes.sql
├── V6__insert_reference_data.sql
└── R__create_views.sql

1.4 التكوين​

# application.properties
spring.profiles.active=dev

# Flyway
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=false

# Logging
logging.level.org.flywaydb=DEBUG
# application-dev.properties
spring.datasource.url=jdbc:h2:mem:flywaydb;DB_CLOSE_DELAY=-1
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

# JPA لا يدير المخطط - Flyway يتولى ذلك!
spring.jpa.hibernate.ddl-auto=validate
spring.jpa.show-sql=true

الجزء 2: الترحيل الأول - المستخدمون​

2.1 سكربت V1​

قم بإنشاء الملف src/main/resources/db/migration/V1__create_users_table.sql:

-- V1__create_users_table.sql
-- إنشاء جدول المستخدمين

CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
password VARCHAR(100) NOT NULL,
first_name VARCHAR(50),
last_name VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT uk_users_username UNIQUE (username)
);

-- تعليق للتوثيق
COMMENT ON TABLE users IS 'جدول المستخدمين للتطبيق';

2.2 الكيان المقابل​

// src/main/java/com/example/flywaydemo/model/User.java
package com.example.flywaydemo.model;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User {

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

@Column(nullable = false, unique = true, length = 50)
private String username;

@Column(nullable = false, length = 100)
private String password;

@Column(name = "first_name", length = 50)
private String firstName;

@Column(name = "last_name", length = 50)
private String lastName;

@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();

@Column(name = "updated_at")
private LocalDateTime updatedAt = LocalDateTime.now();
}

2.3 اختبار​

# بدء التطبيق
mvn spring-boot:run

# تحقق من السجلات - يجب أن ترى:
# Flyway Community Edition
# Successfully validated 1 migration
# Migrating schema "PUBLIC" to version "1 - create users table"
# Successfully applied 1 migration

التحقق من H2:

  1. افتح http://localhost:8080/h2-console
  2. URL: jdbc:h2:mem:flywaydb
  3. تحقق من أن الجدول users موجود
  4. تحقق من أن الجدول flyway_schema_history موجود
SELECT * FROM flyway_schema_history;

الجزء 3: تطور المخطط​

3.1 V2 - إضافة عمود​

-- V2__add_email_to_users.sql
-- إضافة عمود email

ALTER TABLE users ADD COLUMN email VARCHAR(100);

-- إنشاء فهرس على البريد الإلكتروني
CREATE UNIQUE INDEX idx_users_email ON users(email);

تحديث الكيان:

// إضافة في User.java
@Column(unique = true, length = 100)
private String email;

3.2 V3 - جدول جديد للمنتجات​

-- V3__create_products_table.sql
-- جدول المنتجات

CREATE TABLE products (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
stock_quantity INT NOT NULL DEFAULT 0,
category VARCHAR(50),
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT chk_price_positive CHECK (price >= 0),
CONSTRAINT chk_stock_positive CHECK (stock_quantity >= 0)
);
// Product.java
package com.example.flywaydemo.model;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.time.LocalDateTime;

@Entity
@Table(name = "products")
@Data
@NoArgsConstructor
public class Product {

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

@Column(nullable = false, length = 100)
private String name;

@Column(columnDefinition = "TEXT")
private String description;

@Column(nullable = false, precision = 10, scale = 2)
private BigDecimal price;

@Column(name = "stock_quantity", nullable = false)
private Integer stockQuantity = 0;

@Column(length = 50)
private String category;

@Column(nullable = false)
private Boolean active = true;

@Column(name = "created_at")
private LocalDateTime createdAt = LocalDateTime.now();
}

3.3 V4 - جدول الطلبات مع FK​

-- V4__create_orders_table.sql
-- جدول الطلبات مع علاقة إلى المستخدمين

CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(20) DEFAULT 'PENDING',
total_amount DECIMAL(12, 2) NOT NULL,
shipping_address TEXT,

CONSTRAINT fk_orders_user FOREIGN KEY (user_id)
REFERENCES users(id) ON DELETE CASCADE
);

-- جدول الربط بين الطلبات والمنتجات
CREATE TABLE order_items (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
unit_price DECIMAL(10, 2) NOT NULL,

CONSTRAINT fk_order_items_order FOREIGN KEY (order_id)
REFERENCES orders(id) ON DELETE CASCADE,
CONSTRAINT fk_order_items_product FOREIGN KEY (product_id)
REFERENCES products(id)
);

3.4 V5 - فهارس للأداء​

-- V5__add_indexes.sql
-- إضافة فهارس لتحسين الأداء

-- فهرس على تاريخ الطلب
CREATE INDEX idx_orders_date ON orders(order_date);

-- فهرس على الحالة
CREATE INDEX idx_orders_status ON orders(status);

-- فهرس على المستخدم
CREATE INDEX idx_orders_user ON orders(user_id);

-- فهرس على فئة المنتج
CREATE INDEX idx_products_category ON products(category);

-- فهرس على المنتجات النشطة
CREATE INDEX idx_products_active ON products(active) WHERE active = TRUE;

3.5 V6 - بيانات مرجعية​

-- V6__insert_reference_data.sql
-- إدخال البيانات المرجعية

-- المستخدم admin
INSERT INTO users (username, password, email, first_name, last_name)
VALUES ('admin', '$2a$10$hashedpassword', 'admin@example.com', 'Admin', 'System');

-- فئات المنتجات (عبر منتجات العرض)
INSERT INTO products (name, description, price, stock_quantity, category) VALUES
('Laptop Pro', 'حاسوب محمول عالي الأداء', 1299.99, 50, 'ELECTRONICS'),
('Smartphone X', 'هاتف من الجيل الأخير', 899.99, 100, 'ELECTRONICS'),
('T-Shirt Basic', 'تي شيرت 100% قطن', 19.99, 500, 'CLOTHING'),
('Jeans Classic', 'جينز بقصة مستقيمة', 49.99, 200, 'CLOTHING'),
('Livre Java', 'دليل شامل لجافا', 39.99, 75, 'BOOKS');

الجزء 4: الترحيلات المتكررة​

4.1 إنشاء عرض​

الترحيلات المتكررة يتم إعادة تنفيذها إذا تغير الملف.

-- R__create_views.sql
-- عرض المنتجات النشطة مع المخزون

CREATE OR REPLACE VIEW v_available_products AS
SELECT
id,
name,
price,
stock_quantity,
category
FROM products
WHERE active = TRUE
AND stock_quantity > 0;

-- عرض الإحصائيات حسب الفئة
CREATE OR REPLACE VIEW v_category_stats AS
SELECT
category,
COUNT(*) as product_count,
AVG(price) as avg_price,
SUM(stock_quantity) as total_stock
FROM products
WHERE active = TRUE
GROUP BY category;

4.2 اختبار​

قم بتعديل العرض وأعد التشغيل:

-- تعديل R__create_views.sql
CREATE OR REPLACE VIEW v_available_products AS
SELECT
id,
name,
price,
stock_quantity,
category,
'IN_STOCK' as availability -- عمود جديد!
FROM products
WHERE active = TRUE
AND stock_quantity > 0;

عند إعادة التشغيل، يكتشف Flyway تغيير checksum ويعيد تنفيذ السكربت.


الجزء 5: التحقق والأوامر​

5.1 التحقق من حالة الترحيلات​

افتح وحدة التحكم H2 ونفذ:

-- التاريخ الكامل للترحيلات
SELECT
installed_rank,
version,
description,
type,
installed_on,
execution_time,
success
FROM flyway_schema_history
ORDER BY installed_rank;

النتيجة المتوقعة:

installed_rankversiondescriptiontypesuccess
11create users tableSQLtrue
22add email to usersSQLtrue
33create products tableSQLtrue
44create orders tableSQLtrue
55add indexesSQLtrue
66insert reference dataSQLtrue
7NULLcreate viewsSQLtrue

5.2 أوامر Maven (اختياري)​

أضف الإضافة Flyway في pom.xml:

<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<configuration>
<url>jdbc:h2:file:./target/flywaydb</url>
<user>sa</user>
<password></password>
</configuration>
</plugin>
# عرض الحالة
mvn flyway:info

# تطبيق الترحيلات
mvn flyway:migrate

# التحقق
mvn flyway:validate

الجزء 6: إدارة الأخطاء​

6.1 محاكاة خطأ​

قم بإنشاء سكربت يحتوي على خطأ:

-- V7__broken_migration.sql
-- سكربت يحتوي على خطأ في الصياغة

CREATE TABLE broken_table (
id BIGINT PRIMARY KEY
name VARCHAR(50) -- فاصلة مفقودة!
);

أعد تشغيل التطبيق → خطأ!

6.2 التصحيح​

  1. الخيار A: تصحيح السكربت واستخدام repair (للتطوير فقط)
# وضع علامة على أنه تم حله
mvn flyway:repair

# إعادة التطبيق
mvn flyway:migrate
  1. الخيار B: حذف الملف V7 والسطر في flyway_schema_history

  2. الخيار C (للإنتاج): إنشاء V8 للتصحيح

-- V8__fix_broken_table.sql
-- تصحيح الخطأ في V7

DROP TABLE IF EXISTS broken_table;

CREATE TABLE broken_table (
id BIGINT PRIMARY KEY,
name VARCHAR(50)
);

6.3 القاعدة الذهبية​

خطر

لا تقم أبدًا بتعديل سكربت ترحيل تم تنفيذه بالفعل في الإنتاج!

قم دائمًا بإنشاء سكربت جديد لحل المشاكل.


الجزء 7: ترحيل PostgreSQL​

7.1 تكوين الإنتاج​

# application-prod.properties
spring.datasource.url=jdbc:postgresql://localhost:5432/flywaydb
spring.datasource.driver-class-name=org.postgresql.Driver
spring.datasource.username=postgres
spring.datasource.password=secret

spring.h2.console.enabled=false

spring.jpa.hibernate.ddl-auto=validate
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

# Flyway
spring.flyway.enabled=true
spring.flyway.clean-disabled=true

7.2 تعديل السكربتات (إذا لزم الأمر)​

بعض الصياغات تختلف بين H2 و PostgreSQL:

-- H2
CREATE TABLE users (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
...
);

-- PostgreSQL
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
...
);

الحل: استخدام سكربتات خاصة بكل بيئة:

db/migration/
├── h2/
│ └── V1__create_tables.sql
└── postgresql/
└── V1__create_tables.sql
# application-dev.properties
spring.flyway.locations=classpath:db/migration/h2

# application-prod.properties
spring.flyway.locations=classpath:db/migration/postgresql

الجزء 8: نقطة نهاية للتحقق​

8.1 Controller للتحقق من الترحيلات​

// src/main/java/com/example/flywaydemo/controller/MigrationController.java
package com.example.flywaydemo.controller;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.MigrationInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@RestController
@RequestMapping("/api/migrations")
public class MigrationController {

@Autowired
private Flyway flyway;

@GetMapping
public List<Map<String, Object>> getMigrations() {
return Arrays.stream(flyway.info().all())
.map(this::toMap)
.collect(Collectors.toList());
}

@GetMapping("/pending")
public List<Map<String, Object>> getPendingMigrations() {
return Arrays.stream(flyway.info().pending())
.map(this::toMap)
.collect(Collectors.toList());
}

private Map<String, Object> toMap(MigrationInfo info) {
return Map.of(
"version", info.getVersion() != null ? info.getVersion().toString() : "N/A",
"description", info.getDescription(),
"type", info.getType().name(),
"state", info.getState().name(),
"installedOn", info.getInstalledOn() != null ? info.getInstalledOn().toString() : "N/A"
);
}
}

اختبار:

curl http://localhost:8080/api/migrations

ملخص​

الملفات التي تم إنشاؤها​

الملفالوصف
V1__create_users_table.sqlجدول المستخدمين الأساسي
V2__add_email_to_users.sqlإضافة عمود البريد الإلكتروني
V3__create_products_table.sqlجدول المنتجات
V4__create_orders_table.sqlجداول الطلبات وعناصر الطلب
V5__add_indexes.sqlفهارس الأداء
V6__insert_reference_data.sqlالبيانات الأولية
R__create_views.sqlالعروض (متكررة)

الأوامر الرئيسية​

الأمرالإجراء
mvn flyway:infoعرض حالة الترحيلات
mvn flyway:migrateتطبيق الترحيلات
mvn flyway:validateالتحقق من التناسق
mvn flyway:repairإصلاح التاريخ
mvn flyway:cleanتنظيف قاعدة البيانات (خطر!)

ما تعلمته​

  1. ✅ تكوين Flyway مع Spring Boot
  2. ✅ تسمية السكربتات بشكل صحيح (V1__، V2__، R__)
  3. ✅ إنشاء ترحيلات تدريجية
  4. ✅ إدارة البيانات المرجعية
  5. ✅ استخدام الترحيلات المتكررة للعروض
  6. ✅ تصحيح أخطاء الترحيل

🎯 الوحدة مكتملة!​

تهانينا! لقد أكملت الوحدة 7 حول قواعد البيانات والمعاملات.

ملخص الوحدة:

  • الفصل 1: تكوين مصادر البيانات (HikariCP)
  • الفصل 2: إدارة المعاملات (@Transactional)
  • الفصل 3: الترحيل باستخدام Flyway
  • الفصل 4: الترحيل باستخدام Liquibase
  • TP1: تكوين مصدر البيانات
  • TP2: المعاملات والتراجع
  • TP3: ترحيل Flyway (هذا التمرين)

➡️ العودة إلى الوحدة 7


⬅️ العودة إلى خطة الدورة