برنامه نویسی

بهار دسته 5: یک مورد استفاده ساده با تست ها

پیش نیازها: دانش اساسی در مورد جاوا ، دسته بهار ، بهار JPA.

می توانید کد مورد استفاده در این وبلاگ را در این مخزن GitHub پیدا کنید.

بهار دسته چارچوبی در اکوسیستم بهار است که توسعه برنامه های پردازش دسته ای را ساده می کند. پردازش دسته ای شامل رسیدگی به داده های زیادی به طور همزمان ، اغلب به صورت برنامه ریزی شده است.

در اینجا نکات اصلی به زبان ساده تر وجود دارد:

  1. آسانتر کردن کارها: این به توسعه دهندگان کمک می کند تا با ارائه ابزارها و مؤلفه های آماده استفاده ، با پیچیدگی پردازش دسته ای مقابله کنند.
  2. نگه داشتن چیزها: مدیریت معاملات ، اطمینان از اینکه داده ها سازگار و قابل اعتماد هستند ، حتی اگر چیزی در طول پردازش اشتباه باشد.
  3. رسیدگی به کارهای بزرگ: با اجازه دادن به انجام کارها به صورت موازی ، شکستن آنها به قطعات کوچکتر و پردازش یک تکه در یک زمان ، می تواند مقادیر زیادی از داده ها را به طور کارآمد انجام دهد.
  4. کار سازمان یافته: پردازش دسته ای به مشاغل تقسیم می شود که از مراحل تشکیل شده است. یک کار مانند کار کلی است و مراحل بخش های کوچکتر آن کار است.
  5. بخوانید ، کاری انجام دهید ، بنویسید: یک رویکرد ساده خواندن داده ها از یک منبع ، انجام برخی کارها روی آن و سپس نوشتن آن به یک مقصد. اینگونه است که داده ها را پردازش می کند.
  6. برخورد با مشکلات: راه هایی برای مقابله با موضوعاتی مانند اقدامات آزمایشی برای عملیات شکست خورده و پرش از سوابق که نمی توانند به درستی پردازش شوند ، فراهم می کند.
  7. اقدامات سفارشی: به توسعه دهندگان این امکان را می دهد تا کد خود را در نقاط مختلف پردازش دسته ای مانند قبل یا بعد از یک مرحله یا کار ، سفارشی و اضافه کنند.

از نظر عملی ، دسته بندی بهار معمولاً هنگامی استفاده می شود که داده های زیادی برای پردازش منظم داشته باشید ، مانند هنگام جابجایی داده ها بین سیستم ها ، تبدیل آن یا انجام کارهای دوره ای مانند تولید گزارش. این باعث می شود کل فرآیند قابل کنترل تر و مستعد خطا باشد.

چالش برانگیزترین بخش در دسته بهار نحوه نوشتن تست های واحد و ادغام است. به طور ذاتی از پیچیدگی پیکربندی استفاده می شود.

بیایید نمونه ای از دسته ای را انجام دهیم که موارد زیر را انجام می دهد:

پوشه ای را برای دریافت پرونده های جدید تماشا کنید
داده ها را از یک فایل مسطح هنگام اضافه شدن به پوشه بخوانید
پردازش داده ها
داده ها را در یک پایگاه داده ذخیره کنید
برای این مثال ما از Course Batch 5 ، Spring Boot 3 ، Java 17 ، Mapstruct برای نقشه برداری شیء و Maven به عنوان یک ابزار ساخت استفاده خواهیم کرد.

در اینجا چگونه pom.xml ما به نظر می رسد:



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        3.2.2
         
    
    dev.sabri
    FolderMonitor
    0.0.1-SNAPSHOT
    FolderMonitor
    FolderMonitor
    
        17
        1.5.5.Final
    
    
        
            org.springframework.boot
            spring-boot-starter-batch
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
            org.springframework.boot
            spring-boot-starter-data-jpa
        
        
            com.h2database
            h2
            runtime
        
        
            org.projectlombok
            lombok
            true
        
        
            org.mapstruct
            mapstruct
            ${map.struct.version}
        
        
            org.mapstruct
            mapstruct-processor
            ${map.struct.version}
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.batch
            spring-batch-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    

حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

حال بیایید شی موجودیت خود را ایجاد کنیم:

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class Visitors {

    @Id
    private Long id;
    private String firstName;
    private String lastName;
    private String emailAddress;
    private String phoneNumber;
    private String address;
    private String visitDate;
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

ما به یک شی DTO احتیاج داریم که بعداً به موجودیت ما نقشه برداری می شود. ما از یک رکورد استفاده خواهیم کرد:

@Builder
public record VisitorsDto(
        Long id,
        String firstName,
        String lastName,
        String emailAddress,
        String phoneNumber,
        String address,
        String visitDate) {
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

رابط MapStruct به این شکل خواهد بود:

@Mapper(componentModel = "spring")
public interface VisitorsMapper {

    Visitors toVisitors(VisitorsDto visitorsDto);
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

مرحله بعدی ایجاد یک jParePository برای دسترسی به بانک اطلاعاتی خواهد بود ، این یک jparepository ساده و بدون پرس و جوهای سفارشی است:

public interface VisitorsRepository extends JpaRepository {
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

یک کار دسته ای بهار استاندارد ترکیبی از یک یا چند مرحله است.

هر مرحله به خودی خود می تواند حاوی یک وظیفه یا یک مورد Reader ، AttemProcessor و یک آیتم نویسنده باشد.

بیایید با تعریف AtmiteReader خود شروع کنیم ، هدف اصلی در اینجا خواندن داده ها از پرونده مسطح ما است:

  @Bean
  @StepScope
  public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['inputFile']}") final String visitorsFile) throws IOException {
        val flatFileItemReader = new FlatFileItemReader();
        flatFileItemReader.setName("VISITORS_READER");
        flatFileItemReader.setLinesToSkip(1);
        flatFileItemReader.setLineMapper(linMapper());
        flatFileItemReader.setStrict(false);
        flatFileItemReader.setResource(new FileSystemResource(visitorsFile));
        return flatFileItemReader;
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در صورت نقشه برداری از داده ها و زمینه های پرونده ما به شیء جاوا ، بخش مهمی در فرآیند خواندن داده ها در صورت نقشه برداری داده ها.

در این حالت ما از یک FieldsetMapper سفارشی استفاده خواهیم کرد:

public class VisitorsFieldSetMapper implements FieldSetMapper {
    @Override
    public VisitorsDto mapFieldSet(final FieldSet fieldSet) throws BindException {
        return VisitorsDto.builder()
                .id(fieldSet.readLong("id"))
                .firstName(fieldSet.readString("firstName"))
                .lastName(fieldSet.readString("lastName"))
                .emailAddress(fieldSet.readString("emailAddress"))
                .phoneNumber(fieldSet.readString("phoneNumber"))
                .address(fieldSet.readString("address"))
                .visitDate(fieldSet.readString("visitDate"))
                .build();
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پس از خواندن داده های پرونده ، بسته به نیاز ما باید پردازش شود. در مورد استفاده من ، پردازنده یک شیء بازدید کننده را به بازدید کنندگان نهاد ما نقشه می کند:

@Component
public class VisitorsItemProcessor implements ItemProcessor {
    private final VisitorsMapper visitorsMapper;

    public VisitorsItemProcessor(VisitorsMapper visitorsMapper) {
        this.visitorsMapper = visitorsMapper;
    }

    @Override
    public Visitors process(final VisitorsDto visitorsDto) throws Exception {
        return visitorsMapper.toVisitors(visitorsDto);
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

پس از نقشه برداری از شیء ما ، اکنون وقت آن رسیده است که داده ها را در پایگاه داده H2 ما با استفاده از موارد نویسنده به شرح زیر وارد کنیم:

@Component
public class VisitorsItemWriter implements ItemWriter {

    private final VisitorsRepository visitorsRepository;

    public VisitorsItemWriter(VisitorsRepository visitorsRepository) {
        this.visitorsRepository = visitorsRepository;
    }

    @Override
    public void write(Chunk extends Visitors> chunk) throws Exception {
        visitorsRepository.saveAll(chunk.getItems());
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

در اینجا نحوه پیکربندی کامل کار ما به نظر می رسد:

@Configuration
public class VisitorsBatchConfig {


    @Bean
    public Job importVistorsJob(final JobRepository jobRepository, final PlatformTransactionManager transactionManager, final VisitorsRepository visitorsRepository, final VisitorsMapper visitorsMapper) throws IOException {
        return new JobBuilder("importVisitorsJob", jobRepository)
                .start(importVisitorsStep(jobRepository, transactionManager, visitorsRepository, visitorsMapper))
                .build();
    }

    @Bean
    public Step importVisitorsStep(final JobRepository jobRepository, final PlatformTransactionManager transactionManager, final VisitorsRepository visitorsRepository, final VisitorsMapper visitorsMapper) throws IOException {
        return new StepBuilder("importVisitorsStep", jobRepository)
                .chunk(100, transactionManager)
                .reader(flatFileItemReader(null))
                .processor(itemProcessor(visitorsMapper))
                .writer(visitorsItemWriter(visitorsRepository))
                .build();
    }

    @Bean
    public ItemProcessor itemProcessor(final VisitorsMapper visitorsMapper) {
        return new VisitorsItemProcessor(visitorsMapper);
    }

    @Bean
    public VisitorsItemWriter visitorsItemWriter(final VisitorsRepository visitorsRepository) {
        return new VisitorsItemWriter(visitorsRepository);
    }


    @Bean
    @StepScope
    public FlatFileItemReader flatFileItemReader(@Value("#{jobParameters['inputFile']}") final String visitorsFile) throws IOException {
        val flatFileItemReader = new FlatFileItemReader();
        flatFileItemReader.setName("VISITORS_READER");
        flatFileItemReader.setLinesToSkip(1);
        flatFileItemReader.setLineMapper(linMapper());
        flatFileItemReader.setStrict(false);
        flatFileItemReader.setResource(new FileSystemResource(visitorsFile));
        return flatFileItemReader;
    }

    @Bean
    public LineMapper linMapper() {
        val defaultLineMapper = new DefaultLineMapper();
        val lineTokenizer = new DelimitedLineTokenizer();
        lineTokenizer.setDelimiter(",");
        lineTokenizer.setNames("id", "firstName", "lastName", "emailAddress", "phoneNumber", "address", "visitDate");
        lineTokenizer.setStrict(false); // Set strict property to false
        defaultLineMapper.setLineTokenizer(lineTokenizer);
        defaultLineMapper.setFieldSetMapper(new VisitorsFieldSetMapper());
        return defaultLineMapper;

    }

}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

برای بسته بندی دسته خود ، ما باید یک تماشاگر سرویس را تنظیم کنیم تا به دنبال هر پرونده جدیدی که وارد پوشه ما می شود ، جستجو کنیم.

این مرحله در پروژه اصلی بوت بهار ما پیکربندی شده است:

@SpringBootApplication
@EnableBatchProcessing
@EnableScheduling
@Slf4j
public class FolderMonitorApplication {

    private final JobLauncher jobLauncher;
    private final Job job;

    public FolderMonitorApplication(JobLauncher jobLauncher, Job job) {
        this.jobLauncher = jobLauncher;
        this.job = job;
    }

    public static void main(String[] args) {
        new SpringApplicationBuilder(FolderMonitorApplication.class)
                .web(WebApplicationType.SERVLET)
                .run(args)
                .registerShutdownHook();
    }

    public void run(final String inputFile) throws Exception {
        val jobParameters = new JobParametersBuilder()
                .addDate("timestamp", Calendar.getInstance().getTime())
                .addString("inputFile", inputFile)
                .toJobParameters();
        val jobExecution = jobLauncher.run(job, jobParameters);
        while (jobExecution.isRunning()) {
            log.info("..................");
        }
    }

    @Scheduled(fixedRate = 2000)
    public void runJob() {

        val path = Paths.get("/home/sabri/Work"); 
        WatchKey key;
        WatchService watchService = null;
        try {
            watchService = FileSystems.getDefault().newWatchService();
            path.register(watchService, StandardWatchEventKinds.ENTRY_CREATE);

            while ((key = watchService.take()) != null) {
                for (WatchEvent> event : key.pollEvents()) {

                    log.info(
                            "Event kind:" + event.kind()
                                    + ". File affected: " + event.context() + ".");
                    if (event.kind().name().equals("ENTRY_CREATE")) {
                        run(path + "https://dev.to/" + event.context().toString());
                    }
                }
                key.reset();
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

اکنون که دسته ما آماده است بیایید آن را شروع کنیم و پرونده ای را درون پوشه خود قرار دهیم

/خانه/سابری/کار ، در اینجا نحوه ورود سیاههها آمده است:
2024-02-05T22: 58: 34.100+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر تکمیل شد: [{‘inputFile’:'{value=/home/sabri/Work/visitors.csv, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 22:58:34 CET 2024, type=class java.util.Date, identifying=true}’}] و وضعیت زیر: [COMPLETED] در 75ms
2024-02-05T22: 58: 53.023+01: 00 اطلاعات 6931 — [ scheduling-1] dsffoldermonitorapplication: نوع رویداد: enter_create. پرونده تحت تأثیر:. ~ lock.visitors.csv#.
2024-02-05T22: 58: 53.029+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر راه اندازی شد: [{‘inputFile’:'{value=/home/sabri/Work/.~lock.visitors.csv#, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 22:58:53 CET 2024, type=class java.util.Date, identifying=true}’}]2024-02-05T22: 58: 53.031+01: 00 اطلاعات 6931 — [ scheduling-1] osbatch.core.job.simplestephandler: مرحله اجرا: [importVisitorsStep]2024-02-05T22: 58: 53.034+01: 00 اطلاعات 6931 — [ scheduling-1] OSBATCH.CORE.STEP.ABSTRACTERSTEP: مرحله: [importVisitorsStep] در 2MS اجرا شد
2024-02-05T22: 58: 53.035+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر تکمیل شد: [{‘inputFile’:'{value=/home/sabri/Work/.~lock.visitors.csv#, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 22:58:53 CET 2024, type=class java.util.Date, identifying=true}’}] و وضعیت زیر: [COMPLETED] در 5MS
2024-02-05T23: 00: 32.457+01: 00 اطلاعات 6931 — [ scheduling-1] dsffoldermonitorapplication: نوع رویداد: enter_create. پرونده تحت تأثیر: بازدید کنندگان 2.csv.
2024-02-05T23: 00: 32.464+01: 00 اطلاعات 6931 — [ scheduling-1] OSBCLSUPPORT.SIMPLEJOBLAUNCHER: کار: [SimpleJob: [name=importVisitorsJob]]با پارامترهای زیر راه اندازی شد: [{‘inputFile’:'{value=/home/sabri/Work/Visitors2.csv, type=class java.lang.String, identifying=true}’,’timestamp’:'{value=Mon Feb 05 23:00:32 CET 2024, type=class java.util.Date, identifying=true}’}]2024-02-05T23: 00: 32.468+01: 00 اطلاعات 6931 — [ scheduling-1] osbatch.core.job.simplestephandler: مرحله اجرا: [importVisitorsStep]بازدید کننده[id=1, firstName=John, lastName=Doe, emailAddress=john.doe@example.com, phoneNumber=(555) 123-4567, address=123 Main St, visitDate=2024-02-05]بازدید کننده[id=2, firstName=Jane, lastName=Smith, emailAddress=jane.smith@example.com, phoneNumber=(555) 987-6543, address=456 Oak St, visitDate=2024-02-06]بازدید کننده[id=3, firstName=Michael, lastName=Johnson, emailAddress=michael.j@example.com, phoneNumber=(555) 555-5555, address=789 Pine St, visitDate=2024-02-07]بازدید کننده[id=4, firstName=Alice, lastName=Williams, emailAddress=alice.w@example.com, phoneNumber=(555) 456-7890, address=101 Elm St, visitDate=2024-02-08]بازدید کننده[id=5, firstName=David, lastName=Miller, emailAddress=david.m@example.com, phoneNumber=(555) 111-2222, address=202 Birch St, visitDate=2024-02-09]

اکنون که کار ما خوب است ، قسمت اول انجام می شود ، بیایید حرکت کنیم و یک تست برای این دسته بنویسیم.

همانطور که قبل از این که بیشترین قسمت تنظیم شده است ، پیکربندی است ، اینگونه به نظر می رسد:

@Configuration
@EnableBatchProcessing
@EnableJpaRepositories(basePackages = {"dev.sabri.foldermonitor.repositories"})
@EntityScan(basePackages = {"dev.sabri.foldermonitor.domain"})
@ComponentScan(basePackages = {"dev.sabri.foldermonitor.mapper"})
public class VisitorsBatchTestConfig {


    @Bean
    @Primary
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .addScript("/org/springframework/batch/core/schema-h2.sql")
                .build();
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }

    @Bean("entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactoryBean() {
        val localContainerEntityManagerFactoryBean = new LocalContainerEntityManagerFactoryBean();
        localContainerEntityManagerFactoryBean.setDataSource(dataSource());
        localContainerEntityManagerFactoryBean.setPackagesToScan("dev.sabri.foldermonitor.domain");
        localContainerEntityManagerFactoryBean.setPersistenceUnitName("visitors");
        val hibernateJpaVendorAdapter = new HibernateJpaVendorAdapter();
        localContainerEntityManagerFactoryBean.setJpaVendorAdapter(hibernateJpaVendorAdapter);
        return localContainerEntityManagerFactoryBean;


    }

    @Bean
    public JobRepository jobRepository() throws Exception {
        val jobrepositoryFactoryBean = new JobRepositoryFactoryBean();
        jobrepositoryFactoryBean.setDataSource(dataSource());
        jobrepositoryFactoryBean.setTransactionManager(transactionManager());
        jobrepositoryFactoryBean.afterPropertiesSet();
        return jobrepositoryFactoryBean.getObject();

    }
}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

EnableJparePositories برای گفتن دسته من از کجا می توانم همه مخازن من را پیدا کنم
EntityScan برای شناسایی نهادهای من استفاده می شود
componentscan عمدتاً در اینجا برای نقشه برداری من استفاده می شود تا لوبیا با آزمایش من در زمان اجرا تشخیص داده شود
حال بیایید یک کلاس آزمون ساده بنویسیم:

@SpringBatchTest
@SpringJUnitConfig(classes = {VisitorsBatchConfig.class, VisitorsBatchTestConfig.class})
class VisitorsBatchIntegrationTests {

    public static final String INPUt_FILE = "visitors.csv";
    @Autowired
    private JobLauncherTestUtils jobLauncherTestUtils;
    @Autowired
    private JobRepositoryTestUtils jobRepositoryTestUtils;

    @AfterEach
    public void cleanUp() {
        jobRepositoryTestUtils.removeJobExecutions();
    }

    private JobParameters defaultJobParameters() {
        val paramsBuilder = new JobParametersBuilder();
        paramsBuilder.addString("inputFile", INPUt_FILE);
        paramsBuilder.addDate("timestamp", Calendar.getInstance().getTime());
        return paramsBuilder.toJobParameters();
    }


    @Test
    void givenVisitorsFlatFile_whenJobExecuted_thenSuccess(@Autowired Job job) throws Exception {
        // given
        this.jobLauncherTestUtils.setJob(job);
        // when
        val jobExecution = jobLauncherTestUtils.launchJob(defaultJobParameters());
        val actualJobInstance = jobExecution.getJobInstance();
        val actualJobExitStatus = jobExecution.getExitStatus();

        // then
        assertThat(actualJobInstance.getJobName()).isEqualTo("importVisitorsJob");
        assertThat(actualJobExitStatus.getExitCode()).isEqualTo(ExitStatus.COMPLETED.getExitCode());

    }

}
حالت تمام صفحه را وارد کنید

از حالت تمام صفحه خارج شوید

همانطور که در صفحه بعدی خواهید دید ، تست خوب است.

نتایج آزمون
در اینجا ساده از داده های مورد استفاده در این نمونه بهار دسته ای آورده شده است:

VISITOR_ID ، FIRST_NAME ، LAST_NAME ، email_address ، phone_number ، آدرس ، بازدید_دیت
1 ، جان ، doe ، john.doe@sexal.com ، (555) 123-4567،123 خیابان اصلی ، 2024-02-05
2 ، جین ، اسمیت ، jane.smith@به عنوان مثال ..com ، (555) 987-6543،456 خیابان بلوط ، 2024-02-06
3 ، مایکل ، جانسون ، michael.j@مثال ..com ، (555) 555-5555،789 خیابان کاج ، 2024-02-07
4 ، آلیس ، ویلیامز ، alice.w@مثال ..com ، (555) 456-7890،101 خیابان Elm ، 2024-02-08
5 ، دیوید ، میلر ، david.m@مثال ..com ، (555) 111-2222،202 خیابان بیرچ ، 2024-02-09
6 ، سوزان ، جونز ، Susan.J@به عنوان مثال ..com ، (555) 333-4444،303 خیابان سرو ، 2024-02-10
7 ، رابرت ، اسمیت ، Robert.s@به عنوان مثال ..com ، (555) 555-1234،505 خیابان Maple ، 2024-02-11
8 ، امیلی ، دیویس ، emily.d@مثال ..com ، (555) 876-5432،707 خیابان بلوط ، 2024-02-12
9 ، کریستوفر ، کلارک ، Chris.c@به عنوان مثال ..com ، (555) 234-5678،909 خیابان کاج ، 2024-02-13
10 ، Emma ، Johnson ، Emma.j@Exactine.com ، (555) 432-1098،111 Walnut St ، 2024-02-14
11 ، ویلیام ، مارتین ، William.m@به عنوان مثال ..com ، (555) 567-8901،222 خیابان بیرچ ، 2024-02-15
12 ، الیویا ، اندرسون ، olivia.a@مثال ..com ، (555) 789-0123،333 Elm ST ، 2024-02-16
13 ، میسون ، وایت ، mason.w@به عنوان مثال ..com ، (555) 321-0987،444 خیابان بلوط ، 2024-02-17
14 ، سوفیا ، تیلور ، sophia.t@مثال ..com ، (555) 654-3210،555 خیابان افرا ، 2024-02-18
15 ، Aiden ، Brown ، Aiden.b@مثال ..com ، (555) 876-5432،666 Pine ST ، 2024-02-19
و سرانجام ، این کار انجام شده است.

ما یک دسته بهاری عملیاتی کامل با تست هایی داریم که پیکربندی شده و خوب اجرا می شوند.

وقت خود را برای گذراندن مقاله من بگذرانید. نظرات و نظرات شما برای من ارزشمند است ، زیرا آنها به تقویت کار من کمک می کنند ، و من واقعاً برای هر یک از آنها ارزش قائل هستم.

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا