TP6.3 - تمرين المطار-الرحلة-الراكب
الأهداف
سيمكنك هذا التمرين من:
- نمذجة العلاقات المعقدة بين كيانات JPA
- إنشاء استعلامات باستخدام تسميات Spring Data JPA
- استخدام @Query مع JPQL للاستعلامات المتقدمة
- اختبار مهاراتك في Spring Data JPA
المدة المقدرة: 2h30 - 3h
المستوى: متوسط إلى متقدم
السياق
يجب عليك إنشاء تطبيق لإدارة المطار مع:
- Airports (المطارات)
- Flights (الرحلات)
- Passengers (الركاب)
الجزء 1: نمذجة الكيانات
1.1 مخطط العلاقات
1.2 كيان المطار
أنشئ src/main/java/com/example/airport/model/Airport.java:
package com.example.airport.model;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.util.List;
@Entity
@Table(name = "airports")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Airport {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 3)
private String code; // Ex: "YUL", "JFK", "CDG"
@Column(nullable = false)
private String name; // Ex: "Montréal-Trudeau"
@Column(nullable = false)
private String city; // Ex: "Montréal"
@Column(nullable = false)
private String country; // Ex: "Canada"
@OneToMany(mappedBy = "departureAirport")
private List<Flight> departingFlights;
@OneToMany(mappedBy = "arrivalAirport")
private List<Flight> arrivingFlights;
}
1.3 كيان الرحلة
أنشئ src/main/java/com/example/airport/model/Flight.java:
package com.example.airport.model;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
@Entity
@Table(name = "flights")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Flight {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String flightNumber; // Ex: "AC301"
@ManyToOne
@JoinColumn(name = "departure_airport_id", nullable = false)
private Airport departureAirport;
@ManyToOne
@JoinColumn(name = "arrival_airport_id", nullable = false)
private Airport arrivalAirport;
@Column(nullable = false)
private LocalDateTime departureTime;
@Column(nullable = false)
private LocalDateTime arrivalTime;
@Column(nullable = false)
private Double price;
@Column(nullable = false)
private Integer capacity;
@ManyToMany
@JoinTable(
name = "flight_passengers",
joinColumns = @JoinColumn(name = "flight_id"),
inverseJoinColumns = @JoinColumn(name = "passenger_id")
)
private List<Passenger> passengers;
}
1.4 كيان الراكب
أنشئ src/main/java/com/example/airport/model/Passenger.java:
package com.example.airport.model;
import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.AllArgsConstructor;
import java.time.LocalDate;
import java.util.List;
@Entity
@Table(name = "passengers")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Passenger {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String firstName;
@Column(nullable = false)
private String lastName;
@Column(unique = true, nullable = false)
private String email;
@Column(unique = true, nullable = false)
private String passportNumber;
@Column(nullable = false)
private LocalDate birthDate;
@ManyToMany(mappedBy = "passengers")
private List<Flight> flights;
}
الجزء 2: بيانات الاختبار
2.1 ملف data.sql
أنشئ src/main/resources/data.sql:
-- Airports
INSERT INTO airports (code, name, city, country) VALUES ('YUL', 'Montréal-Trudeau International', 'Montréal', 'Canada');
INSERT INTO airports (code, name, city, country) VALUES ('YYZ', 'Toronto Pearson International', 'Toronto', 'Canada');
INSERT INTO airports (code, name, city, country) VALUES ('JFK', 'John F. Kennedy International', 'New York', 'USA');
INSERT INTO airports (code, name, city, country) VALUES ('LAX', 'Los Angeles International', 'Los Angeles', 'USA');
INSERT INTO airports (code, name, city, country) VALUES ('CDG', 'Charles de Gaulle', 'Paris', 'France');
INSERT INTO airports (code, name, city, country) VALUES ('LHR', 'Heathrow', 'Londres', 'UK');
INSERT INTO airports (code, name, city, country) VALUES ('FRA', 'Frankfurt', 'Francfort', 'Germany');
-- Flights
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('AC301', 1, 2, '2025-06-15 08:00:00', '2025-06-15 09:30:00', 250.00, 180);
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('AC401', 1, 3, '2025-06-15 10:00:00', '2025-06-15 11:45:00', 450.00, 200);
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('AF001', 1, 5, '2025-06-15 18:00:00', '2025-06-16 06:30:00', 850.00, 300);
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('BA123', 6, 3, '2025-06-16 09:00:00', '2025-06-16 12:00:00', 550.00, 250);
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('LH456', 7, 5, '2025-06-16 14:00:00', '2025-06-16 15:30:00', 200.00, 150);
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('AA789', 3, 4, '2025-06-17 07:00:00', '2025-06-17 10:30:00', 350.00, 180);
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('AC501', 2, 1, '2025-06-17 16:00:00', '2025-06-17 17:30:00', 250.00, 180);
INSERT INTO flights (flight_number, departure_airport_id, arrival_airport_id, departure_time, arrival_time, price, capacity)
VALUES ('AF002', 5, 1, '2025-06-18 22:00:00', '2025-06-19 06:00:00', 900.00, 300);
-- Passengers
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('Jean', 'Dupont', 'jean.dupont@email.com', 'FR123456', '1985-03-15');
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('Marie', 'Martin', 'marie.martin@email.com', 'FR789012', '1990-07-22');
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('John', 'Smith', 'john.smith@email.com', 'US345678', '1978-11-08');
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('Emma', 'Wilson', 'emma.wilson@email.com', 'UK901234', '1995-01-30');
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('Pierre', 'Bernard', 'pierre.bernard@email.com', 'CA567890', '1982-09-12');
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('Sophie', 'Tremblay', 'sophie.tremblay@email.com', 'CA123789', '2000-05-25');
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('Michael', 'Brown', 'michael.brown@email.com', 'US456123', '1970-12-03');
INSERT INTO passengers (first_name, last_name, email, passport_number, birth_date)
VALUES ('Anna', 'Schmidt', 'anna.schmidt@email.com', 'DE789456', '1988-04-18');
-- Flight-Passenger associations
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (1, 1);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (1, 2);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (1, 5);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (2, 1);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (2, 3);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (3, 1);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (3, 2);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (3, 4);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (3, 8);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (4, 3);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (4, 4);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (5, 8);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (6, 3);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (6, 7);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (7, 5);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (7, 6);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (8, 1);
INSERT INTO flight_passengers (flight_id, passenger_id) VALUES (8, 2);
الجزء 3: 20 استعلام لتنفيذها
مستوى سهل (الأسئلة 1-7)
أنشئ الطرق في المستودعات المناسبة.
السؤال 1: العثور على مطار بواسطة رمزه
المستودع: AirportRepository
التوقيع:
Optional<Airport> findByCode(String code);
الاختبار: العثور على المطار "YUL"
السؤال 2: قائمة بجميع المطارات في بلد معين
المستودع: AirportRepository
التوقيع:
List<Airport> findByCountry(String country);
الاختبار: العثور على جميع المطارات في كندا
السؤال 3: العثور على راكب بواسطة البريد الإلكتروني
المستودع: PassengerRepository
التوقيع:
Optional<Passenger> findByEmail(String email);
الاختبار: العثور على الراكب بالبريد الإلكتروني "jean.dupont@email.com"
السؤال 4: قائمة الركاب حسب اسم العائلة
المستودع: PassengerRepository
التوقيع:
List<Passenger> findByLastName(String lastName);
الاختبار: العثور على جميع الركاب الذين يحملون اسم "Smith"
السؤال 5: العثور على رحلة بواسطة رقمها
المستودع: FlightRepository
التوقيع:
Optional<Flight> findByFlightNumber(String flightNumber);
الاختبار: العثور على الرحلة "AC301"
السؤال 6: قائمة الرحلات التي يكون سعرها أقل من قيمة معينة
المستودع: FlightRepository
التوقيع:
List<Flight> findByPriceLessThan(Double maxPrice);
الاختبار: العثور على الرحلات التي تقل عن 300$
السؤال 7: قائمة الركاب الذين ولدوا قبل تاريخ معين
المستودع: PassengerRepository
التوقيع:
List<Passenger> findByBirthDateBefore(LocalDate date);
الاختبار: العثور على الركاب الذين ولدوا قبل عام 1990
مستوى متوسط (الأسئلة 8-14)
السؤال 8: قائمة الرحلات بين سعرين
المستودع: FlightRepository
التوقيع:
List<Flight> findByPriceBetween(Double minPrice, Double maxPrice);
الاختبار: الرحلات بين 200$ و 500$
السؤال 9: قائمة الرحلات المغادرة من مطار معين
المستودع: FlightRepository
التوقيع:
List<Flight> findByDepartureAirport_Code(String airportCode);
الاختبار: الرحلات المغادرة من "YUL"
السؤال 10: قائمة الرحلات الواصلة إلى بلد معين
المستودع: FlightRepository
التوقيع:
List<Flight> findByArrivalAirport_Country(String country);
الاختبار: الرحلات الواصلة إلى فرنسا
السؤال 11: البحث عن الركاب بواسطة الاسم (يحتوي)
المستودع: PassengerRepository
التوقيع:
List<Passenger> findByLastNameContainingIgnoreCase(String keyword);
الاختبار: الركاب الذين يحتوي اسمهم على "art"
السؤال 12: قائمة الرحلات بعد تاريخ/وقت معين
المستودع: FlightRepository
التوقيع:
List<Flight> findByDepartureTimeAfter(LocalDateTime dateTime);
الاختبار: الرحلات المغادرة بعد 16 يونيو 2025
السؤال 13: المطارات في مدينة معينة مع الترتيب حسب الاسم
المستودع: AirportRepository
التوقيع:
List<Airport> findByCityOrderByNameAsc(String city);
الاختبار: المطارات في نيويورك مرتبة حسب الاسم
السؤال 14: الرحلات ذات السعة الأكبر من قيمة معينة
المستودع: FlightRepository
التوقيع:
List<Flight> findByCapacityGreaterThanEqual(Integer minCapacity);
الاختبار: الرحلات التي تحتوي على 250 مقعدًا على الأقل
مستوى صعب (الأسئلة 15-20)
تتطلب هذه الاستعلامات @Query مع JPQL.
السؤال 15: العثور على الرحلات لراكب بواسطة بريده الإلكتروني
المستودع: FlightRepository
التوقيع:
@Query("SELECT f FROM Flight f JOIN f.passengers p WHERE p.email = :email")
List<Flight> findFlightsByPassengerEmail(@Param("email") String email);
الاختبار: الرحلات للراكب "jean.dupont@email.com"
السؤال 16: حساب عدد الركاب في رحلة معينة
المستودع: FlightRepository
التوقيع:
@Query("SELECT COUNT(p) FROM Flight f JOIN f.passengers p WHERE f.flightNumber = :flightNumber")
Long countPassengersByFlightNumber(@Param("flightNumber") String flightNumber);
الاختبار: عدد الركاب في الرحلة "AC301"
السؤال 17: العثور على الركاب في رحلة معينة
المستودع: PassengerRepository
التوقيع:
@Query("SELECT p FROM Passenger p JOIN p.flights f WHERE f.flightNumber = :flightNumber")
List<Passenger> findPassengersByFlightNumber(@Param("flightNumber") String flightNumber);
الاختبار: الركاب في الرحلة "AF001"
السؤال 18: الرحلات بين مطارين (حسب الرمز)
المستودع: FlightRepository
التوقيع:
@Query("SELECT f FROM Flight f WHERE f.departureAirport.code = :departureCode AND f.arrivalAirport.code = :arrivalCode")
List<Flight> findFlightsBetweenAirports(@Param("departureCode") String departureCode,
@Param("arrivalCode") String arrivalCode);
الاختبار: الرحلات من "YUL" إلى "CDG"
السؤال 19: متوسط سعر الرحلات المغادرة من بلد معين
المستودع: FlightRepository
التوقيع:
@Query("SELECT AVG(f.price) FROM Flight f WHERE f.departureAirport.country = :country")
Double findAveragePriceByDepartureCountry(@Param("country") String country);
الاختبار: متوسط سعر الرحلات المغادرة من كندا
السؤال 20: الركاب الذين قاموا بأكثر من N رحلة
المستودع: PassengerRepository
التوقيع:
@Query("SELECT p FROM Passenger p WHERE SIZE(p.flights) >= :minFlights")
List<Passenger> findFrequentTravelers(@Param("minFlights") int minFlights);
الاختبار: الركاب الذين قاموا برحلتين على الأقل
الجزء 4: الحل الكامل
AirportRepository.java
package com.example.airport.repository;
import com.example.airport.model.Airport;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
public interface AirportRepository extends JpaRepository<Airport, Long> {
// Q1: العثور على مطار بواسطة رمزه
Optional<Airport> findByCode(String code);
// Q2: قائمة بجميع المطارات في بلد معين
List<Airport> findByCountry(String country);
// Q13: المطارات في مدينة معينة مع الترتيب
List<Airport> findByCityOrderByNameAsc(String city);
}
PassengerRepository.java
package com.example.airport.repository;
import com.example.airport.model.Passenger;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Repository
public interface PassengerRepository extends JpaRepository<Passenger, Long> {
// Q3: العثور على راكب بواسطة البريد الإلكتروني
Optional<Passenger> findByEmail(String email);
// Q4: قائمة الركاب حسب اسم العائلة
List<Passenger> findByLastName(String lastName);
// Q7: الركاب الذين ولدوا قبل تاريخ معين
List<Passenger> findByBirthDateBefore(LocalDate date);
// Q11: البحث عن الركاب بواسطة الاسم (يحتوي)
List<Passenger> findByLastNameContainingIgnoreCase(String keyword);
// Q17: الركاب في رحلة معينة
@Query("SELECT p FROM Passenger p JOIN p.flights f WHERE f.flightNumber = :flightNumber")
List<Passenger> findPassengersByFlightNumber(@Param("flightNumber") String flightNumber);
// Q20: الركاب المتكررون (الذين قاموا برحلات N+)
@Query("SELECT p FROM Passenger p WHERE SIZE(p.flights) >= :minFlights")
List<Passenger> findFrequentTravelers(@Param("minFlights") int minFlights);
}
FlightRepository.java
package com.example.airport.repository;
import com.example.airport.model.Flight;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;
@Repository
public interface FlightRepository extends JpaRepository<Flight, Long> {
// Q5: العثور على رحلة بواسطة رقمها
Optional<Flight> findByFlightNumber(String flightNumber);
// Q6: الرحلات التي يكون سعرها أقل من قيمة معينة
List<Flight> findByPriceLessThan(Double maxPrice);
// Q8: الرحلات بين سعرين
List<Flight> findByPriceBetween(Double minPrice, Double maxPrice);
// Q9: الرحلات المغادرة من مطار معين (حسب الرمز)
List<Flight> findByDepartureAirport_Code(String airportCode);
// Q10: الرحلات الواصلة إلى بلد معين
List<Flight> findByArrivalAirport_Country(String country);
// Q12: الرحلات بعد تاريخ/وقت معين
List<Flight> findByDepartureTimeAfter(LocalDateTime dateTime);
// Q14: الرحلات ذات السعة الأكبر من قيمة معينة
List<Flight> findByCapacityGreaterThanEqual(Integer minCapacity);
// Q15: الرحلات لراكب بواسطة بريده الإلكتروني
@Query("SELECT f FROM Flight f JOIN f.passengers p WHERE p.email = :email")
List<Flight> findFlightsByPassengerEmail(@Param("email") String email);
// Q16: حساب عدد الركاب في رحلة معينة
@Query("SELECT COUNT(p) FROM Flight f JOIN f.passengers p WHERE f.flightNumber = :flightNumber")
Long countPassengersByFlightNumber(@Param("flightNumber") String flightNumber);
// Q18: الرحلات بين مطارين
@Query("SELECT f FROM Flight f WHERE f.departureAirport.code = :departureCode AND f.arrivalAirport.code = :arrivalCode")
List<Flight> findFlightsBetweenAirports(@Param("departureCode") String departureCode,
@Param("arrivalCode") String arrivalCode);
// Q19: متوسط سعر الرحلات حسب بلد المغادرة
@Query("SELECT AVG(f.price) FROM Flight f WHERE f.departureAirport.country = :country")
Double findAveragePriceByDepartureCountry(@Param("country") String country);
}
الجزء 5: الاختبارات والتحقق
application.properties
spring.datasource.url=jdbc:h2:mem:airportdb
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.defer-datasource-initialization=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.sql.init.mode=always
فئة الاختبار
أنشئ فئة اختبار للتحقق من استعلاماتك:
package com.example.airport;
import com.example.airport.model.*;
import com.example.airport.repository.*;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class AirportApplicationTests {
@Autowired
private AirportRepository airportRepository;
@Autowired
private FlightRepository flightRepository;
@Autowired
private PassengerRepository passengerRepository;
@Test
void q1_findAirportByCode() {
var airport = airportRepository.findByCode("YUL");
assertTrue(airport.isPresent());
assertEquals("Montréal", airport.get().getCity());
}
@Test
void q2_findAirportsByCountry() {
var airports = airportRepository.findByCountry("Canada");
assertEquals(2, airports.size());
}
@Test
void q6_findFlightsByPriceLessThan() {
var flights = flightRepository.findByPriceLessThan(300.0);
assertFalse(flights.isEmpty());
flights.forEach(f -> assertTrue(f.getPrice() < 300));
}
@Test
void q15_findFlightsByPassengerEmail() {
var flights = flightRepository.findFlightsByPassengerEmail("jean.dupont@email.com");
assertFalse(flights.isEmpty());
System.out.println("Vols de Jean Dupont: " + flights.size());
}
@Test
void q16_countPassengersByFlight() {
Long count = flightRepository.countPassengersByFlightNumber("AC301");
assertEquals(3, count);
}
@Test
void q20_findFrequentTravelers() {
var travelers = passengerRepository.findFrequentTravelers(2);
assertFalse(travelers.isEmpty());
System.out.println("Voyageurs fréquents: " + travelers.size());
}
}
معايير التقييم
| المعيار | النقاط |
|---|---|
| الكيانات نمذجت بشكل صحيح | 15% |
| العلاقات JPA صحيحة (@ManyToOne, @ManyToMany) | 15% |
| الأسئلة 1-7 (سهل) | 21% (3% لكل منها) |
| الأسئلة 8-14 (متوسط) | 28% (4% لكل منها) |
| الأسئلة 15-20 (صعب) | 18% (3% لكل منها) |
| الاختبارات ناجحة | 3% |
| المجموع | 100% |
نصائح
لتسميات التسمية:
findBy+PropertyName= بحث بسيط_يسمح بالتنقل في العلاقات (مثلfindByDepartureAirport_Code)Containing= LIKE %keyword%IgnoreCase= بحث غير حساس لحالة الأحرف
لـ @Query:
- JPQL يستخدم أسماء الفئات في Java، وليس أسماء الجداول في SQL
JOINللتنقل في العلاقاتSIZE()لحساب عناصر المجموعةAVG(),COUNT(),SUM()للتجميعات