JOOQ وحفرة أرنبه. كيف تعيش بدون السبات

في هذه المقالة لن أغرق في JOOQ. أفضل السبات وكل قوة Spring Data JPA وراءه. لكن المقالة لن تكون عنهم.



عندما نستخدم Hibernate و Spring Data JPA ، لا نحتاج إلى التفكير في العمليات الداخلية - معرفة التعليقات التوضيحية وكتابة أسماء الطرق الصحيحة في المستودع - سوف يقوم هذان الوحوش بالباقي لك. في حالة JOOQ ، لسوء الحظ بالنسبة للكثيرين ، يجب عليك الضغط قليلاً والكتابة أكثر من findAllByUserId (Long userId) .

ما هو JOOQ؟


دعنا نكتب استعلام SQL بسيط:

select * from countries c where c.population > 10000000;

يمكننا تنفيذ هذا الطلب في وحدة التحكم. حسنا

لا نشعر في وحدة التحكم. نريد طلبنا أن يرسل هذا الطلب. أنه لم يكن برنامج نصي لمرة واحدة وأنه تم التحقق منه على الأقل لبناء الجملة. نقوم بذلك من خلال Spring Data JPA:

List<Country> findAllByPopulationAfter(Long amount);

يتم تنفيذ نفس الطلب على النحو الوارد أعلاه ، ولكن بحلول الربيع.

ما هي المزايا الواضحة لهذا النهج؟ يتم تنفيذه من خلال إطار عمل قوي ؛ كما أنه يتحقق من صحة الأخطاء. لكن إدارة الاستعلام تهتم بالإطار. ونريد إدارة الطلب بالكامل ، ولكن في نفس الوقت ، حتى يتم التحقق من صحة الطلب تمامًا.
استعمالالاستعلام:

@Query("select c from Country c where c.population > :amount")
List<Country> findAllPopulationGreaterThan(@Param("amount") Long amount);

مثل هذه المقايضة بين SQL و DSL جيدة أيضًا. ولكن إذا لم نرغب في العبث بـ SQL ، فسيكون من دواعي سرورنا رؤية شيء مثل:

return dsl.selectFrom(COUNTRIES)
                .where(COUNTRIES.POPULATION.greaterThan(amount))
                .fetch();

العديد من المكتبات مناسبة لهذا:

QueryDSL
JOOQ
Speedment


About QueryDSL كتبت قبل بضع سنوات. لم أحفر Speedment ، ولكن يبدو أنه شيء أكثر من مجرد مولد DSL بسيط ، بالإضافة إلى أنه سيتعين علي معرفة الطرق التي قاموا من خلالها بتشفير أوامر استعلام SQL. بشكل عام ، سنتحدث اليوم عن JOOQ.

استفسارات JOOQ


نعم ، لقد رأينا بالفعل أحد الاستعلامات أعلاه:

return dsl.selectFrom(COUNTRIES)
                .where(COUNTRIES.POPULATION.greaterThan(amount))
                .fetch();

ماذا هناك؟

على سبيل المثال ، طلب الحصول البسيط:

return dsl.selectFrom(Countries.COUNTRIES)
                .where(Countries.COUNTRIES.ID.eq(id))
                .fetchAny();

أو أدخل الطلب:

return dsl.insertInto(Countries.COUNTRIES)
                .set(Countries.COUNTRIES.NAME, country.getName())
                .set(Countries.COUNTRIES.POPULATION, country.getPopulation())
                .set(Countries.COUNTRIES.GOVERNMENT_FORM, nameOrNull(country.getGovernmentForm()))
                .returning()
                .fetchOne();

كما ترون ، كل شيء واضح ، لا أكثر. ولكن هذا فقط للوهلة الأولى. معرفة مدى عمق حفرة الأرنب هذه يمكن أن تستغرق عدة أسابيع.

لكن لنبدأ من البداية.

نعم سيكون مخضرم


أفضل جرادل. ولكن لسبب ما ، يولي مطورو JOOQ اهتمامًا أقل لـ Gradle ، ويعرضون الانتقال إلى المكونات الإضافية للجهات الخارجية. حسنًا ، سيكون مشروع التدريب على Maven. ولكن إذا كنت ، أيها القارئ العزيز ، تفحص بشكل صحيح في أعماق github وكنت على استعداد لتقديم مشروع مخصص بالكامل على Gradle - اكتب لي وستصبح مؤلفًا مشاركًا لهذه المقالة. الأمر نفسه ينطبق على H2 (سيكون مشروع التدريب على PostgreSQL).

لماذا لم أنجح مع Gradle و H2
, Gradle. jooq-generator . H2, « » H2 , , , .

تبعيات مخضرمة ضرورية:

<dependencies>

        <!-- Spring Starters -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jooq</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- Database -->
        <dependency>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- Helpers -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons.lang3.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!--Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <!-- JOOQ Generator -->
        <dependency>
            <groupId>org.jooq</groupId>
            <artifactId>jooq</artifactId>
            <version>${jooq.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jooq</groupId>
            <artifactId>jooq-meta</artifactId>
            <version>${jooq.version}</version>
        </dependency>
        <dependency>
            <groupId>org.jooq</groupId>
            <artifactId>jooq-codegen</artifactId>
            <version>${jooq.version}</version>
        </dependency>

    </dependencies>

يحتوي Spring بالفعل على مشغل JOOQ الذي سيقوم تلقائيًا بتكوين مصدر البيانات وفقًا للإعدادات المحددة من application.yml. لكن بالنسبة لعمل JOOQ الكامل ، هذا لا يكفي. يجب إنشاء الكيانات. نعم ، نعم ، نحن لا نكتب كيانات ، ولكن نولدها. ما يسمى نهج الجدول الأول.

يبدو هيكل التطبيق المعتاد مع التركيز على العمل مع قاعدة البيانات كما يلي:



حتى طبقة الخدمة ، تنتقل البيانات من خلال التطبيق في شكل نموذج مسطح (DTO). علاوة على ذلك ، عندما تحتاج الخدمة إلى العمل مع قاعدة البيانات ، فإنها تحول DTO إلى كيان ، وترسله إلى المستودع ، وهذا الشخص يحفظه بالفعل في قاعدة البيانات. يرى مطورو JOOQ كل شيء بشكل مختلف:



كما نرى ، قاموا في JOOQ بمراجعة النمط القياسي بجدية وقرروا السير في طريقهم الخاص. الاختلافات كبيرة:

  1. يحدث تحويل DTO إلى كيان بالفعل في المستودع.
  2. نحن لا نكتب كيانًا على هذا النحو. هو مكتوب لنا من قبل مولد JOOQ.

هذا يكفي بالفعل لاستنتاج أن JOOQ ليست بهذه البساطة. إن الكيان الناتج بشكل قابل للتخصيص للغاية ، والتخطيط غير المفهوم في DTO والصعوبات الأخرى سيخيف الكثيرين. ولكن في الحالات التي لا يوجد فيها مكان يمكن الذهاب إليه ، يمكن لدراسة مركزة لهذه الجوانب من JOOQ أن تحمل بجدية وتستغرق أيامًا أو حتى أسابيع. أنا متأكد من أن منشوري سيوفر وقتك بشكل ملحوظ.

سنغطي الجوانب التالية للعمل مع JOOQ:

  • جيل الكيان.
  • كتابة استعلامات CRUD.
  • أمثلية طلبات CRUD.
  • وآخر طلب CRUD التحسين.
  • رسم خرائط الكيانات باستخدام مكتبة JOOQ القياسية.
  • وناقش لماذا كل هذا ضروري.

لنذهب.

ماذا نكتب؟


سيشتمل مشروعنا على كيانين:

الدولة:

@Data
public class Country {

    private Long id;
    private String name;
    private GovernmentForm governmentForm;
    private Integer population;

    private List<City> cities;
}

مدينة:

@Data
public class City {

    private Long id;
    private Long countryId;
    private String name;
}

علاقة رأس بأطراف. يحتوي البلد على العديد من المدن ذات الصلة. تحتوي المدينة على معرف البلد.
تبدو هجرة مسارات الطيران كما يلي:

create table countries
(
    id              bigserial primary key,
    name            varchar(255),
    government_form varchar(255),
    population      int
);

create table cities
(
    id         bigserial primary key,
    country_id bigint,
    name       varchar(255)
);

لنبدأ بتوليد الكيانات.


الكيانات ، كما قلت ، يتم إنشاؤها بشكل منفصل. هناك طريقتان للقيام بذلك.

  • من خلال البرنامج المساعد مخضرم.
  • في كود Java.

توليد الكيانات في مخضرم


عندما يبني المشروع ، يبدأ Maven المولد ويولد الكيانات. ويمكنك استدعاء المولد في أي وقت مناسب وإنشاء الكيانات. هذا مطلوب في تلك اللحظات عندما ، على سبيل المثال ، تغيرت بنية القاعدة. يبدو المكوّن الإضافي في Maven كما يلي:

            <!-- JOOQ Generator Plugin -->
            <plugin>
                <groupId>org.jooq</groupId>
                <artifactId>jooq-codegen-maven</artifactId>
                <version>${jooq.version}</version>
                <executions>
                    <execution>
                        <phase>generate-sources</phase>
                        <goals>
                            <goal>generate</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <jdbc>  <!--    -->
                        <driver>${db.driver}</driver>
                        <url>${db.url}</url>
                        <user>${db.username}</user>
                        <password>${db.password}</password>
                    </jdbc>
                    <generator>
                        <database>
                            <includes>.*</includes>  <!--     -->
                            <excludes>  <!--     -->
                                flyway_schema_history
                            </excludes>
                            <inputSchema>public</inputSchema>  <!--  -->
                        </database>
                        <generate>
                            <records>true</records>
                        </generate>
                        <target>
                            <!--      -->
                            <packageName>ru.xpendence.jooqexample.domain</packageName>
                            <!--  .    target. -->
                            <directory>target/generated-sources/jooq</directory>
                        </target>
                    </generator>
                </configuration>
            </plugin>

إذا قمت بكل شيء بشكل صحيح ، قم بتحديث Maven وضمن الإضافات سترى jooq-codegen.



شغلها. سيولد لك كيانات.



هذه هي الكيانات التي ستستخدمها للوصول إلى قاعدة البيانات. الإزعاج الأول: أنها ثابتة. أي أنها قابلة للتغيير ، ولكن تغيير النوع يحدث بطريقة مبتكرة بحيث يتم سحب وصف العملية هذا إلى مقالة منفصلة. لذلك ، حاول التفكير في أنواع البيانات في مرحلة إنشاء الجدول.

تبدو الكيانات غريبة. هنا ، على سبيل المثال ، هو توقيع من الفئة السجلات البلدان:

public class CountriesRecord extends UpdatableRecordImpl<CountriesRecord> implements Record4<Long, String, String, Integer> {
    //...

كما نرى ، يقوم تطبيق CountryRecord بتطبيق Record4 ، والذي تمت كتابته بواسطة 4 أنواع. يحتوي جدول البلدان على 4 أعمدة ، وبالتالي Record4. هناك 3 أعمدة في المدن ، لذلك Record3. لماذا اخترع هذا ، لا أعرف. يوجد 22 سجلًا في مكتبة JOOQ ، Record1 ... Record22. يمكننا من خلاله استنتاج أن قدرات JOOQ تقتصر على معالجة الجداول بحد أقصى 22 عمودًا ، ولكن هذا ليس كذلك. لدي جداول بها عشرات الأعمدة ، ويقوم JOOQ بتطبيق Record على الفور. حسنًا ، هذا ...

إنشاء كيانات JOOQ في كود Java يبدو كما يلي:

@Component
public class AfterStartupApplicationListener implements ApplicationListener<ContextRefreshedEvent> {

    @Value("${spring.datasource.driver-class-name}")
    private String driver;

    @Value("${spring.datasource.url}")
    private String url;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${jooq.generator.database.name}")
    private String databaseName;

    @Value("${jooq.generator.database.with-includes}")
    private String databaseWithIncludes;

    @Value("${jooq.generator.database.with-input-schema}")
    private String databaseWithInputSchema;

    @Value("${jooq.generator.target.package-name}")
    private String targetPackageName;

    @Value("${jooq.generator.target.directory}")
    private String targetDirectory;

    @SneakyThrows
    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        new GenerationTool().run(configureGenerator());
    }

    private Configuration configureGenerator() {
        return new Configuration()
                .withJdbc(new Jdbc()
                        .withDriver(driver)
                        .withUrl(url)
                        .withUser(username)
                        .withPassword(password))
                .withGenerator(new Generator()
                        .withDatabase(new Database()
                                .withName(databaseName)
                                .withIncludes(databaseWithIncludes)
                                .withExcludes("")
                                .withInputSchema(databaseWithInputSchema))
                        .withTarget(new Target()
                                .withPackageName(targetPackageName)
                                .withDirectory(targetDirectory)));
    }
}

في حالتي ، يبدأ المولد بعد بدء التطبيق بالكامل ، عندما يتم ترحيل جميع عمليات الترحيل الجديدة وتكون قاعدة البيانات محدثة.

لذا ، لدينا نماذج ، لدينا كيانات ، لدينا قاعدة. حان الوقت لإنشاء مستودع وكتابة الاستعلامات.

إنشاء مستودع


تمر جميع الطلبات من خلال DslContext. كرر DslContext.

@Repository
@RequiredArgsConstructor
public class CountryRepository implements CrudRepository<Country> {

    private final DSLContext dsl;

} 

المستودع جاهز.

كتابة استعلامات CRUD


إدراج


إذا استخدمنا SQL ، فسيكون طلب إضافة بلد آخر:

insert into countries(name, government_form, population)
values (' -', 'UNITARY', 100500) returning *;

في JOOQ ، يكون الاستعلام أقرب ما يمكن إلى صيغة SQL:

    public Country insertValues(Country country) {
        return dsl.insertInto(Countries.COUNTRIES)  //insert into countries
                .values(country.getId(), country.getName(), country.getPopulation(), nameOrNull(country.getGovernmentForm()))  //values (? ? ? ?)
                .returning()  //returning
                .fetchOne()  //*
                .into(Country.class);
    }

كما نرى ، لا شيء معقد. ومع ذلك ، فإن هذا الطلب لا يناسبنا ، لأنه في حالتنا يتم إنشاء المعرّف في قاعدة البيانات ويجب أن نأخذ هذا في الاعتبار. إذا كتبنا شيئًا مثل:

.values(null, country.getName(), country.getPopulation(), nameOrNull(country.getGovernmentForm()))

سوف نحضر

org.postgresql.util.PSQLException: :     "id"   NOT NULL

ومع ذلك ، إذا أنشأنا المعرّف على جانب التطبيق ، فسيكون هذا الطلب رحلة. سيتعين علينا إعادة كتابة الطلب ، مع إعطاء كل حقل قيمة محددة. لذا يمكنك:

    public Country insert(Country country) {
        return dsl.insertInto(Countries.COUNTRIES)
                .set(Countries.COUNTRIES.NAME, country.getName())
                .set(Countries.COUNTRIES.POPULATION, country.getPopulation())
                .set(Countries.COUNTRIES.GOVERNMENT_FORM, nameOrNull(country.getGovernmentForm()))
                .returning()
                .fetchOne()
                .into(Country.class);
    }

في هذه الحالة ، نقوم بتعيين الكائن يدويًا في الحقل. هذا الخيار يعمل بشكل جيد ، ولكن هناك أفضل. سيكون من المثالي إرسال الكائن بالكامل للحفظ ، كما هو الحال في Spring Data JPA:

repository.save(country);

يمكن أن يكون كذلك. للقيام بذلك ، نحتاج إلى تعيين نموذجنا في كيان Extended Record وإرساله للحفظ:

    public Country insert(Country country) {
        return dsl.insertInto(Countries.COUNTRIES)
                .set(dsl.newRecord(Countries.COUNTRIES, country))  //   
                .returning()
                .fetchOptional()
                .orElseThrow(() -> new DataAccessException("Error inserting entity: " + country.getId()))
                .into(Country.class);
    }

هذه الطريقة هي الأبسط والأكثر قابلية للفهم لتلك الحالات عندما لا نحتاج إلى تكوين التعيين. لكن إعداد الخرائط سيكون أقل.

كما لاحظنا ، يقوم هذا الاستعلام بإرجاع الكيان بأكمله. يمكنك تحديد ما تحتاجه بالضبط للعودة في طريقة الجلب (). تبدو هكذا:

    public Long insertAndReturnId(Country country) {
        return dsl.insertInto(Countries.COUNTRIES)
                .set(dsl.newRecord(Countries.COUNTRIES, country))
                .returning(Countries.COUNTRIES.ID) //  Record    - id
                .fetchOptional()
                .orElseThrow(() -> new DataAccessException("Error inserting entity: " + country.getId()))
                .get(Countries.COUNTRIES.ID); // id
    }

مرهقة بعض الشيء ، ولكن ، مرة أخرى ، مع ما يمكن مقارنته.

سنكتب بقية طرق CRUD.

تحديث


استعلام SQL:

update countries
set name            = '',
    government_form = 'CONFEDERATE',
    population      = 100500
where id = 1
returning *;

طلب JOOQ:

    public Country update(Country country) {
        return dsl.update(Countries.COUNTRIES)
                .set(dsl.newRecord(Countries.COUNTRIES, country))
                .where(Countries.COUNTRIES.ID.eq(country.getId()))
                .returning()
                .fetchOptional()
                .orElseThrow(() -> new DataAccessException("Error updating entity: " + country.getId()))
                .into(Country.class);
    }

تحديد


سيكون الاستعلام في SQL:

select *
from countries c
where id = ?;

في JOOQ ، سيبدو الاستعلام وفقًا لذلك:

    public Country find(Long id) {
        return dsl.selectFrom(Countries.COUNTRIES) //select * from countries
                .where(Countries.COUNTRIES.ID.eq(id))  //where id = ?
                .fetchAny()  // ,    
                .into(Country.class);
    }

حذف


هناك طلبات ليس لدينا ما نرجع إليه. هذه الاستعلامات تُرجع عدد الصفوف المتأثرة. في الحذف ، ليس لدينا ما نعيده ، لكن المعلومات التي نتلقاها ستظل مفيدة لنا.

    public Boolean delete(Long id) {
        return dsl.deleteFrom(Countries.COUNTRIES)
                .where(Countries.COUNTRIES.ID.eq(id))
                .execute() == 1;
    }

نحذف سطر واحد. لذا سيُرجع استعلام SQL شيئًا مثل:

1 row affected in 5 ms

بعد تلقي مثل هذه الإجابة ، نعرف على وجه اليقين أنه تم حذف الصف. سيكون هذا كافيا للإعلان عن نجاح العملية.

لم تكن خرافة. كانت مجرد مادة تشحيم. استخدام maper منتظم لرسم خرائط رقيقة لكيان في التسجيل والعكس بالعكس


"حسنًا" ، تقول ، أيها القارئ "أين واحد لكثير يا إيمو؟" شيء لم أر اللحظة التي تنمو فيها الدولة مع العديد من المدن. "

وستكون على حق. هذا ليس سباتًا ؛ هنا يجب عليك يدويًا. كل ما عليك فعله هو الحصول على قائمة المدينة التي id = country.getId (). لقد عرضت بالفعل طريقة () ، التي تستخدم مخططًا عاديًا لتعيين السجل إلى كيان. ولكن لدى JOOQ طريقة map () إضافية تسمح لنا بعمل ما نريد باستخدام Record. دعونا نلقي نظرة على وظائفه وإضافة إضافة المدن:

    public Country find(Long id) {
        return dsl.selectFrom(Countries.COUNTRIES)
                .where(Countries.COUNTRIES.ID.eq(id))
                .fetchAny()
                .map(r -> {
                    Country country = r.into(Country.class);
                    country.setCities(cityRepository.findAll(Cities.CITIES.COUNTRY_ID.eq(country.getId())));
                    return country;
                });
    }

كما نرى ، الآن نقوم أولاً بتعيين السجل في البلد ، ثم نقوم بتقديم طلب آخر في المدن ، نحصل على كل المدينة لهذا البلد ونضعها في الجوهر.

ولكن يمكن أن يكون هناك العشرات من هذه المجموعات ، مما يعني أنه يمكن أن يكون هناك عشرات الطلبات. ووصف هذه الطلبات مباشرة في الطريقة هو هكذا. الحل الصحيح هو كتابة مخطط منفصل ووضع كل هذه الطلبات هناك.

@RequiredArgsConstructor
@Component
public class CountryRecordMapper implements RecordMapper<CountriesRecord, Country> {

    private final CityRepository cityRepository;

    @Override
    public Country map(CountriesRecord record) {
        Country country = record.into(Country.class);
        country.setCities(cityRepository.findAll(Cities.CITIES.COUNTRY_ID.eq(country.getId())));
        return country;
    }
}

سيبدو الاستعلام الآن كما يلي:

    public Country findWithCustomMapper(Long id) {
        return dsl.selectFrom(Countries.COUNTRIES)
                .where(Countries.COUNTRIES.ID.eq(id))
                .fetchAny()
                .map(r -> countryRecordMapper.map((CountriesRecord) r));
    }

وهي أكثر إيجازاً ولا تحتوي على منطق إضافي.

حسنًا ، لقد تعلمنا كيفية تعيين السجل إلى كيان ، ولكن ماذا عن تحسين تعيين كيان في السجل؟


حتى الآن لدينا هذا التصميم:

.set(dsl.newRecord(Countries.COUNTRIES, country))

جيد بالفعل ، ولكن ماذا لو كنا بحاجة إلى تعيين حقل على وجه التحديد؟ على سبيل المثال ، لدينا LocalDateTime هناك ، ومولد لـ PostgreSQL مثل الطابع الزمني أنشأ OffsetDateTime. في هذه الحالة ، ببساطة لن يتم تعيين الحقل ولن يتم تسجيله في قاعدة البيانات. في مثل هذه الحالات ، سنحتاج إلى مصمم خرائط آخر سيفعل الشيء نفسه ، ولكن في الاتجاه المعاكس.

نعم ، لكل مصمّم خرائط لدينا


يطلق عليه ذلك. دعونا نكتب وريثه.

@Component
@RequiredArgsConstructor
public class CountryRecordUnmapper implements RecordUnmapper<Country, CountriesRecord> {

    private final DSLContext dsl;

    @Override
    public CountriesRecord unmap(Country country) throws MappingException {
        CountriesRecord record = dsl.newRecord(Countries.COUNTRIES, country);
        record.setPopulation(-1);
        return record;
    }
}

سيظهر الإدخال مع تطبيقه كما يلي:

    public Country insertWithUnmapper(Country country) {
        return dsl.insertInto(Countries.COUNTRIES)
                .set(countryRecordUnmapper.unmap(country))
                .returning()
                .fetchOptional()
                .orElseThrow(() -> new DataAccessException("Error inserting entity: " + country.getId()))
                .into(Country.class);
    }

كما نرى أيضًا.

الاستنتاجات والاستنتاجات


أنا شخصياً أحب السبات أكثر. على الأرجح ، بالنسبة لـ 90 + ٪ من التطبيقات ، فإن استخدامه سيكون أكثر تبريرًا. ولكن إذا كنت أنت ، أيها القارئ ، تريد التحكم في كل طلب ، فإن JOOQ أو مكتبة أخرى مماثلة أكثر ملاءمة لك.

كما هو الحال دائمًا ، أنشر المشروع التدريبي. إنه يرقد هنا .

All Articles