علاقهمند به یادگیری. آن کس که "چرایی" را یافته است، "چگونگی" را نیز خواهد توانست. • فردریش نیچه •
مروری اجمالی به آنچه که هایبرنیت میکند - قسمت سوم
سلام!
به قسمت سوم مقالهی "مروری اجمالی بر هایبرنیت" خوشآمدید. در این مقاله بدون مقدمه به ادامهی مبحث قبل برخواهیم گشت. همراه باشید!
خب، در مقالهی قسمت دوم توضیح Annotationهای پایهای را شروع کرده بودیم. ادامه میدهیم. البته ذکر یک نکته بنظرم با اهمیت است. هرچه جلوتر برویم، توضیح من در مورد انوتیشنهای مشخص مختصرتر خواهد شد. انوتیشنهای مشخص چه انوتیشنی است؟ آنهایی که وظیفهاشان مشخص است، پیادهسازی آنها توسط خود شما براحتی صورت میپذیرد، فلذا لزومی نمیبینم که بابت هرکدام از آنها یک پیادهسازی ارائه دهم.
پرسش. منظور شما از انوتیشنهای پایهای چیست؟
پاسخ. این فقط یک اصطلاح من است، دقت شما ستودنی است. من بطور قراردادی با خودم، انوتیشنهایی که مربوط به نگاشت Entity میشود را پایهای نامیدم. چیزهایی که حتما بکار میآیند.
لینک قسمتهای مرتبط
فهرست
- توضیح Annotationهای پایهای بخش ۲
- عملیات CRUD در هایبرنیت
توضیح Annotationهای پایهای بخش ۲
خب، پس از توضیح بخشی از انونیشنها، حال نوبت به ادامهی روند است.
- @Basic
این انوتیشن خصیصهای بسیار مهم دارد. این انوتیشن خصیصهای بنام fetch دارد که مشخص میکند که فیلد مدنظر به چه شکل باید بازیابی شود. این خصیصه (fetch) دو مقدار مشخص میتواند بگیرد. یک مقدار Lazy و دیگری Eager. بسته به نوع استفاده میتوانید مشخص کنید که از کدام fetch type باید استفاده شود. به مثال زیر توجه کنید:
@Basic(fetch = FetchType.LAZY)
@Lob
private byte[] wav;
انوتیشن @Lob چیست؟! انوتیشن @Lob دقیقا برابر با نوع داده blob در دیتابیس است. وقتی فایلهای حجیم باینری در دیتابیس داشته باشیم، از این نوع داده استفاده میکنیم. در اینجا هم مشخص شده است که فیلد wav از نوع دادهی باینری است (blob).
نکتهی مهم این است که اگر خصیصهی fetch صراحتا مشخص نشود به صورت پیشفرض روی Eager خواهد بود.
پرسش. کاملا گیج شدهام! نه چیزی در مورد fetch type فهمیدم! نه چیزی در مورد @Lob!
اصلا LAZY و EAGER چه فرقی باهم دارند؟ یا blob چیست؟
پاسخ. حق دارید، متوجه بودم که احتمالا چنین حسی پیدا میکنید. اجازه دهید به ۲ سوال شما جداگانه پاسخ دهم.
در رابطه با fetch type باید بگویم که این مبحثی جداگانه است و در برنامهی پیش رو داریم. مهم برای من در اینجا این بود که بگویم در انوتیشن Basic میتوانید نوع fetch type را مشخص کنید. اما اینکه دقیقا fetch type چیست و اینکه Lazy چیست یا Eager چیست، مدنظرم نبوده است. در این مورد در بزودی صحبت خواهیم کرد. کمی صبر کنید.
اما در رابطه با نوع دادهی blob. این را اگر نمیدانید گوگل کنید، خارج از بحث ماست، و کاملا مربوط به دیتابیس است. ولی بطور کلی ما وقتی میخواهیم دادههای باینری را ذخیره کنیم از این نوع داده استفاده میکنیم (مثل عکس، فیلم، موزیک و...).
- @Column
این انوتیشن را دیدهاید. جدید نیست. از آن استفاده هم کردهاید. بیاد دارید که استفاده از خصیصهی name در انوتیشن Column، اسم ستون را مشخص کردید؟ قسمت اول و دوم را نگاه کنید. خب اکنون میخواهیم این انوتیشن را باز کنیم و نشان دهیم چه اطلاعات دقیقتری میتوانیم برای ستونهایمان درنظر بگیریم.
اجازه دهید نگاهی به کد خود این انوتیشن بیاندازیم:
@Target({METHOD, FIELD}) @Retention(RUNTIME)
public @interface Column {
String name() default "";
boolean unique() default false; // خط چهارم
boolean nullable() default true;
boolean insertable() default true;
boolean updatable() default true;
String columnDefinition() default "";
String table() default "";
int length() default 255;
int precision() default 0; // decimal precision
int scale() default 0; // decimal scale
}
این کد خود انوتیشن @Column است، همانطور که ملاحظه میکنید تمام خصیصهها بعلاوهی مقدار پیشفرض آنها لیست شده است. بعنوان مثال در خط چهارم، به خصیصهی unique اشاره شده است که مقدار پیشفرض آن هم false است. این خصیصه مشخص میکند که یک فیلد (یا همان ستون در دیتابیس) باید یکتا باشد یا میتواند مقدار تکراری بگیرد.
همانطور که متوجه شدهاید، به کمک این انوتیشن و خصیصههایش میتوانید محدودیتهای بسیاری را برای یک فیلد درنظر بگیرید. از مهمترین خصیصههایی که میتوان به آن اشاره کرد مقادیر unique، nullable، length و name میباشد. اگر خاطرتان باشد در قسمت دوم از خصیصهی table هم استفاده کردیم (جایی که میخواستیم یک Entity را به چندین جدول تقسیم کنیم).
به مثال زیر دقت کنید:
@Entity
public class Book {
@Id @GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(name = "book_title", nullable = false, updatable = false)
private String title;
private Float price;
@Column(length = 2000)
private String description;
private String isbn;
@Column(name = "nb_of_page", nullable = false)
private Integer nbOfPage;
// متد های گتر و ستر و سازنده
}
- @Temporal
این انوتیشن برای مشخص کردن تاریخ استفاده میشود. مثال زیر گویای مطلب است:
@Temporal(TemporalType.DATE)
private Date dateOfBirth;
- @Transient
کلیدواژهی transient در جاوا را بیاد دارید؟ که اجازه نمیداد یک فیلد مشخص serial شود؟ خاطرتان نیست؟ عیبی ندارد، بطور کلی فراموش کنید این جمله را. این انوتیشین وقتی برای فیلدی استفاده شود، مانع از آن میشود که آن فیلد مشخص نگاشت شود.
بطور کلی ما به ازای هر فیلد در جاوا، یک ستون در جدول خواهیم داشت، ولی اگر این انوتیشن را برای فیلدی استفاده کنیم، مانع از آن میشویم که ستونی برای آن در دیتابیس لحاظ شود. شاید ما در جاوا به فیلدهایی نیاز داشته باشیم، که وجود ستونی برای آنها الزامی نباشد، با استفاده از @Transient به این هدف دست مییابیم!
- @Enumerated
برای نگاشت کلاسهای enum استفاده میشود.
توضیح بیشتر از این خط را در گوگل جستوجو کنید!
- کالشکنی (مجموعهای) از انواع ساده
خب تقریبا داریم به جاهای قشنگ هایبرنیت میرسیم. مطمئن هستم تا اینجای کار کلی برایتان سوال پیش آمده که چطور ارتباطات بین Entityها را برقرار کنیم؟ چطور لیستی (مثلا از تلفن) را نگاشت کنیم؟ همه و همه را خواهم گفت. حوصله کنید و همراه باشید!
در اینجا میخواهیم به این موضوع اشاره کنیم که اگر بخواهیم لیستی از از انواع ساده را مدیریت کنیم چه باید بکنیم. اول توضیح دهیم که منظورمان از انواع ساده چیست. مقصود از انواع ساده، کلاسهایی هستند که Entity نیستند. مثلا لیستی از Stringها، لیستی از Integerها و امثالهم. اینها را اگر بخواهیم مدیریت کنیم از ۲ انوتیشن استفاده میکنیم.
اونتیشن @ElementCollection مشخص میکند که این فیلد، شامل لیستی از انواع ساده است (یعنی Stringها و یا هر چیز دیگری که Entity نیست). در کنار این انوتیشن باید از انوتیشن دیگری نیز استفاده کنیم. یعنی انوتیشن @CollectionTable که با استفاده از آن میتوانیم اطلاعات جدول مربوطهاش را مدیریت کنیم.
ببینید، وقتی ما مثلا لیستی از Stringها را داشته باشیم، منطقا هایبرنیت باید جدولی مجزا برای آن درنظر بگیرید که به کمک کلیدخارجی با Entity ما ارتباط دارد. حال این جدول چه مشخصاتی باید داشته باشد؟ بطور مثال چه نامی باید داشته باشد؟ اینها را با استفاده از @CollectionTable مشخص میکنیم.
پرسش. منظور شما از لیست چیست؟ منظورتان کلاس List یا پیادهسازی آن مثل ArrayList است؟
پاسخ. منظور من مجموعهای از دادهها است. یعنی هم میتواند ArrayList باشد هم HashSet و یا HashMap و یا هر Collection دیگری. لیست منظورم دقیقا یعنی مجموعهای از انواع ساده.
پرسش. کلیدخارجی چیست؟ منظورم آنجایی است که گفتید هایبرنیت یک جدول جدا در نظر میگیرد و به کمک کلیدخارجی به جدول اصلی ارتباط میدهد.
پاسخ. کلیدخارجی هم مشابه مبحث blob کاملا مربوط به بحث دیتابیس است. یادگیری آن هم به عهدهی خود شماست. انتظار من بر این بود که کلیدخارجی را بشناسید، حال که اینطور نیست عیبی ندارد. همینجا صبر کنید، و اول کلیدخارجی را جستوجو کنید و یادبگیرید، بعد ادامه دهید.
خب بیایید فرض کنیم میخواهیم در کلاس دانشجو (Student) یک لیستی از موضوعات مورد علاقهی دانشجو داشته باشیم! فرضی است کاملا! بر نوع مثال خرده نگیرید. باید چکار کنیم؟ با توجه به توضیحاتی که دادیم باید ساده باشد. یک ArrayList به موجودیت (Entity) دانشجو اضافه میکنیم.
نکتهی بسیارمهم: وقت آن است که با دقت تمام خط به خط کدها را نگاه کنید و ملاحظه کنید که دقیقا چه اتفاقی افتاده است. من تمام خروجیها بعلاوهی کدها را میگذارم. فقط با تفسیر خط به خط و نوشتن خودتان میتوانید مطمئن شوید که فرا گرفتید. غیر از این احتمالا چیزی دستگیرتان نشود.
کلاس Student:
package entities;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Student {
@EmbeddedId
private StudentKey studentKey;
@Column(name = "sname")
private String name;
@Column(name = "stell")
private String tell;
private String address;
@ElementCollection(fetch = FetchType.EAGER) /// از این جا به بعد خوب دقت کنید
@CollectionTable(name = "alaghemandiha")
@Column(name = "value")
private List<String> favorites = new ArrayList<String>();
public Student(StudentKey studentKey, String name, String tell, String address) {
this.studentKey = studentKey;
this.name = name;
this.tell = tell;
this.address = address;
}
public List<String> getFavorites() {
return favorites;
}
public void setFavorites(List<String> favorites) {
this.favorites = favorites;
}
public StudentKey getStudentKey() {
return studentKey;
}
public void setStudentKey(StudentKey studentKey) {
this.studentKey = studentKey;
}
public Student() {} // non-arg constructor
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTell() {
return tell;
}
public void setTell(String tell) {
this.tell = tell;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
}
کلاس Test:
import entities.Student;
import entities.StudentKey;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
public class Test {
public static void main(String[] args) {
StudentKey studentKey = new StudentKey(12345,67890);
Student student = new Student(studentKey,"Mohamad", "09121112233", "Tehran");
student.getFavorites().add("Book");
student.getFavorites().add("Programming");
student.getFavorites().add("Dancing");
SessionFactory sessionFactory = new Configuration().configure("hibernate.cfg.xml")
.addAnnotatedClass(Student.class)
.buildSessionFactory();
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
session.save(student);
session.getTransaction().commit();
session.close();
sessionFactory.close();
}
}
خروجی مربوط به جداول:
پرسش. ببینید تا اینجای کار متوجه شدم که چطور ذخیره سازی میکنید، اما اینکه همینها رو چطور باید بازیابی کنم برایم سوال است! چطور باید بعنوان مثال اطلاعات یک دانشجو، بهمراه تمام علاقهمندیهایش را داشته باشم؟
پاسخ. متوجه نگرانی شما هستم، اجازه دهید این موضوع را موکول کنیم به زمانی که میخواهیم راجع به بحث CRUD توضیح دهیم. آنجا بازیابی اطلاعات بحث خواهد شد، دوباره در بخش HQL به آن خواهیم پرداخت، باز هم در مبحث Criteria به آن برخواهیم گشت. پس نگران نباشید. توصیهی قبلی من را بیاد دارید؟ به آن چیزی که نمیدانید تمرکز نکنید، به آنهایی که میدانید تمرکز کنید. همراه باشید!
پرسش. متوجه شدم که چطور یک ArrayList را باید به عنوان یک جدول در نظر گرفت و یک نام هم حتا برای ستون آن گذاشتیم ()، اما چطور یک HashMap را نگاشت کنیم؟ مگر به ۲ ستون نیاز نداریم؟
پاسخ. صحیح است. برای این منظور باید چنین کرد:
@ElementCollection
@CollectionTable(name="track")
@MapKeyColumn (name = "position")
@Column(name = "title")
private Map<Integer, String> tracks = new HashMap<>();
- چطور یک آبجکت را Embedd کنیم؟
بیاد دارید چطور کلید مرکب ساختیم؟ از همان تکنیک میتوانیم برای Embed کردن یک آبجکت درون یک کلاس استفاده کنیم. اگر خوب بیاد ندارید ابتدا قسمت کلید مرکب را مجددا مطالعه کنید، و بعد به کدهای پایین نگاه کنید.
دقت کنید که تمام فیلدهای آبجکت Embed شده تبدیل به ستون در موجودیت مربوطه میشود.
کلاس Address:
package entities;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String country;
private String city;
private String street;
public Address() {}
public Address(String country, String city, String street) {
this.country = country;
this.city = city;
this.street = street;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
تکه کد مربوطه به Address در کلاس Student:
@Embedded
private Address address;
خب دوستان عزیز، حرف بیشتری راجع به نگاشت یک Entity ندارم، اگر حالتی هست که پوشش داده نشده شما متذکر شوید. چیز دیگری که مانده بحث نگاشت روابط است. روابط یکبهیک، یکبهچند، چندبهیک و چندبهچند و بحث مربوط به وراثت.
میخواهم ابتدا در مورد عملیات CRUD با هایبرنیت صحبت کنم، بعد به Relation Mapping بپردازیم.
عملیات CRUD در هایبرنیت
همانطور که احتمالا میدانید، CRUD مخفف Create، Read، Update و Delete است. این چهارگانه، عمدهی عملیاتی است که ما با دیتابیس انجام میدهیم. فلذا در ادامه میخواهیم به این چهار عملیات اصلی بپردازیم. حدس من بر این است که شما این چهار عمل را با خود SQL انجام دادهاید، و حتا از طریق jdbc تجربهی آن را دارید. حالا فرصت مناسبی است که این چهار عملیات را با هایبرنیت بررسی نماییم.
- نحوهی درج (insert)
خب تا اینجای کار نحوهی Insert کردن به دیتابیس را مشاهده کردهاید. ما برای insert از متد save در کلاس Session استفاده کردیم. بیایید مرور کنیم...
قبلا نیز اشاره کردم، برای انجام عملیات با دیتابیس ما نیاز به یک تراکنش داریم (اگر مفصل میخواهید بدانید، انتهای قسمت اول، و ابتدای قسمت دوم را مطالعه کنید). در داخل یک تراکنش است که ما با دیتابیس صحبت میکنیم. در تمام مثالها نیز در داخل یک تراکنش متد save را فراخوانی کردیم.
تاکید میکنم، برای عملیات insert بکمک هایبرنیت، از متد save در کلاس Session استفاده میکنیم:
session.beginTransaction();
session.save(object); // آبجکتی را که میخواهیم پرسیست کنیم
session.getTransaction().commit();
- نحوهی بازیابی (retrieve)
خاطرتان هست که گفتیم هر Entity باید یک کلیداصلی داشته باشد که با انوتیشن @Id نشانش میدادیم؟ فلسفهی کلید اصلی چیست؟ اینکه با کمک آن به رکوردی یکتا برسیم. خب، منطقا در هایبرنیت هم برای بازیابی به کلیداصلی احتیاج داریم.
برای بازیابی اطلاعات، ما از متد get در کلاس Session استفاده مینماییم. اما پارامترهای این متد چیست؟
این متد دریکی از شکلهای خود که ما استفاده میکنیم، ۲ پارامتر بعنوان آرگومان ورودی دریافت میکند. آرگومان اول نام کلاسی (entity) است که ما میخواهیم یک رکورد از آن را بازیابی کنیم. آرگومان دوم کلید آن رکورد است.
فرض کنید در جدول Student، دانشجویی داریم که کلید آن 2231 است. برای بازیابی تمام رکورد آن (در قالب یک آبجکت) چنین رفتار میکنیم:
ابتدا یک آبجکت از نوع دانشجو میسازیم، و سپس خروجی متد get را به آن پاس میدهیم.
Student student = new Student();
session.beginTransaction();
student = session.get(Student.class, 2231);
session.getTransaction().commit();
حال تمام اطلاعات مربوط به دانشجو درون آن آبجکت قرار دارد. به همین راحتی میتوانیم از جدول اطلاعات را بارکشی کنیم.
پرسش. تا اینجا بسیار ساده بود، اما صبر کنید، در آخرین مثالی که برای ما زدید برای کلاس دانشجو یک کلید مرکب ساختید، بعبارتی یک کلاس دیگر بنام StudentKey ایجاد کردید، و ترکیب شمارهملی و شمارهشناسنامه آن را بعنوان کلیداصلی دانشجو قرار دادید، در این حالت چطور باید کلید را مشخص کرد؟
پاسخ. خوشحالم که چنین هوشیارانه در حال دنبال کردن مقالات هستید، بله کاملا بیاد دارم، و تصمیم داشتم که اکنون بعنوان مثال دوم از بازیابی مطرح کنم. گرچه تفاوت خاصی ندارد، تنها اینبار بعنوان کلید (که در مثال اول یک عدد integer بود) باید یک آبجکت کلید (در این مثالی که شما ذکر کردید StudentKey) بعنوان آرگومان دوم پاس بدهید. ادامه مطلب را ملاحظه کنید.
خب، در مثالی که تا به اینجای کار توسعه دادیم، کلید اصلی مرکب بود، من عینا کدهایی که باید برای بازیابی چنین رکوردی استفاده شود را برای شما کپی/پیست میکنم. لطفا خوب به کدها بنگرید:
کلاس Test:
public class Test {
public static void main(String[] args) {
Student student;
SessionFactory sessionFactory = new Configuration().configure("hibernate.cfg.xml")
.addAnnotatedClass(Student.class)
.buildSessionFactory();
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
student = session.get(Student.class, new StudentKey(12345, 67890)); // به این خط توجه شود
session.getTransaction().commit();
System.out.println(student);
session.close();
sessionFactory.close();
}
}
خروجی را ملاحظه کنید:
Student{studentKey=entities.StudentKey@6e019, name='Mohamad', tell='09121112233', favorites=[Book, Programming, Dancing], address=entities.Address@1255b1d1}
همانطور که ملاحظه میکنید، براحتی رکورد مربوطه بازیابی شد.
- نحوهی حذف (delete)
اجازه دهید دیگر بدون پرگویی بگویم، برای حذف یک رکورد دو مرحله کار باید انجام داد:
- خواندن آن رکورد به کمک متد get
- حذف همان آبجکتی که بازیابی شده است به کمک متد delete
همانطور که ملاحظه کردید، برای حذف از متد delete در کلاس Session استفاده میکنیم. به کد زیر توجه کنید، میخواهم تنها رکورد موجودی که در دیتابیس دارم را حذف نمایم!
کلاس Test:
public class Test {
public static void main(String[] args) {
Student student;
SessionFactory sessionFactory = new Configuration().configure("hibernate.cfg.xml") .addAnnotatedClass(Student.class)
.buildSessionFactory();
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
student = session.get(Student.class, new StudentKey(12345, 67890)); student.setTell("0935936912");
session.delete(student);
session.getTransaction().commit();
session.close();
sessionFactory.close();
} }
و خروجی لاگ:
نکتهی جالب را توجه کردید؟ رکورد نه تنها از جدول student حذف شد، بلکه رکوردهای مرتبط آن نیز در جدول alaghemandiha نیز حذف شد.
پرسش. هایبرنیت این کار را انجام میدهد؟ میتوانیم این رفتار را کنترل کنیم؟
پاسخ. بله، هایبرنیت اینکار را انجام میدهد. قطعا در این مثال بخصوص، ما رفتاری غیر از این را انتظار نداشتیم، اما بطور کلی بحثی بنام cascading وجود دارد که مربوط به مدیریت این بخشهاست. در قسمتهای بعدی به آن میپردازیم.
- نحوهی آپدیت (update)
انجام عملیات بروز، مشابه عملیات delete است، یعنی ابتدا آبجکت مربوطه را از دیتابیس بازیابی میکنیم، سپس آن را به کمک متدهای setterهمان آبجکت تغییر میدهیم، و در نهایت از متد update استفاده میکنیم.
فلذا برای آپدیت کردن از متد update در کلاس Session استفاده میکنیم. به مثال زیر توجه کنید، و دقیقا ببینید که چه رخ داده است. من خروجی + لاگ هایبرنیت را برای شما میآورم.
کلاس Test:
public class Test {
public static void main(String[] args) {
Student student;
SessionFactory sessionFactory = new Configuration().configure("hibernate.cfg.xml")
.addAnnotatedClass(Student.class)
.buildSessionFactory();
Session session = sessionFactory.getCurrentSession();
session.beginTransaction();
student = session.get(Student.class, new StudentKey(12345, 67890));
student.setTell("0935936912");
session.update(student);
session.getTransaction().commit();
session.close();
sessionFactory.close();
}
}
به لاگ هایبرنیت مربوط به update توجه کنید:
Hibernate: update Student set city=?, country=?, street=?, sname=?, stell=? where shomare_meli=? and shomare_shenasname=?
و همچنین ببینید که در دیتابیس هم مقدار شمارهتلفن تغییر کرده است:
پرسش. شما مدام میگید که متد فلان از کلاس Session! در صورتیکه اصلا Session کلاس نیست، یک رابط است. اشتباه خودتان را اصلاح کنید.
پاسخ. خوشحالم که به گفتههای من بسنده نکردید و خودتان جستوجو کردید. ابتدا بگویم که احتمال خطا در گفتههای من زیاد است، و به این خاطر ازتون پوزش میخواهم.
بله Sesssion یک interface است که با آبجکتی از کلاس SessionFactory مقداردهی میشود. اما از دید من خود رابط نیز نوعی کلاس است. ولی خوشحالم که متذکر شدید تا دوستان دیگر هم متوجه اصطلاحی که من بکار میبرم شده باشند.
سخن پایانی
خب، حالا با عملیات CRUD هم آشنا شدید. وقت آن است که به یکی از جدیترین بخشهای هایبرنیت برویم. یعنی Relation Mapping. به من بگویید، آیا این مقاله توانسته نیاز شما رو برطرف سازد؟ توجه داشته باشید که برای نگارش این مقالات وقت خیلی زیادی صرف شده است، فلذا اهمیت زیادی برای من دارد تا بدانم آنطور که باید و شاید بکار میآید یا خیر.
پایان بخش سوم.
مطلبی دیگر از این انتشارات
تحلیل بازاریابیِ خردهفروشیها – بخش ششم
مطلبی دیگر از این انتشارات
تحلیل بازاریابیِ خردهفروشیها – بخش سوم
مطلبی دیگر از این انتشارات
تحلیل بازاریابیِ خردهفروشیها – بخش اول