Skip to content

2. انتیته‌های JPA

2.1. مثال ۱ – نمایش شیء یک جدول واحد

2.1.1. جدول [personne]

یک پایگاه داده را در نظر بگیرید که شامل یک جدول واحد به نام [personne] است که هدف آن ذخیره برخی اطلاعات درباره افراد است:

 
ID
کلید اصلی جدول
VERSION
نسخهٔ سطر در جدول. هر بار
هر بار که شخص اصلاح می‌شود، شماره نسخهٔ آن افزایش می‌یابد.
NOM
نام شخص
PRENOM
نام
DATENAISSANCE
تاریخ تولد
MARIE
یک عدد صحیح ۰ (مجرد) یا ۱ (متأهل)
NBENFANTS
تعداد فرزندان شخص

2.1.2. واحد [Personne]

ما در محیط زمان اجرای زیر هستیم:

لایه JPA [5] باید به‌عنوان یک پل بین دنیای رابطه‌ای پایگاه داده [7] و دنیای شیءگرا [4] که توسط برنامه‌های جاوا [3] مدیریت می‌شود، عمل کند.231ZQX. این پل از طریق پیکربندی ایجاد می‌شود و دو روش برای انجام این کار وجود دارد:

  1. با استفاده از فایل‌های XML. این عملاً تنها راه انجام آن بود تا پیش از ظهور JDK 1.5
  1. استفاده از anotationهای جاوا از نسخه JDK 1.5 به بعد

در این سند، ما تقریباً منحصراً از روش دوم استفاده خواهیم کرد.

شیء [Personne] که نمایانگر جدول [personne] ارائه‌شده در بالا است، می‌تواند به شکل زیر باشد:


...

@SuppressWarnings("unused")
@Entity
@Table(name="Personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // سازنده‌ها
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
        setNom(nom);
        setPrenom(prenom);
        setDatenaissance(datenaissance);
        setMarie(marie);
        setNbenfants(nbenfants);
    }

    // toString
    public String toString() {
...
    }

    // گیرنده و تنظیم‌کننده
...
}

پیکربندی با استفاده از anotationهای @Annotation جاوایی انجام می‌شود. آنونیشن‌های جاوا یا توسط کامپایلر و یا توسط ابزارهای تخصصی در زمان اجرا پردازش می‌شوند. به جز آنونیشن روی خط ۳ که برای کامپایلر در نظر گرفته شده است، تمام آنونیشن‌های دیگر در اینجا برای پیاده‌سازی JPA که در حال استفاده است، چه Hibernate و چه TopLink، در نظر گرفته شده‌اند. بنابراین، آنها در زمان اجرا پردازش خواهند شد. در صورت عدم وجود ابزارهایی که قادر به تفسیر آن‌ها باشند، این حاشیه‌نویسی‌ها نادیده گرفته می‌شوند. بنابراین، کلاس [Personne] نشان‌داده‌شده در بالا می‌تواند در زمینه‌ای خارج از JPA استفاده شود.

باید بین دو سناریو برای استفاده از نشانه‌گذاری‌های JPA در یک کلاس C مرتبط با جدول T تمایز قائل شد:

  1. جدول T از قبل وجود دارد: در این صورت، حاشیه‌نویسی‌های JPA باید ساختار موجود (نام‌ها و تعریف‌های ستون‌ها، محدودیت‌های یکپارچگی، کلیدهای خارجی، کلیدهای اصلی و غیره) را بازتولید کنند.
  2. جدول T وجود ندارد و بر اساس نشانک‌های یافت‌شده در کلاس C ایجاد خواهد شد.

مورد دوم آسان‌ترین مورد برای رسیدگی است. با استفاده از نشانه‌گذاری‌های JPA، ساختار جدول T مورد نظر خود را مشخص می‌کنیم. مورد اول اغلب پیچیده‌تر است. ممکن است جدول T مدت‌ها پیش، خارج از هرگونه زمینه JPA ایجاد شده باشد. بنابراین ساختار آن ممکن است برای پل رابطه‌ای/شیءگرا JPA نامناسب باشد. برای ساده‌سازی موضوع، مورد دوم را در نظر می‌گیریم که در آن جدول T مرتبط با کلاس C بر اساس حاشیه‌نویسی‌های JPA کلاس C ایجاد خواهد شد.

بیایید در مورد انوتیشن‌های JPA برای کلاس [Personne] توضیح دهیم:

  • خط ۴: انوتیشن @Entity اولین انوتیشن ضروری است. این انوتیشن قبل از خط اعلام کلاس قرار می‌گیرد و نشان می‌دهد که کلاس مورد نظر باید توسط لایه پایداری JPA مدیریت شود. در صورت عدم وجود این انوتیشن، تمام انوتیشن‌های دیگر JPA نادیده گرفته می‌شوند.
  • خط ۵: anotasyon @Table جدول پایگاه داده‌ای را که کلاس نماینده آن است مشخص می‌کند. آرگومان اصلی آن `name` است که نام جدول را مشخص می‌کند. اگر این آرگومان حذف شود، نام جدول بر اساس نام کلاس، در این مورد [Personne]، انتخاب خواهد شد. بنابراین در مثال ما، anotasyon @Table غیرضروری است.
  • خط ۸: تگ @Id برای تعیین فیلدی در کلاس که با کلید اصلی جدول مطابقت دارد، استفاده می‌شود. این تگ الزامی است. در اینجا نشان می‌دهد که فیلد id در خط ۱۱ با کلید اصلی جدول مطابقت دارد.
  • خط ۹: آناوتیشن @Column برای مرتبط کردن یک فیلد در کلاس با ستون جدول که فیلد به آن نگاشت می‌شود، استفاده می‌شود. ویژگی name نام ستون در جدول را مشخص می‌کند. اگر این ویژگی حذف شود، نام ستون با نام فیلد یکسان خواهد بود. در مثال ما، بنابراین آرگومان name الزامی نبود. آرگومان `nullable=false` مشخص می‌کند که ستونی که با فیلد مرتبط است نمی‌تواند مقدار `NULL` را داشته باشد و بنابراین فیلد باید حتماً یک مقدار داشته باشد.
  • خط ۱۰: تگ @GeneratedValue مشخص می‌کند که کلید اصلی چگونه توسط SGBD تولید می‌شود. این حالت در تمام مثال‌های ما صادق است. این الزامی نیست. بنابراین، شخص ما می‌تواند شماره دانشجویی داشته باشد که به عنوان کلید اصلی عمل می‌کند و توسط SGBD تولید نمی‌شود، بلکه توسط برنامه تعیین می‌شود. در این صورت، حاشیه‌نویسی @GeneratedValue وجود نخواهد داشت. آرگومان `strategy` مشخص می‌کند که کلید اصلی چگونه توسط SGBD تولید می‌شود. همه SGBDها از یک تکنیک یکسان برای تولید مقادیر کلید اصلی استفاده نمی‌کنند. برای مثال:
Firebird
از یک تولیدکننده مقدار استفاده می‌کند که قبل از هر درج فراخوانی می‌شود
SQL server
میدان کلید اصلی با نوع Identity تعریف شده است. نتیجه مشابه تولیدکننده مقدار Firebird است، با این تفاوت که مقدار کلید تنها پس از درج ردیف مشخص می‌شود.
Oracle
از ابجکتی به نام SEQUENCE استفاده می‌کند که دوباره به عنوان تولیدکننده مقدار عمل می‌کند

لایه JPA باید بسته به SGBD، دستورات متفاوتی از SQL را برای ایجاد تولیدکننده مقدار تولید کند. از طریق پیکربندی به آن گفته می‌شود که چه نوع SGBD را باید مدیریت کند. در نتیجه، می‌تواند استراتژی معمول برای تولید مقادیر کلید اصلی برای آن SGBD را تعیین کند. آرگومان strategy = GenerationType.AUTO به لایه JPA دستور می‌دهد که از این استراتژی استاندارد استفاده کند. این تکنیک در تمام مثال‌های این سند برای هفت نمونه SGBD مورد استفاده، کار کرده است.

  • خط ۱۴: تگ @Version فیلدی را که برای مدیریت دسترسی همزمان به همان سطر در جدول استفاده می‌شود، مشخص می‌کند.

برای درک این مسئله دسترسی همزمان به همان سطر در جدول [personne]، فرض کنید یک برنامه وب اجازه می‌دهد جزئیات یک شخص به‌روزرسانی شود و سناریوی زیر را در نظر بگیرید:

در زمان T1، کاربری به نام U1 وارد حالت ویرایش برای شخص P می‌شود. در این نقطه، تعداد فرزندان 0 است. آن‌ها این عدد را به ۱ تغییر می‌دهند، اما قبل از اینکه بتوانند تغییرات خود را ذخیره کنند، کاربری با شناسه U2 ویرایش همان شخص P را آغاز می‌کند. از آنجایی که U1 هنوز تغییرات خود را ذخیره نکرده است، U2 می‌بیند که تعداد فرزندان روی صفحه‌اش به صورت 0 نمایش داده می‌شود. U2 نام شخص P را به حروف بزرگ تغییر می‌دهد. سپس U1 و U2 تغییرات خود را به ترتیب ذخیره می‌کنند. تغییر U2 غالب خواهد بود: در پایگاه داده، نام با حروف بزرگ نوشته می‌شود و تعداد فرزندان روی صفر باقی می‌ماند، حتی اگر U1 فکر کند آن را به ۱ تغییر داده است.

مفهوم نسخهٔ یک شخص به ما کمک می‌کند این مشکل را حل کنیم. بیایید دوباره همان مورد استفاده را در نظر بگیریم:

در زمان T1، کاربری به نام U1 وارد حالت ویرایش برای شخص P می‌شود. در این نقطه، تعداد فرزندان 0 است و نسخه V1 است. آنها تعداد فرزندان را به 1 تغییر می‌دهند، اما قبل از اینکه تغییرات خود را ذخیره کنند، کاربری به نام U2 ویرایش همان شخص P را آغاز می‌کند. از آنجا که U1 هنوز تغییرات خود را ذخیره نکرده است، U2 تعداد فرزندان را 0 و نسخه را V1 می‌بیند. U2 نام شخص P را به حروف بزرگ تغییر می‌دهد. سپس U1 و U2 تغییرات خود را به ترتیب commit می‌کنند. قبل از commit کردن یک تغییر، بررسی می‌کنیم که کاربری که شخص P را اصلاح می‌کند، همان نسخه‌ای را در اختیار داشته باشد که در حال حاضر برای شخص P ثبت شده است. این مورد برای کاربر U1 صادق است. ویرایش آن‌ها بنابراین پذیرفته می‌شود و نسخهٔ شخص ویرایش‌شده از V1 به V2 تغییر می‌کند تا نشان دهد که شخص دچار تغییر شده است. هنگام اعتبارسنجی ویرایش توسط U2، متوجه خواهیم شد که U2 دارای نسخه V1 از شخص P است، در حالی که نسخه فعلی شخص P، V2 است. سپس می‌توانیم به کاربر U2 اطلاع دهیم که شخص دیگری قبلاً تغییراتی ایجاد کرده است و او باید از نسخه جدید شخص P دوباره شروع کند. او این کار را انجام می‌دهد، نسخه‌ای از شخص P (V2) را که اکنون یک فرزند دارد بازیابی می‌کند، نام را بزرگ‌نویس می‌کند و تغییرات را تأیید می‌کند. تغییر آنها پذیرفته خواهد شد اگر شخص ثبت‌شده P هنوز نسخه V2 را داشته باشد. در نهایت، تغییرات انجام‌شده توسط U1 و U2 در نظر گرفته خواهند شد، در حالی که در مورد استفاده بدون نسخه‌ها، یکی از تغییرات از دست رفته بود.

لایه [dao] برنامهٔ مشتری می‌تواند نسخهٔ کلاس [Personne] را مدیریت کند. هرگاه یک شیء P تغییر کند، نسخهٔ آن شیء در جدول یک واحد افزایش می‌یابد. توضیحیه @Version اجازه می‌دهد این مدیریت به لایه JPA واگذار شود. فیلد مورد نظر لازم نیست مانند مثال، version نامیده شود. این فیلد می‌تواند هر نام دیگری داشته باشد.

فیلدهای متناظر با آنوتیشن‌های @Id و @Version برای اهداف پایداری (persistence) وجود دارند. اگر کلاس [Personne] نیازی به پایدارسازی نداشت، این فیلدها ضروری نبودند. بنابراین می‌توانیم ببینیم که یک شیء بسته به اینکه نیاز به پایدارسازی داشته باشد یا نه، به شیوه‌ای متفاوت نمایش داده می‌شود.

  • خط ۱۷: بار دیگر، anotation @Column اطلاعاتی را در مورد ستون جدول [personne] که با فیلد nom از کلاس Personne مرتبط است، ارائه می‌دهد. در اینجا دو آرگومان جدید می‌یابیم:
    • unique=true نشان می‌دهد که نام یک شخص باید منحصر به فرد باشد. این امر منجر به افزودن یک قید منحصر به فرد به ستون NOM در جدول [personne] می‌شود.
    • length=30 تعداد کاراکترهای ستون NOM را روی 30 تنظیم می‌کند. این بدان معناست که نوع داده این ستون VARCHAR(30) خواهد بود.
  • خط ۲۴: anotیشن @Temporal برای مشخص کردن نوع (SQL) که باید به یک ستون یا فیلد از نوع تاریخ/زمان اختصاص داده شود، استفاده می‌شود. نوع TemporalType.DATE تنها به تاریخ اشاره دارد، بدون زمان همراه. سایر انواع ممکن TemporalType.TIME برای رمزگذاری زمان و TemporalType.TIMESTAMP برای رمزگذاری تاریخ و زمان هستند.

اکنون بیایید در مورد بقیه کد کلاس [Personne] توضیح دهیم:

  • خط ۶: کلاس رابط Serializable را پیاده‌سازی می‌کند. sérialisation یک شیء شامل تبدیل آن به دنباله‌ای از بیت‌ها است. désérialisation عملیات معکوس است. سریال‌سازی و دِسریال‌سازی به‌ویژه در برنامه‌های کلاینت–سرور که اشیاء از طریق شبکه مبادله می‌شوند، استفاده می‌شوند. برنامه‌های کاربردی کلاینت یا سرور از این عملیات بی‌خبر هستند، عملیاتی که به‌صورت شفاف توسط JVM انجام می‌شود. با این حال، برای امکان‌پذیر شدن این کار، کلاس‌های اشیاء مبادله شده باید با کلمه کلیدی Serializable «برچسب‌گذاری» شوند.
  • خط ۳۷: یک سازنده برای کلاس. توجه داشته باشید که فیلدهای id و version در میان پارامترها گنجانده نشده‌اند. این به این دلیل است که این دو فیلد توسط لایه JPA مدیریت می‌شوند و نه توسط برنامه.
  • خطوط ۵۱ به بعد: متدهای get و set برای هر یک از فیلدهای کلاس. توجه داشته باشید که انوتیشن‌های JPA ممکن است به جای خود فیلدها، روی متدهای get آنها قرار گیرند. قرارگیری آنوتیشن‌ها، مدی را که JPA باید برای دسترسی به فیلدها استفاده کند، مشخص می‌کند:
    • اگر anotations در سطح فیلد قرار داده شوند، JPA برای خواندن یا نوشتن فیلدها مستقیماً به آنها دسترسی خواهد داشت
    • اگر anotations در سطح get قرار داده شوند، JPA برای خواندن یا نوشتن فیلدها از طریق متدهای get/set به آنها دسترسی خواهد داشت

موقعیت تگ @Id است که موقعیت تگ‌های JPA را در یک کلاس تعیین می‌کند. وقتی در سطح فیلد قرار گیرد، نشان‌دهنده دسترسی مستقیم به فیلدها است؛ وقتی در سطح get قرار گیرد، نشان‌دهنده دسترسی به فیلدها از طریق متدهای get و set است. سایر anotationها باید به همان شیوه anotation @Id قرار گیرند.

2.1.3. پروژه تست Eclipse

ما آزمایش‌های اولیه خود را با استفاده از موجودیت [Personne] که در بالا ذکر شد، انجام خواهیم داد. این کار را با استفاده از معماری زیر انجام می‌دهیم:

  • در [7]: پایگاه داده‌ای که از anotationهای موجود در انتیت [Personne] و همچنین پیکربندی‌های اضافی انجام‌شده در فایلی به نام [persistence.xml] ایجاد خواهد شد
  • به [5, 6]: لایه‌ای از JPA که توسط Hibernate پیاده‌سازی شده است
  • در [4]: موجودیت [Personne]
  • به [3]: یک برنامهٔ آزمایشی مبتنی بر کنسول

ما آزمایش‌های مختلفی را انجام خواهیم داد:

  • ایجاد اسکیما برای BD با استفاده از یک اسکریپت Ant و ابزارهای Hibernate
  • جدول BD را ایجاد کرده و آن را با مقداری داده پر کنید
  • با استفاده از طرحواره BD و انجام چهار عملیات پایه روی جدول [personne] ( درج، به‌روزرسانی، حذف، پرس‌وجو)

ابزارهای مورد نیاز به شرح زیر است:

  • ایکلپس و افزونه‌های آن، همان‌طور که در بخش 5.2 توضیح داده شده است.
  • پروژه [hibernate-personnes-entites] که در پوشه <examples>/hibernate/direct/people-entities قابل دسترسی است
  • فایل‌های مختلف SGBD که در ضمیمه‌ها (بخش ۵ و بعد) توضیح داده شده‌اند.

پروژه Eclipse به شرح زیر است:

  • در [1]: پوشه پروژه اکلیپس
  • در [2]: پروژه‌ای که به اکلپس وارد شده است (File / Import)
  • در [3]: موجودیت [Personne] تحت آزمون
  • در [4]: برنامه‌های آزمون
  • در [5]: [persistence.xml] فایل پیکربندی لایه JPA است
  • در [6]: کتابخانه‌های مورد استفاده. این‌ها در بخش 1.5 شرح داده شده‌اند.
  • در [8]: یک اسکریپت Ant که برای تولید جدول مرتبط با موجودیت [Personne] استفاده خواهد شد
  • در [9]: فایل‌های [persistence.xml] برای هر یک از SGBD‌های مورد استفاده
  • در [10]: اسکیماهای پایگاه دادهٔ تولیدشده برای هر یک از فایل‌های SGBD مورد استفاده

ما این عناصر را یکی‌یکی توصیف خواهیم کرد.

2.1.4. وجود [Personne] (2)

ما اصلاح جزئی در توضیحات قبلی ارائه‌شده برای موجوده [Personne] انجام می‌دهیم و همچنین اطلاعات اضافی را ارائه می‌کنیم:


package entites;

...

@SuppressWarnings({ "unused", "serial" })
@Entity
@Table(name="jpa01_personne")
public class Personne implements Serializable{

    @Id
    @Column(name = "ID", nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;

    @Column(name = "VERSION", nullable = false)
    @Version
    private int version;

    @Column(name = "NOM", length = 30, nullable = false, unique = true)
    private String nom;

    @Column(name = "PRENOM", length = 30, nullable = false)
    private String prenom;

    @Column(name = "DATENAISSANCE", nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(name = "MARIE", nullable = false)
    private boolean marie;

    @Column(name = "NBENFANTS", nullable = false)
    private int nbenfants;

    // سازنده‌ها
    public Personne() {
    }

    public Personne(String nom, String prenom, Date datenaissance, boolean marie,
            int nbenfants) {
....
    }

    // toString
    public String toString() {
        return String.format("[%d,%d,%s,%s,%s,%s,%d]", getId(), getVersion(),
                getNom(), getPrenom(), new SimpleDateFormat("dd/MM/yyyy")
                        .format(getDatenaissance()), isMarie(), getNbenfants());
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
}
  • خط ۷: جدولی را که با انتیت [Personne] مرتبط است، [jpa01_personne] نام‌گذاری می‌کنیم. در این سند، جداول مختلفی در قالب طرحی به نام jpa ایجاد خواهند شد. تا پایان این آموزش، طرح jpa شامل جداول متعددی خواهد بود. برای کمک به خواننده در پیگیری آن‌ها، جداول مرتبط با یکدیگر پیشوند یکسانی خواهند داشت: jpaxx_.
  • خط ۴۵: متدی با نام [toString] برای نمایش ابجویی با نام [Personne] در کنسول.

2.1.5. پیکربندی لایه دسترسی به داده

در پروژهٔ Eclipse فوق، پیکربندی لایهٔ JPA توسط فایل [META-INF/persistence.xml] انجام می‌شود:

در زمان اجرا، فایل [META-INF/persistence.xml] در دایرکتوری classpath برنامه جستجو می‌شود. در پروژهٔ Eclipse ما، همهٔ محتویات پوشه‌های [/src] و [1] به پوشه‌هایی به نام‌های [/bin] و [2] کپی می‌شوند. این بخشی از پروژه classpath است. به همین دلیل [META-INF/persistence.xml] زمانی که لایه JPA پیکربندی شود، پیدا خواهد شد.

به‌طور پیش‌فرض، اکلیپس کد منبع را در پوشه [/src] پروژه قرار نمی‌دهد، بلکه مستقیماً در خود پوشه پروژه قرار می‌دهد. تمام پروژه‌های Eclipse ما طوری پیکربندی خواهند شد که کد منبع در [/src] و کلاس‌های کامپایل‌شده در [/bin] قرار گیرند، همان‌طور که در بخش 5.2.1 نشان داده شده است.

بیایید پیکربندی لایه JPA را که در فایل [persistence.xml] پروژه ما تعریف شده است، بررسی کنیم:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  ارائه‌دهنده -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!--کلاس‌های پایدار -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- لاگ‌ها SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- اتصال JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  ایجاد خودکار طرحواره -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!--  گویش -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  properties DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

برای درک این پیکربندی، باید معماری دسترسی به داده‌های برنامه خود را مجدداً بررسی کنیم:

  • فایل [persistence.xml] لایه‌های [4, 5, 6] را پیکربندی خواهد کرد
  • [4]: پیاده‌سازی Hibernate از JPA
  • [5]: Hibernate از طریق یک استخر اتصال به پایگاه داده دسترسی پیدا می‌کند. استخر اتصال، مخزنی از اتصالات باز به SGBD است. به SGBD توسط چندین کاربر دسترسی پیدا می‌شود، هرچند به دلایل عملکردی، نمی‌تواند از حد N برای تعداد اتصالات همزمان فراتر رود. کد خوب نوشته شده، یک اتصال به SGBD را برای حداقل زمان ممکن باز می‌کند: دستورات SQL را صادر کرده و اتصال را می‌بندد. این کار را هر بار که نیاز به کار با پایگاه داده داشته باشد، تکرار می‌کند. هزینه باز و بسته کردن یک اتصال ناچیز نیست و اینجاست که استخر اتصال وارد عمل می‌شود. هنگامی که برنامه اجرا می‌شود، استخر اتصال، اتصالات N1 را به SGBD باز می‌کند. برنامه کاربردی هر زمان که به یک اتصال نیاز داشته باشد، یک اتصال باز را از استخر درخواست می‌کند. این اتصال به محض اینکه برنامه کاربردی دیگر به آن نیازی نداشته باشد، ترجیحاً در سریع‌ترین زمان ممکن به استخر بازگردانده می‌شود. اتصال بسته نمی‌شود و برای کاربر بعدی در دسترس باقی می‌ماند. بنابراین، استخر اتصالات سیستمی برای اشتراک‌گذاری اتصالات باز است.
  • [6]: درایور JDBC برای دستگاه SGBD

اکنون ببینیم که چگونه فایل [persistence.xml] لایه‌های [4, 5, 6] ذکر شده در بالا را پیکربندی می‌کند:

  • خط ۲: تگ ریشه فایل XML، <persistence> است.
  • خط ۳: از <persistence-unit> برای تعریف یک واحد پایداری استفاده می‌شود. ممکن است چندین واحد پایداری وجود داشته باشد. هر کدام یک نام (ویژگی 'name') و یک نوع تراکنش (ویژگی 'transaction-type') دارند. برنامه از طریق نام واحد پایداری به آن دسترسی پیدا می‌کند، در این مورد 'jpa'. نوع تراکنش RESOURCE_LOCAL نشان می‌دهد که برنامه خود با استفاده از SGBD تراکنش‌ها را مدیریت می‌کند. در اینجا نیز همین‌طور خواهد بود. هنگامی که برنامه در یک کانتینر EJB3 اجرا می‌شود، می‌تواند از سرویس تراکنش آن کانتینر استفاده کند. در این مورد، ما `transaction-type=JTA` (Java Transaction API) را تنظیم خواهیم کرد. `JTA` مقدار پیش‌فرض زمانی است که ویژگی `transaction-type` ذکر نشود.
  • خط ۵: تگ <provider> برای تعریف کلاسی استفاده می‌شود که رابط [javax.persistence.spi.PersistenceProvider] را پیاده‌سازی می‌کند، که به برنامه اجازه می‌دهد لایه پایداری را راه‌اندازی کند. از آنجا که ما از پیاده‌سازی JPA / Hibernate استفاده می‌کنیم، کلاس مورد استفاده در اینجا یک کلاس Hibernate است.
  • خط ۶: تگ <properties> ویژگی‌های خاص provider انتخاب‌شده را معرفی می‌کند. بنابراین، بسته به اینکه Hibernate، Toplink، Kodo و غیره انتخاب شده باشد، ویژگی‌های متفاوتی اعمال خواهند شد. موارد زیر مربوط به Hibernate هستند.
  • خط ۸: به Hibernate دستور می‌دهد تا classpath پروژه را اسکن کند تا کلاس‌های دارای @Entity را پیدا کند تا بتوان آن‌ها را مدیریت کرد. کلاس‌های @Entity را می‌توان با استفاده از تگ‌های <class>nom_de_la_classe</class>، مستقیماً در داخل تگ <persistence-unit> نیز تعریف کرد. این کاری است که ما با پروژه provider JPA / Toplink انجام خواهیم داد.
  • خطوط ۱۰ تا ۱۲ که در اینجا با کامنت غیرفعال شده‌اند، لاگ‌های کنسول Hibernate را پیکربندی می‌کنند:
  • خط ۱۰: برای فعال یا غیرفعال کردن نمایش دستورات SQL صادر شده توسط Hibernate بر روی SGBD. این امر در مرحله یادگیری بسیار مفید است. به دلیل پل رابطه‌ای/شیءگرا، برنامه روی اشیاء پایدار کار می‌کند و عملیات از نوع [persist, merge, remove] را روی آن‌ها اعمال می‌کند. دانستن اینکه کدام دستورات SQL در واقع برای این عملیات صادر می‌شوند، بسیار مفید است. با مطالعهٔ آن‌ها، شما به تدریج شروع به استخراج دستورات SQL می‌کنید که Hibernate هنگام انجام یک عملیات خاص روی اشیاء پایدار تولید خواهد کرد، و پل رابطه‌ای-به-شیء در ذهن شما شکل می‌گیرد.
  • خط ۱۱: دستورات SQL نمایش‌داده‌شده در کنسول را می‌توان به‌خوبی قالب‌بندی کرد تا خواندن آن‌ها آسان‌تر شود
  • خط ۱۲: دستورات نمایش‌داده‌شده SQL نیز حاشیه‌نویسی خواهند شد
  • خطوط ۱۵–۱۹ لایه JDBC را تعریف می‌کنند (لایه [6] در معماری):
  • خط ۱۵: کلاس درایور JDBC برای SGBD، در اینجا MySQL5
  • خط ۱۶: URL پایگاه داده مورد استفاده
  • خطوط ۱۷ و ۱۸: نام کاربری و رمز عبور اتصال
  • ما در اینجا از عناصری استفاده می‌کنیم که در ضمیمه‌ها، بخش 5.5 توضیح داده شده‌اند. از خوانندگان دعوت می‌شود به این بخش در MySQL5 مراجعه کنند.
  • خط ۲۲: Hibernate باید بداند که با کدام SGBD سر و کار دارد. این به این دلیل است که همه SGBDها دارای افزونه‌های اختصاصی SQL هستند، یک روش خاص برای مدیریت تولید خودکار مقادیر کلید اصلی، ... که به این معنی است که Hibernate باید SGBD مورد نظر را بشناسد تا بتواند دستورات SQL را که قابل فهم آن است برایش ارسال کند. [MySQL5InnoDBDialect] به SGBD و MySQL5 با جداول از نوع InnoDB که از تراکنش‌ها پشتیبانی می‌کنند، اشاره دارد.
  • خطوط ۲۴ تا ۲۸ استخر اتصال c3p0 (لایه [5] در معماری) را پیکربندی می‌کنند:
  • خطوط ۲۴ و ۲۵: حداقل (پیش‌فرض ۳) و حداکثر تعداد اتصالات (پیش‌فرض ۱۵) در استخر. تعداد اولیه پیش‌فرض اتصالات ۳ است.
  • خط ۲۶: حداکثر زمان انتظار، به میلی‌ثانیه، برای درخواست اتصال از سمت کلاینت. پس از گذشت این زمان، c3p0 یک استثنا برمی‌گرداند.
  • خط ۲۷: برای دسترسی به BD، Hibernate از دستورات آماده SQL (PreparedStatement) استفاده می‌کند که c3p0 می‌تواند آن‌ها را در حافظه پنهان ذخیره کند. این بدان معناست که اگر برنامه برای بار دوم یک دستور SQL آماده شده را که از قبل در کش موجود است درخواست کند، نیازی به آماده‌سازی مجدد آن نخواهد بود (آماده‌سازی یک دستور SQL هزینه در بر دارد) و دستور موجود در کش استفاده خواهد شد. در اینجا، حداکثر تعداد سفارش‌های آماده‌شده SQL که کش می‌تواند در تمام اتصالات نگه دارد، مشخص می‌شود (یک سفارش آماده‌شده SQL متعلق به یک اتصال واحد است).
  • خط ۲۸: فرکانس، به میلی‌ثانیه، که در آن اتصالات از نظر اعتبار بررسی می‌شوند. یک اتصال در استخر ممکن است به دلایل مختلف نامعتبر شود (درایور JDBC اتصال را به دلیل فعال بودن برای مدت طولانی نامعتبر می‌کند، درایور JDBC دارای «باگ» است، و غیره).
  • خط ۲۰: این مشخص می‌کند که هنگام راه‌اندازی واحد پایداری، طرحواره پایگاه داده برای اشیاء @Entity باید ایجاد شود. Hibernate اکنون تمام ابزارهای لازم برای صدور دستورات مورد نیاز جهت ایجاد جداول پایگاه داده را در اختیار دارد:
  • پیکربندی اشیاء @Entity به آن امکان می‌دهد تا جدول‌های مورد نظر را شناسایی کند
  • خطوط ۱۵–۱۸ و ۲۴–۲۸ به آن اجازه می‌دهند تا یک اتصال با SGBD برقرار کند
  • خط ۲۲ به آن می‌گوید که از کدام گویش SQL برای تولید جداول استفاده کند

بنابراین، فایل [persistence.xml] مورد استفاده در اینجا، هر بار که برنامه اجرا می‌شود یک پایگاه داده جدید ایجاد می‌کند. جداول پس از حذف شدن (drop table)، در صورتی که قبلاً وجود داشته باشند، دوباره ایجاد می‌شوند (create table). باید توجه داشت که این کار به وضوح نباید روی یک پایگاه داده تولیدی انجام شود...

آزمایش‌ها نشان داده‌اند که فاز «حذف»/«ایجاد» برای جداول می‌تواند با شکست مواجه شود. این امر به‌ویژه زمانی رخ می‌داد که در همان آزمایش، از لایه JPA/Hibernate به لایه JPA/TopLink یا بالعکس سوئیچ کردیم. با استفاده از اشیاء یکسان @Entity، این دو پیاده‌سازی دقیقاً همان جداول، ژنراتورها، توالی‌ها و غیره را ایجاد نمی‌کنند و گاهی اوقات فاز حذف/ایجاد با شکست مواجه شده و ما را مجبور به حذف دستی جداول کرده است. بخش «پیوست‌ها»، از پاراگراف ۵ به بعد، ابزارهایی را که می‌توان برای انجام دستی این کار استفاده کرد، توصیف می‌کند. شایان ذکر است که پیاده‌سازی JPA/Hibernate در این مرحله اولیه ایجاد محتوای پایگاه داده، کارآمدترین بود: خرابی‌ها نادر بودند.

ابزارهای مورد استفاده توسط لایه JPA / Hibernate در کتابخانه [jpa-hibernate]، که در بند 1.5 در صفحه 8 توضیح داده شده است، گنجانده شده‌اند. درایورهای مورد نیاز JDBC برای دسترسی به فایل‌های SGBD در کتابخانه [jpa-divers] قرار دارند. هر دو این کتابخانه‌ها در classpath پروژه مورد بررسی اینجا گنجانده شده‌اند. محتویات آن‌ها در زیر خلاصه شده است:

2.1.6. ایجاد پایگاه داده با استفاده از یک اسکریپت Ant

همان‌طور که دیدیم، Hibernate ابزارهایی را برای تولید طرحواره پایگاه داده برای اشیاء @Entity برنامه فراهم می‌کند. Hibernate می‌تواند:

  • فایل متنی حاوی دستورات (SQL) برای ایجاد پایگاه داده را تولید کند. سپس تنها گویش مشخص‌شده در [persistence.xml] استفاده می‌شود.
  • ایجاد جداول متناظر با اشیاء @Entity در پایگاه داده هدف تعریف‌شده در [persistence.xml]. در این مورد، کل فایل [persistence.xml] استفاده می‌شود.

ما یک اسکریپت Ant را ارائه خواهیم داد که قادر به تولید طرحواره پایگاه داده و طرحواره‌های شیء برای اشیاء @Entity است. این اسکریپت از آن من نیست: بر اساس اسکریپتی مشابه از [ref1] ساخته شده است. Ant (ابزار جالب دیگر) یک ابزار پردازش دسته‌ای جاوا است. اسکریپت‌های Ant برای یک تازه‌کار آسان برای فهمیدن نیستند. ما فقط از یکی از آن‌ها استفاده خواهیم کرد، همان‌که اکنون در حال توضیح آن هستیم:

  • در [1]: ساختار دایرکتوری مثال‌های این آموزش.
  • در [2]: پوشه [personnes-entites] پروژه Eclipse که در حال حاضر مورد مطالعه است
  • به [3]: پوشه <lib> حاوی پنج کتابخانه JAR تعریف‌شده در بخش 1.5.
  • [4]: آرشیو [hibernate-tools.jar] مورد نیاز برای یکی از وظایف در اسکریپت [ant-hibernate.xml] که قصد داریم بررسی کنیم.
  • در [5]: پروژه Eclipse و اسکریپت [ant-hibernate.xml]
  • در [6]: پوشه [src] پروژه

اسکریپت [ant-hibernate.xml] [5] از فایل‌های JAR موجود در پوشه <lib> [3] استفاده خواهد کرد، به‌ویژه آرشیوهای [hibernate-tools.jar] و [4] از پوشه [lib/hibernate]. ما ساختار پوشه را بازتولید کرده‌ایم تا خواننده بتواند ببیند که برای یافتن پوشه [lib] از پوشه‌های [personnes-entites] و [2] در داخل اسکریپت [ant-hibernate.xml]، باید مسیر زیر را دنبال کرد: ../../../lib.

بیایید نگاهی به اسکریپت [ant-hibernate.xml] بیندازیم:


<project name="jpa-hibernate" default="compile" basedir=".">

    <!-- نام و نسخه پروژه -->
    <property name="proj.name" value="jpa-hibernate" />
    <property name="proj.shortname" value="jpa-hibernate" />
    <property name="version" value="1.0" />

    <!-- ویژگی‌های جهانی -->
    <property name="src.java.dir" value="src" />
    <property name="lib.dir" value="../../../lib" />
    <property name="build.dir" value="bin" />

    <!-- مسیر کلاس پروژه -->
    <path id="project.classpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <!-- فایل‌های پیکربندی که باید در مسیر کلاس باشند-->
    <patternset id="conf">
        <include name="**/*.xml" />
        <include name="**/*.properties" />
    </patternset>

    <!--پاکسازی پروژه -->
    <target name="clean" description="Nettoyer le projet">
        <delete dir="${build.dir}" />
        <mkdir dir="${build.dir}" />
    </target>

    <!-- کامپایل پروژه -->
<target name="compile" depends="clean">
        <javac srcdir="${src.java.dir}" destdir="${build.dir}" classpathref="project.classpath" />
    </target>

    <!-- کپی فایل‌های پیکربندی به مسیر کلاس -->
    <target name="copyconf">
        <mkdir dir="${build.dir}" />
        <copy todir="${build.dir}">
            <fileset dir="${src.java.dir}">
                <patternset refid="conf" />
            </fileset>
        </copy>
    </target>

    <!--ابزارهای Hibernate -->
    <taskdef name="hibernatetool" classname="org.hibernate.tool.ant.HibernateToolTask" classpathref="project.classpath" />

    <!--تولید پایگاه داده DDL -->
    <target name="DDL" depends="compile, copyconf" description="Génération DDL base">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- از META-INF/persistence.xml استفاده کنید -->
            <jpaconfiguration />
            <!-- صادرات -->
            <hbm2ddl drop="true" create="true" export="false" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>

    <!-- ایجاد پایگاه داده -->
    <target name="BD" depends="compile, copyconf" description="Génération BD">

        <hibernatetool destdir="${basedir}">
            <classpath path="${build.dir}" />
            <!-- از META-INF/persistence.xml استفاده کنید -->
            <jpaconfiguration />
            <!-- صادرات -->
            <hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
        </hibernatetool>
    </target>
</project>
  • خط ۱: پروژه [ant] با نام «jpa-hibernate» نامگذاری شده است. این پروژه شامل مجموعه‌ای از وظایف است که یکی از آن‌ها وظیفهٔ پیش‌فرض است: در این مورد، وظیفه‌ای به نام «compile». یک اسکریپت ant برای اجرای یک وظیفه T فراخوانی می‌شود. اگر هیچ وظیفه‌ای مشخص نشود، وظیفه پیش‌فرض اجرا می‌شود. `basedir="."` نشان می‌دهد که برای تمام مسیرهای نسبی یافت‌شده در اسکریپت، نقطه شروع، پوشه‌ای است که اسکریپت `ant` را در خود دارد؛ در این مورد، پوشه `<examples>/hibernate/direct/people-entities` است.
  • خطوط ۳–۱۱: متغیرهای اسکریپت را با استفاده از تگ <property name="nomVariable" value="valeurVariable"/> تعریف می‌کنند. سپس می‌توان از این متغیر در اسکریپت با استفاده از notation ${nomVariable} استفاده کرد. نام‌ها می‌توانند هر چیزی باشند. بیایید نگاهی دقیق‌تر به متغیرهای تعریف‌شده در خطوط ۹–۱۱ بیندازیم:
    • خط ۹: متغیری به نام «src.java.dir» را تعریف می‌کند (این نام دلخواه است) که بعداً در اسکریپت به پوشه‌ای که حاوی کد منبع جاوا است اشاره خواهد کرد. مقدار آن «src» است، یک مسیر نسبی به پوشه‌ای که توسط ویژگی basedir (خط ۱) مشخص شده است. بنابراین این مسیر «./src» است، که در آن «.» به پوشه <examples>/hibernate/direct/people-entities اشاره می‌کند. در واقع، کد منبع جاوا در پوشه <people-entities>/src قرار دارد (به [6] در بالا مراجعه کنید).
    • خط ۱۰: متغیری به نام "lib.dir" تعریف می‌کند که بعداً در اسکریپت به پوشه‌ای که فایل‌های JAR مورد نیاز وظایف جاوا اسکریپت را در خود دارد، اشاره خواهد کرد. مقدار آن ../../../lib به پوشه <exemples>/lib اشاره دارد (به [3] در بالا مراجعه کنید).
    • خط ۱۰: یک متغیر به نام "build.dir" را تعریف می‌کند که، در ادامه اسکریپت، به پوشه‌ای اشاره خواهد کرد که فایل‌های .class تولید شده از کامپایل فایل‌های منبع .java در آن قرار می‌گیرند. مقدار آن "bin" است که به پوشه <personnes-entites>/bin اشاره دارد. ما قبلاً توضیح داده‌ایم که در پروژه اکلیپسی که بررسی کردیم، پوشه <bin> جایی بود که فایل‌های .class در آن تولید می‌شدند. Ant نیز همین کار را انجام خواهد داد.
    • خطوط ۱۴–۱۸: تگ <path> برای تعریف عناصری از classpath که وظایف ant به آنها نیاز خواهند داشت، استفاده می‌شود. در اینجا، مسیر «project.classpath» (که نام آن می‌تواند هر چیزی باشد) تمام فایل‌های .jar را در ساختار پوشه <examples>/lib با هم گروه‌بندی می‌کند.
    • خطوط 21–24: تگ <patternset> برای مشخص کردن مجموعه‌ای از فایل‌ها با استفاده از الگوهای نام‌گذاری به کار می‌رود. در اینجا الگوی با نام patternset به تمام فایل‌هایی با پسوند .xml یا .properties اشاره می‌کند. این patternset برای اشاره به فایل‌های .xml و .properties در پوشه <src> استفاده خواهد شد (persistence.xml, log4j.properties) (به [6] مراجعه کنید)، که فایل‌های پیکربندی برنامه هستند. هنگامی که برخی وظایف اجرا می‌شوند، این فایل‌ها باید به پوشه <bin> کپی شوند تا در classpath پروژه گنجانده شوند. سپس برای اشاره به آن‌ها از conf patternset استفاده خواهیم کرد.
    • خطوط ۲۷–۳۰: تگ <target> یک وظیفه را در اسکریپت مشخص می‌کند. این اولین موردی است که با آن مواجه می‌شویم. هر چیزی که قبل از آن آمده است به پیکربندی محیط اجرای اسکریپت ant مربوط می‌شود. این وظیفه «clean» نام دارد. این وظیفه در دو مرحله اجرا می‌شود: پوشه <bin> حذف می‌شود (خط ۲۸) و سپس دوباره ایجاد می‌شود (خط ۲۹).
    • خطوط ۳۳–۳۵: وظیفه «compile» که وظیفه پیش‌فرض اسکریپت است (خط ۱). این وظیفه از طریق ویژگی «depends» به وظیفه «clean» وابسته است. این بدان معناست که قبل از اجرای وظیفه «compile»، ant باید ابتدا وظیفه «clean» را، c.a.d، برای پاک کردن پوشه <bin> اجرا کند. هدف وظیفه «compile» در اینجا، کامپایل کردن کد منبع جاوا در پوشه <src> است.
    • خط ۳۴: کامپایلر جاوا را با سه پارامتر فراخوانی می‌کند:
      • srcdir: پوشه‌ای که حاوی کد منبع جاوا است، در این مورد پوشه <src>
      • destdir: پوشه‌ای که فایل‌های تولیدشده .class در آن ذخیره می‌شوند، در این مورد پوشه <bin>
      • classpathref: مسیر کلاس (classpath) مورد استفاده برای کامپایل؛ در اینجا، تمام فایل‌های JAR در ساختار پوشه <lib>
  • (ادامه)
    • خطوط ۳۸–۴۵: وظیفه `copyconf` که هدف آن کپی کردن تمام فایل‌های .xml و .properties از دایرکتوری <src> به دایرکتوری <bin> است.
    • خط ۴۸: تعریف یک وظیفه با استفاده از تگ <taskdef>. چنین وظیفه‌ای برای استفاده مجدد در بخش‌های دیگر اسکریپت در نظر گرفته شده است. این یک سهولت کدنویسی است. از آنجا که این وظیفه در نقاط مختلف اسکریپت استفاده می‌شود، یک‌بار با تگ <taskdef> تعریف شده و سپس هر زمان که نیاز باشد با نامش مجدداً فراخوانی می‌شود.
      • این وظیفه با نام hibernatetool (ویژگی name) فراخوانی می‌شود.
      • کلاس آن توسط ویژگی classname تعریف می‌شود. در اینجا، کلاس مشخص‌شده در آرشیو [hibernate-tools.jar] که قبلاً در مورد آن بحث کرده‌ایم، یافت خواهد شد.
      • ویژگی classpathref به ant می‌گوید که کجا به دنبال کلاس مذکور بگردد.
  • (ادامه)
    • خطوط ۵۱–۶۰ مربوط به کاری است که در اینجا مورد توجه ماست: تولید طرحواره پایگاه داده برای اشیاء @Entity در پروژه Eclipse ما.
      • خط ۵۱: این وظیفه DDL نامیده می‌شود (همان‌طور که در زبان تعریف داده؛ SQL با ایجاد اشیاء پایگاه داده مرتبط است). این وظیفه به وظایف «compile» و «copyconf» به ترتیب وابستگی دارد. بنابراین، وظیفه DDL به ترتیب، اجرای وظایف «clean»، «compile» و «copyconf» را آغاز می‌کند. هنگامی که وظیفه DDL شروع می‌شود، پوشه <bin> حاوی فایل‌های .class تولید شده از فایل‌های منبع .java، به‌ویژه اشیاء @Entity، و همچنین فایل [META-INF/persistence.xml] است که لایه JPA / Hibernate را پیکربندی می‌کند.
      • خطوط ۵۳–۵۹: وظیفه [hibernatetool] که در خط ۴۸ تعریف شده است، فراخوانی می‌شود. علاوه بر پارامترهای تعریف‌شده در خط ۴۸، پارامترهای متعددی به آن ارسال می‌شود:
      • خط ۵۳: پوشه خروجی برای نتایج تولیدشده توسط وظیفه، پوشه جاری خواهد بود.
      • خط ۵۴: پوشهٔ classpath وظیفه، پوشهٔ <bin> خواهد بود
      • خط ۵۶: به وظیفه [hibernatetool] می‌گوید که چگونه می‌تواند محیط اجرای خود را تعیین کند: تگ <jpaconfiguration/> به آن می‌گوید که در محیط JPA قرار دارد و بنابراین باید از فایل [META-INF/persistence.xml] استفاده کند، فایلی که در اینجا در classpath خود آن را پیدا خواهد کرد.
      • خط ۵۸ شرایط تولید پایگاه داده را تعیین می‌کند: `drop=true` نشان می‌دهد که دستورات `DROP TABLE` مربوط به `SQL` باید قبل از ایجاد جداول اجرا شوند؛ `create=true` نشان می‌دهد که فایل متنی حاوی دستورات ایجاد پایگاه داده `SQL` باید ایجاد شود؛ `outputfilename` نام این فایل `SQL` را مشخص می‌کند — در این مورد، schema.sql — در پوشه <ddl> پروژه Eclipse؛ `export=false` نشان می‌دهد که دستورات تولید شده SQL نباید در یک اتصال به پایگاه داده SGBD اجرا شوند. این مهم است: این بدان معناست که برای اجرای وظیفه، نیازی به راه‌اندازی هدف SGBD نیست. delimiter کاراکتری را که دو دستور SQL را در طرحواره تولیدشده جدا می‌کند، تعیین می‌کند، در حالی که format=true به سیستم دستور می‌دهد تا قالب‌بندی پایه‌ای را روی متن تولیدشده اعمال کند.
  • (ادامه)
    • خطوط ۶۳–۷۲ وظیفه‌ای به نام BD را تعریف می‌کنند. این وظیفه با وظیفه قبلی، DDL، یکسان است، با این تفاوت که این بار پایگاه داده را ایجاد می‌کند (export="true" در خط ۷۰). این وظیفه با استفاده از اطلاعات موجود در [persistence.xml]، یک اتصال به SGBD برقرار می‌کند تا اسکیمای SQL را اجرا کرده و پایگاه داده را تولید کند. برای اجرای وظیفه BD، وظیفه SGBD باید قبلاً اجرا شده باشد.

2.1.7. اجرای وظیفه « » قبل از «DDL»

برای اجرای اسکریپت [ant-hibernate.xml]، ابتدا باید چند پیکربندی را در Eclipse انجام دهیم.

  • در [1]: [External Tools] را انتخاب کنید
  • در [2]: یک پیکربندی جدید ant ایجاد کنید
  • به [3]: پیکربندی را ant نامگذاری کنید
  • در [5]: با استفاده از دکمه [4]، اسکریپت ant را انتخاب کنید
  • در [6]: تغییرات را اعمال کنید
  • به [7]: پیکربندی ant DDL ایجاد شد
  • در [8]: در برگه JRE، شما JRE مورد استفاده را تعریف می‌کنید. میدان [10] معمولاً از قبل با JRE مورد استفاده در Eclipse پر شده است. بنابراین، معمولاً در این پنل کاری برای انجام دادن وجود ندارد. با این حال، من با موردی مواجه شده‌ام که اسکریپت ant نتوانست کامپایلر <javac> را پیدا کند. این کامپایلر در یک JRE (محیط اجرایی جاوای (Java Runtime Environment)) قرار ندارد، بلکه در یک JDK (کیت توسعه جاوای (Java Development Kit)) قرار دارد. ابزار Eclipse ant این کامپایلر را از طریق متغیر محیطی JAVA_HOME (شروع / کنترل پنل / کارایی و نگهداری / سیستم / برگهٔ پیشرفته / دکمهٔ متغیرهای محیطی) [A]. اگر این متغیر تنظیم نشده باشد، می‌توانید با تنظیم [10] به JDK به جای JRE، به ant اجازه دهید کامپایلر <javac> را پیدا کند. این در همان پوشه‌ای که JRE و [B] در آن قرار دارند، موجود است. ما از دکمه [9] برای اعلام JDK در میان JRE و [C] موجود استفاده خواهیم کرد تا بتوانیم سپس آن را در [10] انتخاب کنیم..
  • در [12]: در زبانه [Targets]، وظیفه DDL را انتخاب کنید. بدین ترتیب، پیکربندی ant – که آن را DDL و [7] نامیده‌ایم – با اجرای وظیفه‌ای به نام DDL و [12]، که، همان‌طور که می‌دانیم، این اسکیما DDL را برای طرحواره پایگاه داده اشیاء @Entity برنامه تولید می‌کند.
  • به [13]: پیکربندی اعتبارسنجی می‌شود
  • به [14]: آن را اجرا می‌کنیم

ویوی [console] گزارش‌های اجرای وظیفه ant DDL را نمایش می‌دهد:


Buildfile: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\ant-hibernate.xml
clean:
   [delete] Deleting directory C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
    [mkdir] Created dir: C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
compile:
    [javac] Compiling 3 source files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
copyconf:
     [copy] Copying 2 files to C:\data\2006-2007\eclipse\dvp-jpa\hibernate\direct\personnes-entites\bin
DDL:
[hibernatetool] Executing Hibernate Tool with a JPA Configuration
[hibernatetool] 1. task: hbm2ddl (Generates database schema)
[hibernatetool] drop table if exists jpa01_personne;
[hibernatetool] create table jpa01_personne (
[hibernatetool] ID integer not null auto_increment,
[hibernatetool] VERSION integer not null,
[hibernatetool] NOM varchar(30) not null unique,
[hibernatetool] PRENOM varchar(30) not null,
[hibernatetool] DATENAISSANCE date not null,
[hibernatetool] MARIE bit not null,
[hibernatetool] NBENFANTS integer not null,
[hibernatetool] primary key (ID)
[hibernatetool] ) ENGINE=InnoDB;
BUILD SUCCESSFUL
Total time: 5 seconds
  • توجه داشته باشید که وظیفه DDL با نام [hibernatetool] (خط ۱۰) نامگذاری شده است و به وظایف clean (خط ۲)، وابسته است، compile (خط ۵) و copyconf (خط ۷).
  • خط ۱۰: وظیفه [hibernatetool] فایل [persistence.xml] را از یک پیکربندی JPA پردازش می‌کند
  • خط ۱۱: وظیفه [hbm2ddl] طرحواره DDL را برای پایگاه داده تولید خواهد کرد
  • خطوط ۱۲–۲۲: شِمای DDL از پایگاه داده

به یاد داشته باشید که ما به وظیفه [hbm2ddl] دستور داده بودیم تا طرحواره DDL را در یک مکان مشخص تولید کند:


<hbm2ddl drop="true" create="true" export="true" outputfilename="ddl/schema.sql" delimiter=";" format="true" />
  • خط ۷۴: اسکیما باید در فایل ddl/schema.sql تولید شود. بیایید بررسی کنیم:
  • در [1]: فایل ddl/schema.sql واقعاً موجود است (برای تازه کردن درخت دایرکتوری، F5 را اجرا کنید)
  • به [2]: محتویات آن. این شِما برای یک پایگاه داده به نام MySQL5 است. فایل پیکربندی [persistence.xml] برای لایه JPA در واقع یک SGBD MySQL5 را مشخص کرده است (خط ۸ زیر):


            <!-- ورود JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
...
            <!--  ایجاد خودکار طرحواره -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!--  گویش -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQL5InnoDBDialect" />
            <!--  properties DataSource c3p0 -->
...

بیایید با بررسی پیکربندی شیء @Entity Person و اسکیمای تولیدشده DDL، نگاشت شیء-رابطه‌ای انجام‌شده در اینجا را بررسی کنیم:

چند نکته شایان ذکر است:

  • A1-B1: نام جدول مشخص‌شده در A1 دقیقاً همان نامی است که در B1 استفاده شده است. توجه داشته باشید که drop در B1 مقدم بر create است.
  • A2-B2: روش مورد استفاده برای تولید کلید اصلی را نشان می‌دهد. حالت AUTO که در A2 مشخص شده است، منجر به ویژگی autoincrement مختص MySQL5 شد. حالت تولید کلید اصلی اغلب مختص SGBD است.
  • A3-B3: نوع بیت SQL را که مختص MySQL5 است نشان می‌دهد تا یک نوع جاوا boolean را نمایش دهد.

بیایید این آزمایش را با یک SGBD دیگر تکرار کنیم:

  • پوشه [conf] [1] حاوی فایل‌های [persistence.xml] برای فایل‌های مختلف SGBD است. برای مثال، فایل Oracle با نام [2] را بردارید و آن را به جای فایل قبلی در پوشه [META-INF] [3] قرار دهید. محتویات آن به شرح زیر است:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  ارائه‌دهنده -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!-- کلاس‌های پایدار -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- لاگ‌ها SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- اتصال JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  ایجاد خودکار طرحواره -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!--  گویش -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!--  ویژگی‌ها DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

از خوانندگان دعوت می‌شود تا به بخش مربوط به Oracle در ضمیمه‌ها (بند 5.7) مراجعه کنند، به‌ویژه برای درک پیکربندی JDBC.

تنها خط ۲۵ در اینجا واقعاً مهم است: به Hibernate گفته می‌شود که SGBD اکنون یک Oracle SGBD است. اجرای وظیفه Ant با شناسه DDL، نتیجه [4] را که در بالا نشان داده شده است، تولید می‌کند. توجه داشته باشید که اسکیمای Oracle با اسکیمای MySQL5 متفاوت است. این یکی از نقاط قوت کلیدی JPA است: توسعه‌دهنده نیازی ندارد که به این جزئیات بپردازد، که این امر قابلیت حمل (portability) توسعه او را به طور قابل توجهی افزایش می‌دهد.

2.1.8. اجرای وظیفه و BD

شاید به یاد داشته باشید که وظیفه ant، با نام BD، همان کاری را انجام می‌دهد که وظیفه ant DDL انجام می‌دهد، اما همچنین پایگاه داده را نیز ایجاد می‌کند. بنابراین باید وظیفه SGBD اجرا شود. اکنون مورد SGBD و MySQL5 را بررسی می‌کنیم و از خواننده دعوت می‌کنیم فایل [conf/mysql5/persistence.xml] را در پوشه [src/META-INF] کپی کند. برای بررسی اینکه وظیفه به درستی کار می‌کند، ما از افزونه Explorer SQL (به بخش 5.2.6 مراجعه کنید) برای بررسی وضعیت JPA BD قبل و بعد از اجرای وظایف ant و BD استفاده خواهیم کرد.

ابتدا باید پیکربندی جدیدی به نام ant ایجاد کنیم تا وظیفه BD را اجرا کنیم. به خواننده توصیه می‌شود رویهٔ تشریح‌شده برای پیکربندی ant DDL را در بخش 2.1.7 دنبال کند. پیکربندی جدید ant با نام BD نام‌گذاری خواهد شد:

  • به [1]: پیکربندی قبلی با نام DDL را کپی کنید
  • به [2]: پیکربندی جدید BD نام‌گذاری می‌شود. این وظیفه ant BD [3] را اجرا می‌کند که به‌صورت فیزیکی پایگاه داده را ایجاد می‌کند.
  • پس از انجام این کار، SGBD و MySQL5 را اجرا کنید (بخش 5.5).

اکنون از افزونه SQL Explorer برای بررسی پایگاه‌های داده‌ای که توسط SGBD مدیریت می‌شوند، استفاده می‌کنیم. خواننده در صورت لزوم باید پیش از این با این افزونه آشنا شود (بخش 5.2.6 را ببینید).

  • [1]: نمای SQL Explorer را باز کنید [Window / Open Perspective / Other]
  • [2]: در صورت لزوم، یک اتصال [mysql5-jpa] ایجاد کنید (به بخش 5.5.5، صفحه 252 مراجعه کنید) و آن را باز کنید
  • [3]: با نام کاربری jpa / jpa وارد شوید
  • [4]: اکنون به MySQL5 متصل شده‌اید.
  • در [5]: jpa در BD تنها یک جدول دارد: [articles]
  • در [6]: ما اجرای وظیفه ant BD را آغاز می‌کنیم. از آنجا که در چشم‌انداز [SQL Explorer] هستیم، نمی‌توانیم نمای [Console] را که گزارش‌های وظیفه را نمایش می‌دهد، ببینیم. می‌توانیم این نما [Window / Show View / ...] را نمایش دهیم یا به چشم‌انداز جاوا [Window / Open Perspective / ...] بازگردیم.
  • در [7]: پس از اتمام وظیفه Ant با شناسه BD، ممکن است بخواهید به چشم‌انداز [SQL Explorer] بازگردید و درخت JPA با شناسه BD را تازه کنید.
  • در [8]: جدول [jpa01_personne] که ایجاد شده است، قابل مشاهده است.

از خواننده دعوت می‌شود تا این تولید BD را با استفاده از سایر فایل‌های SGBD تکرار کند. رویه‌ای که باید دنبال شود به شرح زیر است:

  • فایل [conf/<sgbd>/persistence.xml] را در پوشه [src/META-INF] کپی کنید، جایی که <sgbd> همان SGBD است که آزمایش شده است
  • با دنبال کردن دستورالعمل‌های ضمیمه‌های مربوطه، <sgbd> را اجرا کنید
  • در نمای SQL Explorer، یک اتصال به <sgbd> ایجاد کنید. این موضوع همچنین در ضمیمه‌های مربوط به هر یک از فایل‌های SGBD توضیح داده شده است
  • آزمایش‌های قبلی را تکرار کنید

در این مرحله، به بینش‌های متعددی دست یافته‌ایم:

  • درک بهتری از مفهوم پل شیء-رابطه‌ای داریم. در اینجا، این پل با استفاده از Hibernate پیاده‌سازی شده است. بعداً از Toplink استفاده خواهیم کرد.
  • ما می‌دانیم که این پل شیء-رابطه‌ای در دو مکان پیکربندی شده است:
  • در اشیاء @Entity، جایی که ما پیوندهای بین فیلدهای شیء و ستون‌های جدول را در BD مشخص می‌کنیم
  • در [META-INF/persistence.xml]، جایی که ما اطلاعات مربوط به دو مؤلفه پل شیء-رابطه‌ای را به پیاده‌سازی JPA ارائه می‌دهیم: اشیاء @Entity (شیء) و پایگاه داده (رابطه‌ای).
  • ما دو وظیفه Ant به نام‌های DDL و BD ایجاد کرده‌ایم که به ما امکان می‌دهند پایگاه داده را بر اساس پیکربندی قبلی، حتی پیش از نوشتن هرگونه کد جاوا، ایجاد کنیم.

اکنون که لایه JPA برنامه ما به درستی پیکربندی شده است، می‌توانیم با استفاده از کد جاوا به بررسی لایه‌های API و JPA بپردازیم.

2.1.9. : زمینه پایداری یک برنامه

بیایید نگاهی دقیق‌تر به محیط زمان اجرای یک کلاینت JPA بیندازیم:

ما می‌دانیم که لایه JPA [2] یک پل شیء-رابطه‌ای [3] / [4] ایجاد می‌کند. مجموعهٔ اشیایی که توسط لایهٔ JPA در چارچوب این پل شیء-رابطه‌ای مدیریت می‌شوند، «زمینهٔ پایداری» نامیده می‌شود. برای دسترسی به داده‌ها در زمینه پایداری، یک کلاینت JPA [1] باید از طریق لایه JPA [2] اقدام کند:

  1. می‌تواند یک شیء ایجاد کند و از لایه JPA بخواهد آن را پایدار سازد. سپس این شیء بخشی از زمینه پایداری می‌شود.
  2. می‌تواند از لایه [JPA] یک مرجع به یک شی دائمی موجود درخواست کند.
  3. می‌تواند یک شیء پایدار را که از لایه JPA به دست آورده است، تغییر دهد.
  4. می‌تواند از لایه JPA بخواهد یک شیء را از زمینه پایداری حذف کند.

لایه JPA رابطی به نام [EntityManager] را در اختیار کلاینت قرار می‌دهد که همان‌طور که از نامش پیداست، برای مدیریت اشیاء @Entity در زمینه پایداری استفاده می‌شود. متدهای اصلی این رابط در زیر فهرست شده‌اند:

void persist(Object entity)
افزودن entity به زمینه پایداری
void remove(Object entity)
حذف entity از زمینه پایداری
<T> T merge(T entity)
یک شیء entity را از کلاینت ادغام می‌کند، که توسط زمینه پایداری مدیریت نمی‌شود،
با شیء entity از زمینه پایداری که دارای کلید اولیه یکسان است.
نتیجهٔ بازگردانده‌شده، شیء entity از زمینهٔ پایداری است.
<T> T find(Class<T> entityClass,
 Object primaryKey)
یک شیء بازیابی‌شده از پایگاه داده را قرار می‌دهد
از طریق کلید اصلی خود. نوع شیء T امکان‌پذیر می‌سازد
لایه JPA برای تعیین اینکه کدام جدول را پرس‌وجو کند.
شیء پایدار ایجادشده به کلاینت بازگردانده می‌شود.
Query createQuery(String queryText)
یک شیء پرس‌وجو را از یک پرس‌وجوی JPQL ایجاد می‌کند
(زبان پرس‌وجوی پایداری جاوا). یک پرس‌وجوی JPQL مشابه است
به یک پرس‌وجوی SQL شباهت دارد، با این تفاوت که به جای جداول، اشیاء را پرس‌وجو می‌کند.
Query createNativeQuery(String queryText)
یک روش مشابه مورد قبلی، با این تفاوت که queryText است،
یک دستور SQL به جای JPQL.
Query createNamedQuery(String name)
این روش با createQuery یکسان است، به جز اینکه فرمان JPQL queryText دارد
به یک فایل پیکربندی منتقل شده و نامی به آن اختصاص داده شده است.
این نام است که به‌عنوان پارامتر متد عمل می‌کند.

یک شیء EntityManager چرخه عمر دارد که لزوماً با چرخه عمر برنامه یکسان نیست. این شیء یک شروع و یک پایان دارد. بنابراین، یک کلاینت JPA می‌تواند به طور متوالی با اشیاء EntityManager مختلف کار کند. زمینه پایداری مرتبط با یک EntityManager چرخه عمر یکسانی با خود شیء دارد. این دو از یکدیگر جدایی‌ناپذیرند. هنگامی که یک شیء EntityManager بسته می‌شود، زمینه پایداری آن در صورت لزوم با پایگاه داده همگام‌سازی شده و سپس از بین می‌رود. برای به‌دست آوردن یک زمینه پایداری جدید، باید یک EntityManager جدید ایجاد شود.

کلاینت JPA می‌تواند با استفاده از عبارت زیر یک EntityManager – و در نتیجه یک زمینه پایداری – ایجاد کند:


        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
  • javax.persistence.Persistence یک کلاس ایستا است که برای به‌دست‌آوردن یک فابریک برای اشیاء EntityManager استفاده می‌شود. این فابریک به یک واحد پایداری خاص متصل است. توجه داشته باشید که فایل پیکربندی [META-INF/persistence.xml] برای تعریف واحدهای پایداری استفاده می‌شود و این واحدها نامی دارند:

    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">

در مثال بالا، واحد پایداری jpa نامیده می‌شود. این واحد دارای پیکربندی خاص خود است، به‌ویژه SGBD که با آن کار می‌کند. دستور [Persistence.createEntityManagerFactory("jpa")] یک کارخانهٔ شیء از نوع EntityManagerFactory ایجاد می‌کند که قادر به ارائهٔ اشیاء EntityManager است و برای مدیریت زمینه‌های پایداری مرتبط با واحد پایداری با نام jpa در نظر گرفته شده است. یک شیء EntityManager، و در نتیجه یک زمینه پایداری، به شرح زیر از شیء EntityManagerFactory به دست می‌آید:

        EntityManager em = emf.createEntityManager();

روش‌های زیر از رابط [EntityManager] برای مدیریت چرخهٔ عمر زمینهٔ پایداری استفاده می‌شوند:

void close()
زمینه پایداری بسته می‌شود. باعث همگام‌سازی زمینه پایداری با پایگاه داده می‌شود:
  • اگر یک شیء در زمینه در پایگاه داده موجود نباشد، با استفاده از یک عملیات (SQL INSERT) درج می‌شود
  • اگر یک شیء در زمینه در پایگاه داده موجود باشد و از زمان خوانده شدن تغییر کرده باشد، عملیاتی (SQL یا UPDATE) برای پایدارسازی تغییر انجام می‌شود
  • اگر یک شیء زمینه پس از یک عملیات remove روی آن به عنوان «حذف شده» علامت‌گذاری شده باشد، یک عملیات SQL DELETE برای حذف آن از پایگاه داده انجام می‌شود.
void clear()
زمینه پایداری از تمام اشیاء خود تخلیه می‌شود اما بسته نمی‌شود.
void flush()
زمینه پایداری با پایگاه داده همگام‌سازی می‌شود، همان‌طور که برای close() توصیف شده است.

کلاینت JPA می‌تواند با استفاده از متد [EntityManager].flush که در بالا توضیح داده شد، همگام‌سازی زمینه پایداری با پایگاه داده را اجباری کند. همگام‌سازی می‌تواند صریح یا ضمنی باشد. در حالت اول، مشتری موظف است عملیات flush را هر زمان که بخواهد همگام‌سازی انجام دهد، فراخوانی کند؛ در غیر این صورت، این عملیات در زمان‌های مشخصی که بعداً تعیین خواهیم کرد، انجام می‌شوند. حالت همگام‌سازی توسط متدهای زیر از رابط [EntityManager] مدیریت می‌شود:

void setFlushMode(FlushModeType flushMode)
دو مقدار ممکن برای flushmode وجود دارد:
FlushModeType.AUTO (پیش‌فرض): همگام‌سازی قبل از هر پرس‌وجوی SELECT انجام شده روی پایگاه داده صورت می‌گیرد.
FlushModeType.COMMIT: همگام‌سازی تنها در پایان تراکنش‌ها در پایگاه داده انجام می‌شود.
FlushModeType getFlushMode()
حالت همگام‌سازی فعلی را تنظیم می‌کند

خلاصه اینکه، در حالت FlushModeType.AUTO که حالت پیش‌فرض است، زمینه پایداری در زمان‌های زیر با پایگاه داده همگام‌سازی می‌شود:

  1. پیش از هر تراکنش SELECT،
  2. در پایان یک تراکنش روی پایگاه داده
  3. پس از یک عملیات flush یا close روی زمینه پایداری

در حالت FlushModeType.COMMIT، روند کار مشابه است، با این تفاوت که عملیات ۱ انجام نمی‌شود. حالت عادی تعامل با لایه JPA، حالت تراکنشی است. کلاینت عملیات مختلفی را در داخل یک تراکنش روی زمینه پایداری انجام می‌دهد. در این حالت، نقاطی که زمینه پایداری با پایگاه داده همگام‌سازی می‌شود، موارد ۱ و ۲ بالا در حالت AUTO و فقط مورد ۲ در حالت COMMIT هستند.

در پایان، با فرمان API از رابط پرس‌وجو (Query interface) خاتمه می‌دهیم که به شما امکان می‌دهد فرمان‌های JPQL را به زمینه پایداری (persistence context) یا فرمان‌های SQL را مستقیماً به پایگاه داده صادر کنید تا داده‌ها را بازیابی کنید. رابط پرس‌وجو به شرح زیر است:

ما باید از روش‌های ۱ تا ۴ فوق استفاده کنیم:

  • ۱ – متد getResultList یک SELECT را اجرا می‌کند که چندین شیء را بازمی‌گرداند. این‌ها در یک شیء List دریافت خواهند شد. این شیء یک رابط است. این شیء یک شیء Iterator را فراهم می‌کند که امکان پیمایش عناصر لیست L را به شکل زیر فراهم می‌سازد:

        Iterator iterator = L.iterator();
        while (iterator.hasNext()) {
             // پردازش شیء iterator.next() که نماینده آیتم فعلی در لیست است
...
}

همچنین می‌توان به لیست L از طریق یک for دسترسی داشت:


        for (Object o : L) {
             // دسترسی به شیء o
}
  • ۲ – متد getSingleResult یک فرمان JPQL / SQL / SELECT را اجرا می‌کند که یک شیء واحد را بازمی‌گرداند.
  • ۳ - متد executeUpdate یک دستور به‌روزرسانی یا حذف SQL را اجرا می‌کند و تعداد ردیف‌های تحت تأثیر عملیات را بازمی‌گرداند.
  • ۴ – متد setParameter(String, Object) امکان اختصاص یک مقدار به یک پارامتر نام‌گذاری‌شده در یک دستور JPQL پیکربندی‌شده را فراهم می‌کند
  • ۵ – متد setParameter(int, Object) به پارامتر با نام آن ارجاع نمی‌دهد، بلکه با موقعیت آن در دستور JPQL.

2.1.10. یک مشتری اول: JPA

بیایید از دیدگاه جاوا به پروژه بازگردیم:

 

ما اکنون تقریباً همه چیز را در مورد این پروژه می‌دانیم، به جز محتویات پوشه [src/tests] که اکنون در حال بررسی آن هستیم. این پوشه حاوی دو برنامه آزمایشی برای لایه JPA است:

  • [InitDB.java] برنامه‌ای است که چند سطر را در جدول پایگاه داده [jpa01_personne] درج می‌کند. کد آن اولین عناصر لایه JPA را در اختیار ما قرار خواهد داد.
  • [Main.java] برنامه‌ای است که عملیات تعریف‌شده در CRUD را بر روی جدول [jpa01_personne] انجام می‌دهد. تحلیل کد آن به ما امکان می‌دهد تا مفاهیم اساسی زمینه پایداری (persistence context) و چرخه عمر اشیاء در آن زمینه را بررسی کنیم.

2.1.10.1. کد

کد برنامه [InitDB.java] به شرح زیر است:


package tests;

import java.text.ParseException;
import java.text.SimpleDateFormat;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.EntityTransaction;
import javax.persistence.Persistence;

import entites.Personne;

public class InitDB {
    //ثابت‌ها
    private final static String TABLE_NAME = "jpa01_personne";

    public static void main(String[] args) throws ParseException {
        // واحد پایداری
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        // یک EntityManagerFactory را از واحد پایداری بازیابی می‌کند
        EntityManager em = emf.createEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // حذف رکوردها از جدول افراد
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        // ایجاد دو شخص
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // پایداری افراد
        em.persist(p1);
        em.persist(p2);
        // نمایش افراد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // پایان تراکنش
        tx.commit();
        // پایان EntityManager
        em.close();
        // پایان EntityManagerFactory
        emf.close();
        // ثبت
        System.out.println("terminé ...");
    }
}

این کد باید با توجه به توضیحات ارائه شده در بخش 2.1.9 خوانده شود.

  • خط ۱۹: یک شیء emf با نام EntityManagerFactory برای واحد پایداری jpa (تعریف‌شده در persistence.xml) درخواست می‌شود. این عملیات معمولاً تنها یک بار در طول عمر یک برنامه انجام می‌شود.
  • خط ۲۱: یک شیء EMF با شناسه EntityManager برای مدیریت یک زمینه پایداری درخواست شده است.
  • خط ۲۳: یک شیء تراکنش برای مدیریت یک تراکنش درخواست شده است. باید توجه داشت که عملیات روی زمینه پایداری در داخل یک تراکنش انجام می‌شوند. خواهیم دید که این امر اجباری نیست، اما اگر این کار انجام نشود، ممکن است مشکلاتی پیش آید. اگر برنامه در یک کانتینر EJB3 در حال اجرا باشد، عملیات روی زمینه پایداری همیشه در داخل یک تراکنش انجام می‌شوند.
  • خط ۲۴: تراکنش آغاز می‌شود
  • خط ۲۶: یک دستور DELETE از نوع SQL را روی جدول «jpa01_personne» (nativeQuery) اجرا می‌کند. این کار برای پاک کردن تمام محتویات جدول و در نتیجه مشاهده بهتر نتیجه اجرای برنامه [InitDB] انجام می‌شود.
  • خطوط ۲۸–۲۹: دو شیء Personne p1 و p2 ایجاد می‌شوند. این‌ها اشیاء عادی هستند و در این مرحله هیچ ارتباطی با زمینه پایداری ندارند. در رابطه با زمینه پایداری، Hibernate این اشیاء را در حالت موقت توصیف می‌کند، در مقابل اشیاء پایدار که توسط زمینه پایداری مدیریت می‌شوند. ما در عوض از اصطلاح «ابجکت‌های غیرمستمر» (اصطلاحی غیرفرانسوی) برای اشاره به این که این اجزا هنوز توسط زمینه پایداری مدیریت نمی‌شوند، و «ابجکت‌های مستمر» برای آنهایی که توسط آن مدیریت می‌شوند، استفاده خواهیم کرد. ما با یک دسته سوم از اجزا مواجه خواهیم شد: اجزای جداشده، که اجزایی هستند که قبلاً مستمر بوده‌اند اما زمینه پایداری آنها بسته شده است. کلاینت ممکن است ارجاع‌هایی به چنین اشیایی داشته باشد، که این موضوع توضیح می‌دهد چرا آن‌ها لزوماً هنگام بسته‌شدن زمینه پایداری نابود نمی‌شوند. سپس گفته می‌شود که آن‌ها در وضعیت جداشده (detached) قرار دارند. عملیات [EntityManager].merge به آن‌ها اجازه می‌دهد تا به یک زمینه پایداری تازه‌ساخت متصل شوند.
  • خطوط ۳۱–۳۲: افراد p1 و p2 توسط عملیات [EntityManager].persist در زمینه پایداری گنجانده می‌شوند. آنها سپس به اشیاء پایدار تبدیل می‌شوند.
  • خطوط ۳۵–۳۷: یک دستور JPQL اجرا می‌شود: «select p from Person p order by p.nom asc». Personne نه خود جدول (که jpa01_personne نامیده می‌شود) بلکه شیء @Entity مرتبط با آن جدول است. این یک پرس‌وجوی JPQL (زبان پرس‌وجوی پایداری جاوا) در زمینهٔ پایداری است، نه یک پرس‌وجوی SQL در پایگاه داده. با این حال، به جز شیء Personne که جایگزین جدول jpa01_personne شده است، نحو یکسان است. یک حلقه for بر روی لیست (افراد) بازگردانده شده توسط select تکرار می‌کند تا هر عنصر را در کنسول نمایش دهد. هدف در اینجا تأیید این است که عناصری که در خطوط ۳۱–۳۲ در زمینه پایداری قرار داده شده‌اند، واقعاً در جدول موجود هستند. به‌طور شفاف، زمینه پایداری با پایگاه داده همگام‌سازی خواهد شد. در واقع، یک پرس‌وجوی select اجرا خواهد شد و ما گفته‌ایم که این یکی از مواردی است که همگام‌سازی رخ می‌دهد. بنابراین در همین نقطه است که در پس‌زمینه، JPA / هیبِرنِیت دو دستور SQL و insert را صادر می‌کند که دو نفر را در جدول jpa01_personne درج می‌کند. عملیات persist این کار را انجام نداده بود. این عملیات اشیاء را بدون تأثیر بر پایگاه داده به زمینه پایداری اضافه می‌کند. تغییرات واقعی در حین همگام‌سازی رخ می‌دهند، در این مورد درست قبل از عملیات select روی پایگاه داده.
  • خط ۳۹: تراکنشی که در خط ۲۴ شروع شده بود، commit می‌شود. یک همگام‌سازی دوباره انجام خواهد شد. در اینجا هیچ اتفاقی نمی‌افتد زیرا از آخرین همگام‌سازی، زمینه پایداری تغییر نکرده است.
  • خط ۴۱: زمینه پایداری بسته می‌شود.
  • خط ۴۳: فابریک EntityManager بسته می‌شود.

2.1.10.2. اجرای کد

  • راه‌اندازی SGBD MySQL5
  • در صورت لزوم conf/mysql5/persistence.xml را در META-INF/persistence.xml قرار دهید
  • برنامه [InitDB] را اجرا کنید

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

  • در [1]: خروجی کنسول در نمای جاوا. نتیجه مورد انتظار به دست آمده است.
  • در [2]: ما محتویات جدول [jpa01_personne] را با استفاده از نمای SQL Explorer بررسی می‌کنیم، همان‌طور که در بخش 2.1.8 توضیح داده شده است. دو نکته قابل توجه است:
    • کلید اصلی ID به‌طور خودکار ایجاد شده است
    • همین موضوع در مورد شماره نسخه نیز صدق می‌کند. می‌توانیم ببینیم که اولین نسخه شماره 0.. دارد.

در اینجا با اولین عناصر چارچوب JPA روبرو هستیم. ما با موفقیت داده‌ها را در یک جدول وارد کرده‌ایم. ما بر اساس این پایه، تست دوم را خواهیم نوشت، اما ابتدا بیایید درباره لاگ‌ها صحبت کنیم.

2.1.11. پیاده‌سازی لاگ‌های Hibernate

امکان دارد بفهمیم کدام دستورات SQL توسط لایه JPA / Hibernate به پایگاه داده صادر شده‌اند. دانستن این موضوع برای این است که ببینیم آیا لایه JPA به اندازه توسعه‌دهنده‌ای که خود دستورات SQL را نوشته است، کارآمد است یا خیر.

با JPA / Hibernate، می‌توان لاگ‌های SQL را در فایل [persistence.xml] بررسی کرد:


            <!-- کلاس‌های پایدار -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- گزارش‌ها SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!--ورود JDBC -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />

  • خطوط ۴–۶: لاگ‌های SQL در این مرحله فعال نبودند. اکنون با حذف تگ‌های کامنت از خطوط ۳ و ۷، فعال شده‌اند.

اپلیکیشن [InitDB] را مجدداً اجرا کنید. خروجی کنسول در این صورت به شرح زیر خواهد بود:

Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
[personnes]
Hibernate: 
    select
        personne0_.ID as ID0_,
        personne0_.VERSION as VERSION0_,
        personne0_.NOM as NOM0_,
        personne0_.PRENOM as PRENOM0_,
        personne0_.DATENAISSANCE as DATENAIS5_0_,
        personne0_.MARIE as MARIE0_,
        personne0_.NBENFANTS as NBENFANTS0_ 
    from
        jpa01_personne personne0_ 
    order by
        personne0_.NOM asc
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
terminé ...
  • خطوط ۲–۴: دستور SQL delete که از دستورالعمل زیر حاصل می‌شود:

        // حذف رکوردها از جدول افراد
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
  • خطوط ۵–۱۸: دستورات SQL insert که از دستورالعمل‌ها حاصل می‌شوند:

        // پایداری افراد
        em.persist(p1);
        em.persist(p2);
  • خطوط ۲۱–۳۲: عبارت SELECT SQL از دستور:

        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) 

اگر خروجی‌های کنسول میانی را اجرا کنیم، خواهیم دید که لاگ‌های SQL برای دستور کد جاوا I زمانی نوشته می‌شوند که دستور I اجرا شود. این بدان معنا نیست که دستور SQL نمایش‌داده‌شده در آن لحظه روی پایگاه داده اجرا می‌شود. در واقع این دستور برای اجرا در هماهنگ‌سازی بعدی زمینه پایداری با پایگاه داده، در حافظه پنهان قرار می‌گیرد.

لاگ‌های بیشتری را می‌توان از طریق فایل [src/log4j.properties] به دست آورد:

  • در [1]، فایل [log4j.properties] توسط آرشیو [log4j-1.2.13.jar] [2] از ابزاری به نام LOG4j پردازش می‌شود. (لاگ‌های جاوا)، در آدرس [http://logging.apache.org/log4j/docs/index.html] در دسترس است. مطابق پوشه [src] پروژه Eclipse، می‌دانیم که [log4j.properties] به طور خودکار به پوشه [bin] پروژه [3] کپی خواهد شد. پس از انجام این کار، اکنون در پوشه classpath پروژه قرار دارد و آرشیو [2] آن را از آنجا بازیابی خواهد کرد.

فایل [log4j.properties] به ما امکان می‌دهد تا برخی از لاگ‌های Hibernate را نظارت کنیم. در اجراهای قبلی، محتویات آن به شرح زیر بود:


# ارسال مستقیم پیام‌های لاگ به stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target=System.out
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n

# گزینهٔ لاگ‌گیر ریشه
log4j.rootLogger=ERROR, stdout

# گزینه‌های گزارش‌گیری Hibernate (INFO فقط پیام‌های راه‌اندازی را نمایش می‌دهد)
#log4j.logger.org.hibernate=INFO

# Log JDBC bind parameter runtime arguments
#log4j.logger.org.hibernate.type=DEBUG

من زیاد در مورد این پیکربندی توضیح نمی‌دهم، چون هرگز وقت نکرده‌ام تا به درستی با LOG4j آشنا شوم.

  • خطوط ۱ تا ۸ در تمام فایل‌های log4j.properties که با آن‌ها مواجه شده‌ام، ظاهر می‌شوند
  • خطوط ۱۰ تا ۱۴ در فایل‌های log4j.properties از مثال‌های Hibernate وجود دارند.
  • خط ۱۱: کنترل لاگ‌های عمومی Hibernate را بر عهده دارد. از آنجا که این خط کامنت شده است، این لاگ‌ها در اینجا غیرفعال هستند. چندین سطح لاگ‌گیری در دسترس است: INFO (اطلاعات عمومی دربارهٔ کاری که Hibernate انجام می‌دهد)، WARN (Hibernate ما را از یک مشکل احتمالی آگاه می‌کند)، DEBUG (لاگ‌های تفصیلی). سطح INFO کمترین میزان جزئیات را دارد، در حالی که حالت DEBUG بیشترین جزئیات را ارائه می‌دهد. فعال کردن خط ۱۱ به شما امکان می‌دهد ببینید Hibernate چه کاری انجام می‌دهد، به‌ویژه هنگام راه‌اندازی برنامه. این امر اغلب مفید است.
  • خط ۱۲، در صورت فعال بودن، به شما امکان می‌دهد تا آرگومان‌های واقعی مورد استفاده در هنگام اجرای پرس‌وجوهای پیکربندی‌شده SQL را مشاهده کنید.

بیایید با غیرفعال کردن کامنت خط 14 شروع کنیم


# لاگ JDBC آرگومان‌های زمان اجرا پارامتر پیوندی
log4j.logger.org.hibernate.type=DEBUG

و [InitDB] را مجدداً اجرا کنید. لاگ‌های جدید تولیدشده توسط این تغییر به شرح زیر است (نمای جزئی):

Hibernate: 
    insert 
    into
        jpa01_personne
        (VERSION, NOM, PRENOM, DATENAISSANCE, MARIE, NBENFANTS) 
    values
        (?, ?, ?, ?, ?, ?)
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 1
07:20:03,843 DEBUG StringType:80 - binding 'Durant' to parameter: 2
07:20:03,843 DEBUG StringType:80 - binding 'Sylvie' to parameter: 3
07:20:03,843 DEBUG DateType:80 - binding '05 juillet 2001' to parameter: 4
07:20:03,843 DEBUG BooleanType:80 - binding 'false' to parameter: 5
07:20:03,843 DEBUG IntegerType:80 - binding '0' to parameter: 6
  • خطوط ۸ تا ۱۰ ورودی‌های جدید لاگ هستند که در نتیجه فعال‌سازی خط ۱۴ از [log4j.properties] ایجاد شده‌اند. آن‌ها پنج مقداری را که به پارامترهای رسمی ؟ در پرس‌وجوی پارامتریک در خطوط ۲ تا ۷ اختصاص داده شده‌اند، نشان می‌دهند. بنابراین می‌توانیم ببینیم که به ستون VERSION مقدار 0 (خط 8) اختصاص داده خواهد شد.

اکنون خط ۱۱ از [log4j.properties] را فعال کنیم:

# گزینه‌های گزارش‌گیری Hibernate (INFO فقط پیام‌های راه‌اندازی را نمایش می‌دهد)
log4j.logger.org.hibernate=INFO

و دوباره [InitDB] را اجرا کنید:

07:50:23,937  INFO Version:15 - Hibernate EntityManager 3.2.0.CR3
07:50:23,968  INFO Version:15 - Hibernate Annotations 3.2.0.CR3
07:50:23,984  INFO Environment:500 - Hibernate 3.2.0.cr5
07:50:23,984  INFO Environment:533 - hibernate.properties not found
07:50:23,984  INFO Environment:667 - Bytecode provider name : cglib
07:50:24,000  INFO Environment:584 - using JDK 1.4 java.sql.Timestamp handling
07:50:24,375  INFO AnnotationBinder:387 - Binding entity from annotated class: entites.Personne
07:50:24,421  INFO EntityBinder:340 - Bind entity entites.Personne on table jpa01_personne
07:50:24,609  INFO C3P0ConnectionProvider:50 - C3P0 using driver: com.mysql.jdbc.Driver at URL: jdbc:mysql://localhost:3306/jpa
07:50:24,609  INFO C3P0ConnectionProvider:51 - Connection properties: {user=jpa, password=****, autocommit=true, release_mode=auto}
07:50:24,609  INFO C3P0ConnectionProvider:54 - autocommit mode: true
07:50:25,296  INFO SettingsFactory:81 - RDBMS: MySQL, version: 5.0.37-community-nt
07:50:25,296  INFO SettingsFactory:82 - JDBC driver: MySQL-AB JDBC Driver, version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
07:50:25,312  INFO Dialect:141 - Using dialect: org.hibernate.dialect.MySQL5InnoDBDialect
07:50:25,312  INFO TransactionFactoryFactory:34 - Transaction strategy: org.hibernate.transaction.JDBCTransactionFactory
07:50:25,312  INFO TransactionManagerLookupFactory:33 - No TransactionManagerLookup configured (in JTA environment, use of read-write or transactional second-level cache is not recommended)
07:50:25,328  INFO SettingsFactory:134 - Automatic flush during beforeCompletion(): disabled
07:50:25,328  INFO SettingsFactory:138 - Automatic session close at end of transaction: disabled
07:50:25,328  INFO SettingsFactory:145 - JDBC batch size: 15
07:50:25,328  INFO SettingsFactory:148 - JDBC batch updates for versioned data: disabled
07:50:25,328  INFO SettingsFactory:153 - Scrollable result sets: enabled
07:50:25,328  INFO SettingsFactory:161 - JDBC3 getGeneratedKeys(): enabled
07:50:25,328  INFO SettingsFactory:169 - Connection release mode: auto
07:50:25,328  INFO SettingsFactory:193 - Maximum outer join fetch depth: 2
07:50:25,328  INFO SettingsFactory:196 - Default batch fetch size: 1
07:50:25,328  INFO SettingsFactory:200 - Generate SQL with comments: disabled
07:50:25,328  INFO SettingsFactory:204 - Order SQL updates by primary key: disabled
07:50:25,328  INFO SettingsFactory:369 - Query translator: org.hibernate.hql.ast.ASTQueryTranslatorFactory
07:50:25,328  INFO ASTQueryTranslatorFactory:24 - Using ASTQueryTranslatorFactory
07:50:25,328  INFO SettingsFactory:212 - Query language substitutions: {}
07:50:25,328  INFO SettingsFactory:217 - JPA-QL strict compliance: enabled
07:50:25,328  INFO SettingsFactory:222 - Second-level cache: enabled
07:50:25,328  INFO SettingsFactory:226 - Query cache: disabled
07:50:25,328  INFO SettingsFactory:356 - Cache provider: org.hibernate.cache.NoCacheProvider
07:50:25,328  INFO SettingsFactory:241 - Optimize cache for minimal puts: disabled
07:50:25,328  INFO SettingsFactory:250 - Structured second-level cache entries: disabled
07:50:25,343  INFO SettingsFactory:270 - Echoing all SQL to stdout
07:50:25,343  INFO SettingsFactory:277 - Statistics: disabled
07:50:25,343  INFO SettingsFactory:281 - Deleted entity synthetic identifier rollback: disabled
07:50:25,343  INFO SettingsFactory:296 - Default entity-mode: pojo
07:50:25,468  INFO SessionFactoryImpl:161 - building session factory
07:50:25,750  INFO SessionFactoryObjectFactory:82 - Not binding factory to JNDI, no JNDI name configured
07:50:25,765  INFO SchemaExport:154 - Running hbm2ddl schema export
07:50:25,765  INFO SchemaExport:179 - exporting generated schema to database
07:50:25,968  INFO SchemaExport:196 - schema export complete
Hibernate: 
    delete 
    from
        jpa01_personne
Hibernate: 
    ... 

خواندن این لاگ‌ها اطلاعات بسیار جالبی را ارائه می‌دهد:

  • خط ۷: Hibernate نام کلاسی با تگ @Entity را که یافته است مشخص می‌کند
  • خط ۸: نشان می‌دهد که کلاس [Personne] به جدول [jpa01_personne] پیوند داده خواهد شد
  • خط ۹: استخر اتصال C3P0 را که باید استفاده شود، نام درایور JDBC و URL پایگاه داده را که باید مدیریت شود، مشخص می‌کند
  • خط ۱۰: جزئیات بیشتری از اتصال JDBC را ارائه می‌دهد: مالک، نوع commit و غیره.
  • خط ۱۴: گویش مورد استفاده برای ارتباط با SGBD
  • خط ۱۵: نوع تراکنش مورد استفاده. JDBCTransactionFactory نشان می‌دهد که برنامه تراکنش‌های خود را مدیریت می‌کند. این برنامه در داخل یک کانتینر EJB3 که سرویس تراکنش خود را فراهم می‌کند، اجرا نمی‌شود.
  • خطوط زیر مربوط به گزینه‌های پیکربندی Hibernate هستند که با آن‌ها مواجه نشده‌ایم. از خوانندگان علاقه‌مند دعوت می‌شود تا به مستندات Hibernate مراجعه کنند.
  • خط ۳۷: دستورات SQL روی کنسول نمایش داده خواهند شد. این مورد در [persistence.xml] درخواست شده است:

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true" />
            <property name="use_sql_comments" value="true" />
  • خطوط ۴۳–۴۵: شِمای پایگاه داده به SGBD و c.a.d صادر می‌شود. سپس پایگاه داده تخلیه و مجدداً ایجاد می‌شود. این مکانیزم از پیکربندی ارائه‌شده در [persistence.xml] (خط ۴ زیر) نشأت می‌گیرد:

            ...
            <property name="hibernate.connection.password" value="jpa" />
            <!-- ایجاد خودکار طرحواره -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!-- گویش -->
            ...

وقتی یک برنامه با یک خطای Hibernate که قابل درک نیست از کار می‌افتد، اولین قدم فعال کردن لاگ‌گیری Hibernate در حالت DEBUG درون [log4j.properties] است تا درک بهتری به دست آید:


# گزینه لاگر ریشه
log4j.rootLogger=ERROR, stdout

# گزینه‌های لاگ‌گیری Hibernate (INFO فقط پیام‌های راه‌اندازی را نمایش می‌دهد)
log4j.logger.org.hibernate=DEBUG

در بقیه این سند، ثبت گزارش (logging) به طور پیش‌فرض غیرفعال شده است تا خروجی کنسول خواناتر باشد.

2.1.12. کاوش در زبان JPQL / HQL با استفاده از کنسول Hibernate

توجه: این بخش به افزونه Hibernate Tools (بخش 5.2.5) نیاز دارد.

در کد برنامه [InitDB] از پرس‌وجوی JPQL استفاده کردیم. JPQL (زبان پرس‌وجوی پایداری جاوا) زبانی برای پرس‌وجو در زمینهٔ پیوستگی است. پرس‌وجوی مواجه شده به شرح زیر بود:

select p from Personne p order by p.nom asc

این عبارت تمام رکوردهای جدول مرتبط با @Entity [Personne] را انتخاب کرده و آن‌ها را به ترتیب صعودی بر اساس نام بازگردانده است. در عبارت پرس‌وجوی بالا، p.nom فیلد «name» یک نمونه p از کلاس [Personne] است. بنابراین، یک پرس‌وجو برای JPQL بر روی اشیاء @Entity در زمینه پایداری عمل می‌کند و نه مستقیماً بر روی جداول پایگاه داده. لایه JPA به نوبه خود این پرس‌وجوی JPQL را به یک پرس‌وجوی SQL مناسب برای SGBD که با آن کار می‌کند، تبدیل خواهد کرد. بنابراین، در مورد پیاده‌سازی JPA / در Hibernate متصل به SGBD MySQL5، پرس‌وجوی قبلی JPQL به پرس‌وجوی زیر SQL ترجمه می‌شود:

select
  personne0_.ID as ID0_,
  personne0_.VERSION as VERSION0_,
  personne0_.NOM as NOM0_,
  personne0_.PRENOM as PRENOM0_,
  personne0_.DATENAISSANCE as DATENAIS5_0_,
  personne0_.MARIE as MARIE0_,
  personne0_.NBENFANTS as NBENFANTS0_ 
 from
  jpa01_personne personne0_ 
 order by
  personne0_.NOM asc

لایه JPA از پیکربندی شیء @Entity [Personne] برای تولید دستور صحیح SQL استفاده کرد. این پل شیء-رابطه‌ای است که در اینجا پیاده‌سازی شده است.

پلاگین [Hibernate Tools] (بخش 5.2.5) ابزاری به نام «کنسول Hibernate» را فراهم می‌کند که اجازه می‌دهد

  • صدور دستورات JPQL یا مجموعهٔ فراگیرتر HQL (زبان پرس‌وجوی Hibernate) در زمینهٔ پایداری
  • برای بازیابی نتایج
  • تا معادل SQL را که علیه پایگاه داده اجرا شده است، بیابید

کنسول Hibernate ابزاری بسیار ارزشمند برای یادگیری زبان JPQL و آشنایی با پل JPQL / SQL است. معلوم است که JPA به شدت از ابزارهای ORM مانند Hibernate و Toplink بهره می‌برد. JPQL بسیار شبیه زبان HQL در Hibernate است اما همه ویژگی‌های آن را شامل نمی‌شود. در کنسول Hibernate می‌توانید دستورات HQL را صادر کنید که به‌طور معمول در کنسول اجرا می‌شوند اما جزئی از زبان JPQL نیستند و بنابراین نمی‌توان از آن‌ها در یک کلاینت JPA استفاده کرد. در مواردی که چنین باشد، به آن اشاره خواهیم کرد.

بیایید یک کنسول Hibernate برای پروژه فعلی Eclipse خود ایجاد کنیم:

  • [1]: ما به چشم‌انداز [Hibernate Console] (پنجره / باز کردن چشم‌انداز / دیگر) سوئیچ می‌کنیم
  • [2]: ما یک پیکربندی جدید را در پنجره [Hibernate Configuration] ایجاد می‌کنیم
  • با استفاده از دکمه [4]، پروژه جاوا را که پیکربندی Hibernate برای آن ایجاد می‌شود، انتخاب می‌کنیم. نام آن در [3] نمایش داده می‌شود.
  • در [5]، نام دلخواه خود را برای این پیکربندی وارد می‌کنیم. در اینجا، ما از [3] استفاده کرده‌ایم.
  • در [6]، مشخص می‌کنیم که از پیکربندی‌ای به نام JPA استفاده می‌کنیم تا ابزار بداند که باید فایل [META-INF/persistence.xml] را پردازش کند.
  • به [7]: مشخص می‌کنیم که در این فایل [META-INF/persistence.xml]، باید از واحد پایداری با نام jpa استفاده شود.
  • در [8]، پیکربندی را اعتبارسنجی می‌کنیم.

سپس باید SGBD اجرا شود. در اینجا، MySQL5 است.

  • در [1]: پیکربندی ایجادشده دارای ساختار درختی سه‌شاخه‌ای است
  • در [2]: شاخه [Configuration] اشیایی را که کنسول برای پیکربندی خود استفاده کرده است، فهرست می‌کند: در اینجا، @Entity Personne.
  • در [3]: Session Factory یک مفهوم Hibernate است که مشابه EntityManager در JPA است. این مفهوم با استفاده از اشیاء موجود در شاخه [Configuration]، شکاف شیء-رابطه‌ای را پر می‌کند. در [3]، اشیاء زمینه پایداری ارائه می‌شوند، در اینجا بار دیگر @Entity Personne.
  • در [4]: پایگاه داده‌ای که از طریق پیکربندی موجود در [persistence.xml] به آن دسترسی پیدا می‌شود. این شامل جدول [jpa01_personne] است.
  • در [1]، یک ویرایشگر ایجاد می‌شود: HQL
  • در ویرایشگر HQL،
    • در [2]، پیکربندی Hibernate را که می‌خواهید استفاده کنید انتخاب کنید اگر چندین مورد وجود داشته باشد
    • در [3]، دستوری را که می‌خواهید اجرا کنید در JPQL تایپ کنید
    • در [4]، آن را اجرا کنید
  • در [5]، نتایج پرس‌وجو در پنجره [Hibernate Query Result] نمایش داده می‌شوند. ممکن است در اینجا با دو مشکل مواجه شوید:
    • هیچ نتیجه‌ای دریافت نمی‌کنید (هیچ ردیفی). کنسول Hibernate از محتویات [persistence.xml] برای ایجاد یک اتصال به SGBD استفاده کرده است. با این حال، این پیکربندی دارای خواصی است که به پایگاه داده دستور می‌دهد تا خالی شود:

            <property name="hibernate.hbm2ddl.auto" value="create" />

بنابراین، باید برنامه [InitDB] را مجدداً اجرا کنید، پیش از آنکه دستور JPQL فوق را دوباره اجرا کنید.

  • (ادامه)
    • پنجره [Hibernate Query Result] نمایش داده نمی‌شود. این پنجره با استفاده از [Window / Show View / ...] فراخوانی می‌شود.

پنجره [Hibernate Dynamic SQL preview] ([1] در زیر) به شما امکان می‌دهد پرس‌وجوی SQL را که برای اجرای دستور JPQL که در حال حاضر در حال نوشتن آن هستید، اجرا خواهد شد، مشاهده کنید. به محض اینکه نحو دستور JPQL صحیح باشد، دستور متناظر SQL در این پنجره ظاهر می‌شود:

  • در [2]، دستور قبلی HQL را حذف می‌کنیم
  • در [3]، یک فرمان جدید اجرا می‌شود
  • در [4]، نتیجه
  • در [5]، فرمان SQL که بر اساس

ویرایشگر HQL در نوشتن دستورات HQL کمک می‌کند:

  • در [1]: هنگامی که ویرایشگر تشخیص می‌دهد که p یک شیء Personne است، می‌تواند در حین تایپ کردن، فیلدهای p را پیشنهاد دهد.
  • در [2]: یک دستور نادرست HQL. شما باید تایپ کنید جایی که p.marie=true.
  • در [3]: خطا در پنجره [SQL Preview] گزارش شده است

از خواننده دعوت می‌شود تا دستورات بیشتری از HQL / JPQL را روی پایگاه داده صادر کند.

2.1.13. یک مشتری دوم: JPA

بیایید از منظر جاوا به پروژه بازگردیم:

 
  • [InitDB.java] برنامه‌ای است که چند ردیف را در جدول [jpa01_personne] در پایگاه داده وارد کرده است. تحلیل کد آن به ما امکان داده است تا جزئیات اولیه API و JPA را گردآوری کنیم.
  • [Main.java] برنامه‌ای است که عملیات CRUD را بر روی جدول [jpa01_personne] انجام می‌دهد. تحلیل کد آن به ما امکان می‌دهد تا مفاهیم بنیادی زمینه پایداری و چرخه عمر اشیاء در آن زمینه را مجدداً بررسی کنیم.

2.1.13.1. ساختار کد

[Main.java] مجموعه‌ای از تست‌ها را اجرا خواهد کرد که هر یک با هدف نمایش جنبه‌ای خاص از JPA طراحی شده‌اند:

 

روش [main]

  • متدهای test1 تا test11 را به‌ترتیب فراخوانی می‌کند. کد هر یک از این متدها را به‌طور جداگانه ارائه خواهیم داد.
  • همچنین از متدهای کمکی خصوصی استفاده می‌کند: clean، dump، log، getEntityManager و getNewEntityManager.

ما روش main و روش‌های به اصطلاح ابزار کمکی را ارائه می‌دهیم:


package tests;

...
import entites.Personne;

@SuppressWarnings("unchecked")
public class Main {

    // ثوابت
    private final static String TABLE_NAME = "jpa01_personne";

    // زمینه پایداری
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
    private static EntityManager em = null;

    // ابجکت‌های مشترک
    private static Personne p1, p2, newp1;

    public static void main(String[] args) throws Exception {
        //پاکسازی پایگاه داده
        log("clean");clean();

        // خروجی جدول
        dump();

        // test1
        log("test1");test1();

...
        // test11
        log("test11");test11();

        // پایان زمینه پایداری
        if (em.isOpen())
            em.close();

        // بستن EntityManagerFactory
        emf.close();
    }

    // بازیابی EntityManager فعلی
    private static EntityManager getEntityManager() {
        if (em == null || !em.isOpen()) {
            em = emf.createEntityManager();
        }
        return em;
    }

    // بازیابی یک QZXW2HTMLCRW50aXR5TWFuYWdlc جدید ZQX
    private static EntityManager getNewEntityManager() {
        if (em != null && em.isOpen()) {
            em.close();
        }
        em = emf.createEntityManager();
        return em;
    }

    // نمایش محتویات جدول
    private static void dump() {
        // زمینه پایداری فعلی
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // نمایش افراد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // پایان تراکنش
        tx.commit();
    }

    //پاک کردن BD
    private static void clean() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // حذف عناصر جدول PERSONNES
        em.createNativeQuery("delete from " + TABLE_NAME).executeUpdate();
        //پایان تراکنش
        tx.commit();
    }

    // گزارش‌ها
    private static void log(String message) {
        System.out.println("main : ----------- " + message);
    }

    // ایجاد شیء
    public static void test1() throws ParseException {
...
    }

    // تغییر یک شیء زمینه
    public static void test2() {
...
    }

    // ابجکت‌های درخواست
    public static void test3() {
...
    }

    // حذف یک شیء متعلق به زمینه پایداری
    public static void test4() {
....
    }

    //جدا کردن، دوباره متصل کردن و تغییر دادن
    public static void test5() {
...
    }

    // حذف یک شیء که به زمینه پایداری تعلق ندارد
    public static void test6() {
...
    }

    // تغییر یک شیء که به زمینه پایداری تعلق ندارد
    public static void test7() {
...
    }

    // اتصال مجدد یک شیء به زمینه پایداری
    public static void test8() {
...
    }

    //یک پرس‌وجوی SELECT همگام‌سازی را آغاز می‌کند
    // همگام‌سازی پایگاه داده با زمینه پایداری
    public static void test9() {
....
    }

    // کنترل نسخه (قفل‌گذاری خوش‌بینانه)
    public static void test10() {
...
    }

    // برگشت یک تراکنش
    public static void test11() throws ParseException {
...
    }

}
  • خط ۱۳: شیء EMF با شناسه EntityManagerFactory که از واحد پایداری JPA تعریف‌شده در [persistence.xml] ساخته شده است. این امکان را به ما می‌دهد تا در سراسر برنامه، زمینه‌های پایداری مختلفی ایجاد کنیم.
  • خط ۱۴: یک کنتکست پایداری EntityManager که هنوز инициалиزه نشده است
  • خط 17: سه شیء [Personne] که در میان تست‌ها مشترک هستند
  • خط ۲۱: جدول jpa01_personne پاک‌سازی شده و سپس در خط ۲۴ نمایش داده می‌شود تا اطمینان حاصل شود که با یک جدول خالی شروع می‌کنیم.
  • خطوط ۲۷–۳۱: توالی تست‌ها
  • خطوط 34–35: اگر زمینه پایداری باز باشد، بسته می‌شود.
  • خط ۳۸: شیء emf با شناسه EntityManagerFactory بسته می‌شود.
  • خطوط ۴۲–۴۷: متد [getEntityManager]، شیء EntityManager (یا زمینه پایداری) را به شیء فعلی تبدیل می‌کند یا در صورتی که وجود نداشته باشد، یک شیء جدید ایجاد می‌کند (خطوط ۴۳–۴۴).
  • خطوط ۵۰–۵۶: متد [getNewEntityManager] یک زمینه پایداری جدید ایجاد می‌کند. اگر قبلاً موردی وجود داشته باشد، بسته می‌شود (خطوط ۵۱–۵۲)
  • خطوط 59–72: متد [dump] محتویات جدول [jpa01_personne] را نمایش می‌دهد. این کد قبلاً در [InitDB] مشاهده شده است.
  • خطوط ۷۵–۸۵: متد [clean] جدول [jpa01_personne] را پاک می‌کند. این کد قبلاً در [InitDB] مشاهده شده است.
  • خطوط ۸۸–۹۰: متد [log] پیامی را که به‌عنوان پارامتر به آن ارسال شده است، روی کنسول نمایش می‌دهد تا مورد توجه قرار گیرد.

اکنون می‌توانیم به بررسی تست‌ها بپردازیم.

2.1.13.2. آزمون ۱

کد مربوط به تست ۱ به شرح زیر است:


// ایجاد اشیاء
    public static void test1() throws ParseException {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // ایجاد افراد
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // پایداری اشخاص
        em.persist(p1);
        em.persist(p2);
        //پایان تراکنش
        tx.commit();
        //نمایش جدول
        dump();

}

این کد قبلاً در [InitDB] دیده شده است: دو نفر را ایجاد می‌کند و آن‌ها را در زمینه پایداری قرار می‌دهد.

  • خط ۴: زمینه پایداری جاری بازیابی می‌شود
  • خطوط ۶–۷: دو شخص ایجاد می‌شوند
  • خطوط ۹–۱۵: دو شخص در یک تراکنش در زمینه پایداری قرار می‌گیرند.
  • خط ۱۵: در نتیجه commit تراکنش، زمینه پایداری با پایگاه داده همگام‌سازی می‌شود. این دو شخص به جدول [jpa01_personne] اضافه خواهند شد.
  • خط ۱۷: جدول نمایش داده می‌شود

خروجی کنسول برای این تست اول به شرح زیر است:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]

2.1.13.3. آزمون ۲

کد برای تست ۲ به شرح زیر است:


// تغییر یک شیء زمینه
    public static void test2() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // افزایش تعداد فرزندان برای p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // تغییر وضعیت تأهل
        p1.setMarie(false);
        //شیء p1 به‌طور خودکار ذخیره می‌شود (بررسی کثیف)
        // در طول همگام‌سازی بعدی (commit یا select)
        //پایان تراکنش
        tx.commit();
        //جدول جدید نمایش داده می‌شود
        dump();
    }
  • هدف از تست ۲ این است که یک شیء را در زمینه پایداری اصلاح کرده و سپس محتویات جدول را نمایش دهیم تا ببینیم آیا اصلاح انجام شده است یا خیر
  • خط ۴: زمینه پایداری فعلی بازیابی می‌شود
  • خطوط ۶–۷: عملیات‌ها در داخل یک تراکنش انجام خواهند شد
  • خطوط ۹ و ۱۱: تعداد فرزندان شخص p1 و وضعیت تأهل او تغییر داده می‌شود
  • خط ۱۵: پایان تراکنش، بنابراین زمینه پایداری با پایگاه داده همگام‌سازی می‌شود
  • خط ۱۷: نمایش جدول

خروجی کنسول برای تست ۲ به شرح زیر است:

1
2
3
4
5
6
7
8
main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
  • خط ۴: شخص p1 قبل از اصلاح
  • خط ۸: شخص p1 پس از اصلاح. توجه کنید که شماره نسخه آن به ۱ تغییر کرده است. این عدد هر بار که سطر به‌روزرسانی می‌شود، ۱ افزایش می‌یابد.

2.1.13.4. آزمون ۳

کد برای تست ۳ به شرح زیر است:


    // ابجکت‌های درخواست
    public static void test3() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // فرد درخواست‌کننده p1
        Personne p1b = em.find(Personne.class, p1.getId());
        // از آنجایی که p1 قبلاً در زمینه پایداری قرار دارد، هیچ دسترسی به پایگاه داده صورت نگرفت
        // p1b و p1 یکسان هستند
        System.out.format("p1==p1b ? %s%n", p1 == p1b);
        //درخواست یک شیء که وجود ندارد، یک نشانگر null برمی‌گرداند
        Personne px = em.find(Personne.class, -4);
        System.out.format("px==null ? %s%n", px == null);
        //پایان تراکنش
        tx.commit();
}
  • آزمون ۳ بر روی متد [EntityManager.find] تمرکز دارد که یک شیء را از پایگاه داده بازیابی کرده و در زمینه پایداری قرار می‌دهد. ما دیگر تراکنشی را که در تمام آزمون‌ها رخ می‌دهد توضیح نخواهیم داد، مگر اینکه به روشی غیرمعمول استفاده شود.
  • خط ۹: از کانتکست پایداری می‌خواهیم شخص را که کلید اصلی یکسانی با person p1 دارد، برگرداند. دو حالت وجود دارد:
    • p1 از قبل در زمینه پایداری قرار دارد. در اینجا نیز همین وضعیت برقرار است. بنابراین هیچ دسترسی به پایگاه داده انجام نمی‌شود. متد find صرفاً مرجعی به شیء پایدارسازی‌شده بازمی‌گرداند.
    • p1 در زمینه پایداری قرار ندارد. در این حالت، از طریق کلید اصلی ارائه‌شده به پایگاه داده دسترسی پیدا می‌شود. سطر بازیابی‌شده در زمینه پایداری قرار می‌گیرد و find مرجعی به این شیء جدید پایدار شده بازمی‌گرداند.
  • خط ۱۲: بررسی می‌شود تا اطمینان حاصل شود که متد find مرجعی به شیء p1 را که از قبل در زمینه قرار دارد، بازگردانده است
  • خط 14: درخواستی برای یک شیء ارسال می‌شود که نه در زمینه پایداری و نه در پایگاه داده وجود ندارد. سپس متد find نشانگر null را بازمی‌گرداند. این موضوع در خط 15 تأیید می‌شود.

خروجی کنسول برای تست ۳ به شرح زیر است:

1
2
3
main : ----------- test3
p1==p1b ? true
px==null ? true

2.1.13.5. آزمون ۴

کد برای آزمون ۴ به شرح زیر است:


    // حذف یک شیء متعلق به زمینه پایداری
    public static void test4() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // شیء پایدارسازی‌شده p2 حذف می‌شود
        em.remove(p2);
        //پایان تراکنش
        tx.commit();
        //جدول جدید نمایش داده می‌شود
        dump();
}
  • آزمون ۴ بر روی متد [EntityManager.remove] تمرکز دارد که امکان حذف یک عنصر از زمینه پایداری و در نتیجه از پایگاه داده را فراهم می‌کند.
  • خط ۹: شخص p2 از زمینه پایداری حذف می‌شود
  • خط ۱۱: زمینه با پایگاه داده همگام‌سازی می‌شود
  • خط ۱۳: جدول نمایش داده می‌شود. معمولاً شخص p2 دیگر نباید آنجا باشد.

خروجی کنسول برای تست ۴ به شرح زیر است:

main : ----------- test1
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,0,Martin,Paul,31/01/2000,true,2]
main : ----------- test2
[personnes]
[2,0,Durant,Sylvie,05/07/2001,false,0]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test3
p1==p1b ? true
px==null ? true
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
  • خط ۳: شخص p2 در test1
  • خطوط ۱۲–۱۴: آنها پس از test4 دیگر وجود ندارند.

2.1.13.6. آزمون ۵

کد برای آزمون ۵ به شرح زیر است:


//جدا کردن، دوباره متصل کردن و تغییر دادن
    public static void test5() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 جدا شد
        Personne oldp1=p1;
        //p1 به زمینهٔ جدید متصل می‌شود
        p1 = em.find(Personne.class, p1.getId());
        // تأیید
        System.out.format("p1==oldp1 ? %s%n", p1 == oldp1);        
        //پایان تراکنش
        tx.commit();
        // افزایش تعداد فرزندان برای p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // جدول جدید را نمایش دهید
        dump();
    }
  • آزمون ۵ چرخهٔ عمر اشیاء پایدار شده را در چندین زمینهٔ پایداری متوالی بررسی می‌کند. تا این لحظه، ما همواره از یک زمینهٔ پایداری یکسان در آزمون‌های مختلف استفاده کرده بودیم.
  • خط ۴: یک زمینه پایداری جدید درخواست می‌شود. متد [getNewEntityManager] زمینه قبلی را می‌بندد و یکی جدید را باز می‌کند. در نتیجه، اشیاء p1 و p2 که توسط برنامه نگهداری می‌شوند دیگر در وضعیت پایدار نیستند. آنها متعلق به زمینه‌ای بودند که بسته شده است. گفته می‌شود که آن‌ها در وضعیت جداشده (detached) قرار دارند. آن‌ها به کنتکست پایداری جدید تعلق ندارند.
  • خطوط ۶–۷: شروع تراکنش. در اینجا، از آن به روشی غیرمعمول استفاده می‌شود.
  • خط ۹: ما آدرس شیء p1 را که اکنون جدا شده است، ثبت می‌کنیم.
  • خط ۱۱: از کانتکست پایداری برای شخص p1 (با استفاده از کلید اصلی p1) پرس‌وجو می‌شود. از آنجایی که کانتکست جدید است، شخص p1 در آن وجود ندارد. بنابراین یک پرس‌وجوی پایگاه داده انجام خواهد شد. شیء بازگشتی در کانتکست جدید قرار داده خواهد شد.
  • خط ۱۳: ما بررسی می‌کنیم که شی دائمی p1 در زمینه با شی oldp1، که شی جداشده قبلی p1 بود، متفاوت است.
  • خط ۱۵: تراکنش تکمیل می‌شود
  • خط ۱۷: شیء پایدار جدید p1 خارج از تراکنش تغییر می‌کند. در این حالت چه اتفاقی می‌افتد؟ می‌خواهیم بفهمیم.
  • خط ۱۹: ما درخواست نمایش جدول را داریم. به یاد داشته باشید که به دلیل select صادر شده توسط متد dump، زمینه پایداری به طور خودکار با پایگاه داده همگام‌سازی می‌شود.

خروجی کنسول برای آزمون ۵ به شرح زیر است:

1
2
3
4
5
6
7
main : ----------- test4
[personnes]
[1,1,Martin,Paul,31/01/2000,false,3]
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • خط ۵: متد find واقعاً به پایگاه داده دسترسی داشته است؛ در غیر این صورت، دو اشاره گر برابر می‌بودند
  • خطوط ۷ و ۳: تعداد فرزندان p1 واقعاً ۱ واحد افزایش یافته است. بنابراین، این تغییر که خارج از یک تراکنش انجام شده، در نظر گرفته شده است. این موضوع در واقع به SGBD مورد استفاده بستگی دارد. در یک SGBD، یک فرمان SQL همیشه در داخل یک تراکنش اجرا می‌شود. اگر کلاینت JPA خود یک تراکنش صریح را آغاز نکند، SGBD سپس یک تراکنش ضمنی را آغاز خواهد کرد. دو سناریوی رایج وجود دارد:
    • ۱ – هر دستور SQL به‌صورت جداگانه بخشی از یک تراکنش است که قبل از دستور باز شده و پس از آن بسته می‌شود. به این حالت، حالت خودتأیید (autocommit) گفته می‌شود. بنابراین، همه چیز طوری رفتار می‌کند که گویی مشتری JPA برای هر فرمان SQL، تراکنش‌های جداگانه‌ای را انجام می‌دهد.
    • ۲ - SGBD در حالت autocommit نیست و یک تراکنش ضمنی را با اولین سفارش SQL آغاز می‌کند، سفارشی که مشتری JPA خارج از یک تراکنش صادر کرده است و بستن آن را به خود مشتری واگذار می‌کند. تمام دستورات SQL که توسط کلاینت JPA صادر می‌شوند، سپس بخشی از تراکنش ضمنی هستند. این تراکنش ممکن است به دلیل رویدادهای مختلف پایان یابد: کلاینت اتصال را می‌بندد، یک تراکنش جدید را آغاز می‌کند و غیره.

این وضعیت به پیکربندی SGBD بستگی دارد. بنابراین این کد قابل حمل نیست. کمی بعد، ما کدی را نشان خواهیم داد که از تراکنش‌ها استفاده نمی‌کند و خواهیم دید که همه نمونه‌های SGBD در مورد این کد به یک شکل رفتار نمی‌کنند. بنابراین ما کار کردن خارج از تراکنش‌ها را یک خطای برنامه‌نویسی در نظر می‌گیریم.

  • خط ۷: توجه کنید که شماره نسخه به ۲ تغییر کرده است.

2.1.13.7. آزمون ۶

کد مربوط به تست ۶ به شرح زیر است:


// حذف یک شیء که به زمینه پایداری تعلق ندارد
    public static void test6() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 که به زمینهٔ جدید تعلق ندارد، حذف می‌شود
        try {
            em.remove(p1);
            //پایان تراکنش
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur à la suppression de p1 : [%s,%s]%n", e1.getClass().getName(), e1.getMessage());
            // معامله برگشت داده می‌شود
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // نمایش جدول جدید
        dump();
    }
  • آزمون ۶ تلاش می‌کند شیئی را که به زمینه پایداری تعلق ندارد، حذف کند.
  • خط ۴: یک زمینه پایداری جدید درخواست می‌شود. بنابراین زمینه قدیمی بسته می‌شود و اشیایی که در آن بودند جدا می‌شوند. این مورد برای شیء p1 از تست ۵ قبلی صادق است.
  • خطوط ۶–۷: شروع تراکنش.
  • خط ۱۰: شیء جداشده p1 حذف می‌شود. می‌دانیم این کار باعث ایجاد استثنا خواهد شد، بنابراین عملیات را در یک بلوک try/catch قرار داده‌ایم.
  • خط ۱۲: commit انجام نخواهد شد.
  • خطوط ۱۶–۲۱: یک تراکنش باید با یک commit (تمام عملیات در تراکنش commit می‌شوند) یا یک rollback (تمام عملیات در تراکنش roll back می‌شوند) پایان یابد. یک استثنا رخ داده است، بنابراین ما یک rollback برای تراکنش صادر می‌کنیم. هیچ چیزی برای بازگشت (roll back) وجود ندارد زیرا تنها عملیات در تراکنش ناموفق بوده است، اما rollback تراکنش را خاتمه می‌دهد. این اولین باری است که از عملیات [EntityTransaction].rollback استفاده می‌کنیم. ما باید از همان مثال‌های اول این کار را انجام می‌دادیم. ما این کار را برای ساده‌ نگه داشتن کد انجام ندادیم. با این حال، خواننده باید به خاطر داشته باشد که مورد rollback برای تراکنش باید همیشه در کد در نظر گرفته شود.
  • خط ۲۴: جدول نمایش داده می‌شود. به طور معمول، نباید تغییر کرده باشد.

خروجی کنسول برای تست ۶ به شرح زیر است:

1
2
3
4
5
6
7
8
main : ----------- test5
p1==oldp1 ? false
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • خط ۶: حذف p1 ناموفق بوده است. پیام استثنا توضیح می‌دهد که تلاشی برای حذف یک شیء جداشده (detached object) صورت گرفته است، که بنابراین بخشی از زمینه (context) نیست. این کار امکان‌پذیر نیست.
  • خط ۸: شخص p1 هنوز وجود دارد.

2.1.13.8. آزمون ۷

کد برای تست ۷ به شرح زیر است:


// تغییر یک شیء که به زمینه پایداری تعلق ندارد
    public static void test7() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // افزایش تعداد فرزندان p1 که به زمینه جدید تعلق ندارند
        p1.setNbenfants(p1.getNbenfants() + 1);
        //پایان تراکنش
        tx.commit();
        // نمایش جدول جدید – نباید تغییر کرده باشد
        dump();
    }
  • آزمون ۷ تلاش می‌کند یک شیء را که به زمینه پایداری تعلق ندارد تغییر دهد و ببیند این کار چه تأثیری بر پایگاه داده دارد. ممکن است تصور شود که این کار هیچ تأثیری ندارد. نتایج آزمون نیز همین را نشان می‌دهد.
  • خط ۴: یک زمینه پایداری جدید درخواست می‌شود. بنابراین ما یک زمینه جدید داریم که هیچ شی پایداری‌شده‌ای در آن وجود ندارد.
  • خطوط ۶–۷: شروع تراکنش.
  • خط ۹: شیء جداشده p1 اصلاح می‌شود. این عملیاتی است که شامل زمینه پایداری em نمی‌شود. بنابراین نباید انتظار خطا یا چیزی از این دست را داشته باشیم. این یک عملیات پایه بر روی POJO است.
  • خط ۱۱: commit باعث همگام‌سازی کانکست با پایگاه داده می‌شود. این کانکست خالی است. بنابراین پایگاه داده تغییر نمی‌کند.
  • خط ۲۴: جدول نمایش داده می‌شود. به طور معمول، نباید تغییر کرده باشد.

خروجی کنسول برای تست ۷ به شرح زیر است:

1
2
3
4
5
6
7
main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
  • خط ۷: شخص p1 در پایگاه داده تغییر نکرده است. با این حال، برای تست بعدی باید به خاطر داشته باشیم که در حافظه، تعداد فرزندان او اکنون ۵ است.

2.1.13.9. آزمون ۸

کد مربوط به تست ۸ به شرح زیر است:


    // اتصال مجدد یک شیء به زمینه پایداری
    public static void test8() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // شیء جداشده p1 به زمینهٔ جدید متصل می‌شود
        newp1 = em.merge(p1);
        //اکنون newp1 جزئی از زمینه است، نه p1
        //پایان تراکنش
        tx.commit();
        //جدول جدید نمایش داده می‌شود – تعداد فرزندان p1 باید تغییر کرده باشد
        dump();
}
  • آزمون ۸ یک شی جداشده را دوباره به زمینه پایداری متصل می‌کند.
  • خط ۴: یک زمینه پایداری جدید درخواست می‌شود. بنابراین ما یک زمینه جدید داریم که هیچ شیء پایداری در آن وجود ندارد.
  • خطوط ۶–۷: شروع تراکنش.
  • خط ۹: شیء جداشده p1 به زمینه پایداری متصل می‌شود. عملیات ادغام ممکن است شامل چندین سناریو باشد:
    • حالت ۱: یک شیء پایدار ps1 در زمینه پایداری وجود دارد که کلید اصلی آن با شیء جداشده p1 یکسان است. محتویات p1 به ps1 کپی می‌شود و merge به مرجع ps1 تبدیل می‌شود.
    • مورد دوم: هیچ شی دائمی ps1 با همان کلید اصلی شی جداشده p1 در زمینه پایداری وجود ندارد. سپس از پایگاه داده پرس‌وجو می‌شود تا مشخص شود آیا شیء مورد نظر در پایگاه داده وجود دارد یا خیر. در صورت وجود، این شیء وارد زمینه پایداری شده، به شیء پایدار ps1 تبدیل می‌شود و فرآیند به مورد قبلی ۱ بازمی‌گردد.
    • مورد ۳: هیچ شیئی با کلید اصلی مشابه شیء جداشده p1، نه در زمینه پایداری و نه در پایگاه داده، وجود ندارد. سپس یک شیء جدید [Personne] (new) ایجاد شده و در زمینه پایداری قرار می‌گیرد. فرآیند سپس به مورد ۱ بازمی‌گردد.
    • در نهایت: شیء جداشده p1 همچنان جداشده باقی می‌ماند. عملیات merge یک مرجع (در اینجا newp1) به شیء پایدار ps1 که از merge مشتق شده است، بازمی‌گرداند. اکنون برنامهٔ مشتری باید با شی پایدار ps1 کار کند و نه با شی جداشده p1.
    • توجه داشته باشید که بین موارد ۱ و ۳ در مورد ترتیب SQL که برای merge برنامه‌ریزی شده است، تفاوت وجود دارد: در موارد ۱ و ۲، این سفارش UPDATE است، در حالی که در مورد ۳، سفارشی با شماره INSERT است.
  • خط ۱۲: commit باعث همگام‌سازی کانکست با پایگاه داده می‌شود. این کانکست دیگر خالی نیست. این کانکست شامل شیء newp1 است. این شیء در پایگاه داده پایدارسازی خواهد شد.
  • خط 24: جدول برای تأیید این موضوع نمایش داده می‌شود.

خروجی کنسول برای تست ۸ به شرح زیر است:

main : ----------- test6
Erreur à la suppression de p1 : [java.lang.IllegalArgumentException,Removing a detached instance entites.Personne#1]
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test7
[personnes]
[1,2,Martin,Paul,31/01/2000,false,4]
main : ----------- test8
[personnes]
[1,3,Martin,Paul,31/01/2000,false,5]
  • تعداد فرزندان برای p1 در آزمون 6 (خط 4) برابر با 4 بود، سپس در آزمون 7 به 5 تغییر یافت اما در پایگاه داده ذخیره نشد (خط 7). پس از merge، newp1 در پایگاه داده ذخیره شد: در خط ۱۰، واقعاً ۵ فرزند وجود دارد.
  • خط ۱۰: شماره نسخه newp1 به ۳ تغییر یافته است.

2.1.13.10. آزمون ۹

کد مربوط به تست ۹ به شرح زیر است:


//یک پرس‌وجوی SELECT همگام‌سازی را آغاز می‌کند
    // همگام‌سازی پایگاه داده با زمینه پایداری
    public static void test9() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // تعداد فرزندان برای newp1 افزایش می‌یابد
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        //نمایش افراد – تعداد فرزندان برای newp1 باید تغییر کرده باشد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        //پایان تراکنش
        tx.commit();
    }
  • آزمون ۹ با هدف نمایش مکانیزم همگام‌سازی زمینه است که به‌طور خودکار پیش از select انجام می‌شود.
  • خط ۵: زمینه پایداری تغییر نمی‌کند. بنابراین newp1 در آن قرار دارد.
  • خطوط ۷–۸: شروع تراکنش.
  • خط ۱۰: تعداد فرزندان شیء پایدار newp1 به اندازه ۱ افزایش می‌یابد (۵ → ۶).
  • خطوط ۱۲–۱۵: جدول با استفاده از عبارت SELECT نمایش داده می‌شود. قبل از اجرای select، زمینه با پایگاه داده همگام‌سازی خواهد شد.
  • خط 17: پایان تراکنش

برای مشاهده همگام‌سازی، خروجی لاگ Hibernate را در حالت DEBUG (log4j.properties) فعال کنید:


# گزینهٔ لاگ‌گیر ریشه
log4j.rootLogger=ERROR, stdout

# گزینه‌های لاگ‌گیری Hibernate (INFO فقط پیام‌های راه‌اندازی را نمایش می‌دهد)
log4j.logger.org.hibernate=DEBUG

خروجی کنسول برای تست ۹ به شرح زیر است:

main : ----------- test9
14:27:27,250 DEBUG JDBCTransaction:54 - begin
14:27:27,250 DEBUG ConnectionManager:415 - opening JDBC connection
14:27:27,250 DEBUG JDBCTransaction:59 - current autocommit status: true
14:27:27,250 DEBUG JDBCTransaction:62 - disabling autocommit
14:27:27,250 DEBUG JDBCContext:210 - after transaction begin
[personnes]
14:27:27,250 DEBUG QueryPlanCache:76 - located HQL query plan in cache (select p from Personne p order by p.nom asc)
14:27:27,250 DEBUG AbstractFlushingEventListener:58 - flushing session
...
14:27:27,250 DEBUG AbstractEntityPersister:3116 - entites.Personne.nbenfants is dirty
14:27:27,250 DEBUG DefaultFlushEntityEventListener:229 - Updating entity: [entites.Personne#1]
14:27:27,250 DEBUG Versioning:27 - Incrementing: 3 to 4
...
14:27:27,250 DEBUG AbstractFlushingEventListener:85 - Flushed: 0 insertions, 1 updates, 0 deletions to 1 objects
...
14:27:27,250 DEBUG ConnectionManager:463 - registering flush begin
14:27:27,250 DEBUG AbstractEntityPersister:2274 - Updating entity: [entites.Personne#1]
14:27:27,265 DEBUG AbstractEntityPersister:2276 - Existing version: 3 -> New version: 4
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - update jpa01_personne set VERSION=?, NOM=?, PRENOM=?, DATENAISSANCE=?, MARIE=?, NBENFANTS=? where ID=? and VERSION=?
14:27:27,265 DEBUG AbstractBatcher:476 - preparing statement
14:27:27,265 DEBUG AbstractEntityPersister:1927 - Dehydrating entity: [entites.Personne#1]
14:27:27,265 DEBUG IntegerType:80 - binding '4' to parameter: 1
14:27:27,265 DEBUG StringType:80 - binding 'Martin' to parameter: 2
14:27:27,265 DEBUG StringType:80 - binding 'Paul' to parameter: 3
14:27:27,265 DEBUG DateType:80 - binding '31 janvier 2000' to parameter: 4
14:27:27,265 DEBUG BooleanType:80 - binding 'false' to parameter: 5
14:27:27,265 DEBUG IntegerType:80 - binding '6' to parameter: 6
14:27:27,265 DEBUG IntegerType:80 - binding '1' to parameter: 7
14:27:27,265 DEBUG IntegerType:80 - binding '3' to parameter: 8
14:27:27,265 DEBUG AbstractBatcher:366 - about to close PreparedStatement (open PreparedStatements: 1, globally: 1)
14:27:27,265 DEBUG AbstractBatcher:525 - closing statement
14:27:27,265 DEBUG ConnectionManager:472 - registering flush end
14:27:27,265 DEBUG HQLQueryPlan:150 - find: select p from Personne p order by p.nom asc
14:27:27,265 DEBUG QueryParameters:277 - named parameters: {}
14:27:27,265 DEBUG AbstractBatcher:358 - about to open PreparedStatement (open PreparedStatements: 0, globally: 0)
14:27:27,265 DEBUG SQL:393 - select personne0_.ID as ID0_, personne0_.VERSION as VERSION0_, personne0_.NOM as NOM0_, personne0_.PRENOM as PRENOM0_, personne0_.DATENAISSANCE as DATENAIS5_0_, personne0_.MARIE as MARIE0_, personne0_.NBENFANTS as NBENFANTS0_ from jpa01_personne personne0_ order by personne0_.NOM asc
...
14:27:27,265 DEBUG Loader:1164 - result row: EntityKey[entites.Personne#1]
...
14:27:27,265 DEBUG Loader:839 - total objects hydrated: 0
14:27:27,265 DEBUG StatefulPersistenceContext:748 - initializing non-lazy collections
[1,4,Martin,Paul,31/01/2000,false,6]
14:27:27,265 DEBUG JDBCTransaction:103 - commit
14:27:27,265 DEBUG SessionImpl:337 - automatically flushing session
...
14:27:27,265 DEBUG AbstractFlushingEventListener:91 - Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
...
14:27:27,296 DEBUG JDBCTransaction:116 - committed JDBC Connection
...
  • خط ۱: تست ۹ شروع می‌شود
  • خطوط ۲–۶: تراکنش JDBC آغاز می‌شود. حالت autocommit برای SGBD غیرفعال است (خط ۵)
  • خط ۷: خروجی ناشی از خط ۱۲ کد جاوا. خطوط بعدی کد جاوا یک select را آغاز کرده و در نتیجه زمینه پایداری را با پایگاه داده همگام‌سازی می‌کنند.
  • خط ۸: دستور JPQL که قصد داریم صادر کنیم، قبلاً صادر شده است. Hibernate آن را در کش «دستورهای آماده» خود پیدا می‌کند.
  • خط ۹: هایبرنت اعلام می‌کند که قصد دارد زمینه پایداری را فلاش کند
  • خطوط ۱۱–۱۲: Hibernate (Hb) تشخیص می‌دهد که انتیت Person#1 (با کلید اصلی 1) تغییر کرده است (آلوده).
  • خطوط ۱۲–۱۳: Hb گزارش می‌دهد که در حال به‌روزرسانی این عنصر است و شماره نسخه آن را از ۳ به ۴ افزایش می‌دهد.
  • خط ۱۵: همگام‌سازی زمینه منجر به ۰ درج، ۱ به‌روزرسانی و ۰ حذف خواهد شد
  • خطوط 17–34: همگام‌سازی زمینه (فلش). توجه: افزایش نسخه (خط ۱۹)، دستور به‌روزرسانی آماده‌شده SQL (خط ۲۱)، مقادیر پارامتر برای دستور update (خطوط ۲۴–۳۱).
  • خط ۳۵: فرمان select آغاز می‌شود
  • خط ۳۸: job SQL که قرار است اجرا شود
  • خط ۴۰: select تنها یک سطر را بازمی‌گرداند
  • خط ۴۲: Hb متوجه می‌شود که در زمینه پایداری (persistence context) خود، از قبل موجوده Personne#1 را که پرس‌وجوی SELECT از پایگاه داده بازیابی کرده است، در اختیار دارد. بنابراین، این سطر را از پایگاه داده به داخل زمینه کپی نمی‌کند – عملیاتی که آن را «هیدریشن» (hydration) می‌نامد.
  • خط ۴۳: بررسی می‌کند که آیا اشیایی که توسط select بازگردانده شده‌اند وابستگی‌هایی (معمولاً کلیدهای خارجی) دارند که باید بارگذاری شوند (مجموعه‌های غیر تنبل). در این مورد، هیچ وابستگی‌ای وجود ندارد.
  • خط ۴۴: نمایشی که توسط کد جاوا فعال شده است
  • خط ۴۵: پایان تراکنش JDBC درخواست‌شده توسط کد جاوا
  • خط ۴۶: همگام‌سازی خودکار زمینه که در طول عملیات commit انجام می‌شود، آغاز می‌گردد.
  • خط ۴۸: Hb تشخیص می‌دهد که زمینه از زمان همگام‌سازی قبلی تغییر نکرده است.
  • خط ۵۰: پایان commit.

بار دیگر، لاگ‌های Hibernate در حالت DEBUG برای درک دقیق آنچه Hibernate انجام می‌دهد بسیار مفید هستند.

2.1.13.11. آزمون ۱۰

کد مربوط به test10 به شرح زیر است:


//کنترل نسخه (قفل‌گذاری خوش‌بینانه)
    public static void test10() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // افزایش نسخه newp1 مستقیماً در پایگاه داده (پرس‌وجوی بومی)
        em.createNativeQuery(String.format("update %s set VERSION=VERSION+1 WHERE ID=%d", TABLE_NAME, newp1.getId())).executeUpdate();
        //پایان تراکنش
        tx.commit();
        // شروع تراکنش جدید
        tx = em.getTransaction();
        tx.begin();
        // افزایش تعداد فرزندان newp1
        newp1.setNbenfants(newp1.getNbenfants() + 1);
        //پایان تراکنش – باید شکست بخورد زیرا newp1 دیگر نسخه صحیح را ندارد
        try {
            tx.commit();
        } catch (RuntimeException e1) {
            System.out.format("Erreur lors de la mise à jour de newp1 [%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause().getClass().getName(), e1.getCause().getMessage());
            //تراکنش برگشت داده می‌شود
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s,%s]%n", e2.getClass().getName(), e2.getMessage());
            }
        }
        // بستن زمینه که دیگر به‌روز نیست
        em.close();
        // خروجی جدول – نسخهٔ p1 باید تغییر کرده باشد
        dump();
    }
  • آزمون ۱۰ با هدف نمایش مکانیزم ارائه‌شده توسط فیلد version از @Entity Person که دارای ویژگی @Version از نوع JPA است، انجام می‌شود. ما توضیح داده‌ایم که این anotation تضمین می‌کند که در پایگاه داده، مقدار ستونی که با anotation @Version مرتبط است، هر بار که عملی update روی سطری که به آن تعلق دارد انجام می‌شود، افزایش یابد. این مکانیزم، که به آن قفل‌گذاری خوش‌بینانه نیز گفته می‌شود، مستلزم آن است که کلاینتی که می‌خواهد یک شیء O را در پایگاه داده تغییر دهد، باید جدیدترین نسخه آن شیء را در اختیار داشته باشد. اگر چنین نباشد، یعنی آن شیء از زمانی که کلاینت آن را دریافت کرده تغییر کرده است و باید به کلاینت اطلاع داده شود.
  • خط ۴: زمینه پایداری تغییر نمی‌کند. بنابراین newp1 در داخل آن قرار دارد.
  • خطوط ۶–۷: شروع یک تراکنش.
  • خط ۹: نسخهٔ شیء newp1 مستقیماً در پایگاه داده یک واحد افزایش می‌یابد (۴ → ۵). پرس‌وجوهای نوع nativeQuery از زمینه پایداری عبور کرده و مستقیماً به پایگاه داده دسترسی پیدا می‌کنند. در نتیجه، شیء پایدار newp1 و رکورد متناظر آن در پایگاه داده دیگر نسخه یکسانی ندارند.
  • خط ۱۰: پایان تراکنش اول
  • خطوط ۱۳–۱۴: شروع یک تراکنش دوم
  • خط ۱۶: تعداد فرزندان شیء پایدار newp1 به اندازه ۱ افزایش می‌یابد (۶ → ۷).
  • خط ۱۹: پایان تراکنش. بنابراین یک همگام‌سازی انجام می‌شود. این باعث می‌شود که تعداد فرزندان newp1 در پایگاه داده به‌روزرسانی شود. این به‌روزرسانی با شکست مواجه می‌شود زیرا شیء پایدار newp1 نسخه ۴ را دارد، در حالی که شیء مورد نظر برای به‌روزرسانی در پایگاه داده نسخه ۵ را دارد. یک استثنا پرتاب خواهد شد، که این موضوع بلوک try/catch در کد را توجیه می‌کند.
  • خط ۲۱: استثنا و علت آن نمایش داده می‌شود.
  • خط ۲۵: بازگشت تراکنش
  • خط ۳۳: نمایش جدول: باید ببینیم که نسخه newp1 در پایگاه داده ۵ است.

خروجی کنسول برای تست ۱۰ به شرح زیر است:

1
2
3
4
5
6
7
main : ----------- test9
[personnes]
[1,4,Martin,Paul,31/01/2000,false,6]
main : ----------- test10
Erreur lors de la mise à jour de newp1 [javax.persistence.RollbackException,Error while commiting the transaction,org.hibernate.StaleObjectStateException,Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [entites.Personne#1]]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • خط ۵: commit واقعاً یک استثنا پرتاب می‌کند. نوع آن [javax.persistence.RollbackException] است. پیام مرتبط مبهم است. اگر به علت این استثنا (Exception.getCause) نگاه کنیم، می‌بینیم که یک استثنای Hibernate به دلیل تلاش برای تغییر یک سطر در پایگاه داده بدون داشتن نسخه صحیح رخ داده است.
  • خط ۷: می‌بینیم که نسخه newp1 در پایگاه داده واقعاً توسط nativeQuery به ۵ به‌روزرسانی شده است.

2.1.13.12. آزمون ۱۱

کد مربوط به test11 به شرح زیر است:


// برگشت یک تراکنش
    public static void test11() throws ParseException {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = null;
        try {
            tx = em.getTransaction();
            tx.begin();
            // اتصال مجدد p1 به زمینه از طریق بازیابی آن از پایگاه داده
            p1 = em.find(Personne.class, p1.getId());
            //افزایش تعداد فرزندان برای p1
            p1.setNbenfants(p1.getNbenfants() + 1);
            //نمایش افراد – تعداد فرزندان p1 باید تغییر کرده باشد
            System.out.println("[personnes]");
            for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
                System.out.println(p);
            }
            // ایجاد دو شخص با نام یکسان، که توسط DDL ممنوع است
            Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
            // پایداری افراد
            em.persist(p3);
            em.persist(p4);
            //پایان تراکنش
            tx.commit();
        } catch (RuntimeException e1) {
            // یک خطا رخ داده است
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // زمینهٔ فعلی در حال رها شدن است
            em.clear();
        }
        // dump – جدول نباید به دلیل بازگشت به عقب تغییر کرده باشد
        dump();
    }
  • آزمون ۱۱ مکانیزم تراکنش rollback را بررسی می‌کند. یک تراکنش به‌صورت «همه‌یا-هیچ» عمل می‌کند: عملیات‌های SQL که در آن وجود دارد یا همگی با موفقیت اجرا می‌شوند (commit) یا اگر حتی یکی از آن‌ها با شکست مواجه شود، همگی لغو می‌شوند (rollback).
  • خط ۴: ما با همان زمینه پایداری (persistence context) ادامه می‌دهیم. خواننده ممکن است به یاد داشته باشد که این زمینه پس از خرابی در تست قبلی بسته شده بود. در این مورد، [getEntityManager] یک زمینه کاملاً جدید و در نتیجه خالی را بازمی‌گرداند.
  • خطوط ۷–۲۷: یک بلوک try/catch واحد برای رسیدگی به هر مشکلی که ممکن است پیش آید
  • خطوط ۸–۹: شروع یک تراکنش که شامل چندین عملیات SQL خواهد بود
  • خط ۱۱: p1 از پایگاه داده بازیابی شده و در زمینه قرار می‌گیرد
  • خط ۱۳: تعداد فرزندان p1 افزایش می‌یابد (۶ → ۷)
  • خطوط ۱۵–۱۸: محتویات پایگاه داده نمایش داده می‌شوند که این امر باعث همگام‌سازی زمینه (context) می‌شود. در پایگاه داده، تعداد فرزندان p1 به ۷ تغییر خواهد کرد که باید توسط خروجی کنسول تأیید شود.
  • خطوط ۲۰–۲۱: ایجاد دو شخص به نام‌های p3 و p4 با نام یکسان. با این حال، فیلد «name» از @Entity Person دارای ویژگی «unique=true» است که منجر به اعمال محدودیت یکتایی بر ستون «NOM» در جدول «[jpa01_personne]» شده است.
  • خطوط ۲۳–۲۴: افراد p3 و p4 به زمینه پایداری اضافه می‌شوند.
  • خط ۲۶: تراکنش کامیت می‌شود. این کار با یک همگام‌سازی دوم از زمینه دنبال می‌شود، که همگام‌سازی اول در طول تراکنش select انجام شده بود. JPA دو دستور، SQL و insert، را برای افراد p3 و p4 صادر خواهد کرد. p3 درج خواهد شد. برای p4، SGBD یک استثنا پرتاب می‌کند، زیرا p4 نام یکسانی با p3 دارد. بنابراین p4 درج نمی‌شود و درایور JDBC یک استثنا را به کلاینت پرتاب می‌کند.
  • خط ۲۷: استثنا مدیریت می‌شود
  • خطوط ۲۹–۳۱: ما استثناء و دو علت قبلی آن در زنجیره استثناء را که ما را به این نقطه رسانده‌اند، نمایش می‌دهیم.
  • خط ۳۴: ما تراکنش فعال فعلی را رول‌بک می‌کنیم. این تراکنش در خط ۹ کد جاوا آغاز شده بود. از آن زمان، عملیاتی با شناسه update برای تغییر تعداد فرزندان برای p1 انجام شده است، و پس از آن عملیاتی با شناسه insert برای شخص p3 انجام شده است. تمام این موارد توسط رول‌بک معکوس خواهد شد.
  • خط ۳۹: زمینه پایداری پاک می‌شود
  • خط ۴۲: جدول [jpa01_personne] نمایش داده می‌شود. باید بررسی شود که p1 همچنان ۶ فرزند دارد و نه p3 و نه p4 در جدول وجود ندارند.

خروجی کنسول برای آزمون ۱۱ به شرح زیر است:


main : ----------- test11
[personnes]
[1,6,Martin,Paul,31/01/2000,false,7]
14:50:30,312 ERROR JDBCExceptionReporter:72 - Duplicate entry 'X' for key 2
Erreur dans transaction [javax.persistence.EntityExistsException,org.hibernate.exception.ConstraintViolationException: could not insert: [entites.Personne],org.hibernate.exception.ConstraintViolationException,could not insert: [entites.Personne],java.sql.SQLException,Duplicate entry 'X' for key 2]
[personnes]
[1,5,Martin,Paul,31/01/2000,false,6]
  • خط ۳: تعداد فرزندان p1 در پایگاه داده از ۶ به ۷ افزایش یافته است؛ نسخه p1 به ۶ به‌روزرسانی شده است.
  • خط ۴: استثنایی که در حین commit تراکنش گرفته شده است. با بررسی دقیق‌تر، می‌توان دید که علت آن کلید تکراری X (نام) است. این خطا به دلیل درج رکورد p4 رخ داده است، در حالی که رکورد p3 که قبلاً درج شده، نیز دارای نام X است.
  • خط ۷: جدول پس از رول‌بک. p1 به نسخهٔ ۵ بازگشته و ۶ فرزند دارد؛ p3 و p4 درج نشده‌اند.

2.1.13.13. آزمون ۱۲

کد برای test12 به شرح زیر است:


    // ما دوباره همان کار را انجام می‌دهیم اما بدون تراکنش‌ها
    // ما همان نتیجهٔ قبل را با SGBD می‌گیریم: FIREBIRD, ORACLE XE, POSTGRES, MYSQL5
    //با SQLSERVER منجر به یک جدول خالی می‌شود. اتصال در وضعیتی باقی می‌ماند که مانع از اجرای مجدد برنامه می‌شود
    // برنامه. سپس سرور باید مجدداً راه‌اندازی شود.
    //همین امر در مورد SGBD دربی نیز صدق می‌کند
    // HSQL اولین شخص را وارد می‌کند – هیچ بازگشت به عقب (rollback) وجود ندارد

    public static void test12() throws ParseException {
        // p1 دوباره متصل می‌شود
        p1 = em.find(Personne.class, p1.getId());
        // تعداد فرزندان برای p1 را افزایش می‌دهد
        p1.setNbenfants(p1.getNbenfants() + 1);
        //نمایش افراد – تعداد فرزندان برای p1 باید تغییر کرده باشد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // ایجاد دو شخص با نام یکسان، که توسط DDL ممنوع است
        Personne p3 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p4 = new Personne("X", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        // پایداری افراد
        em.persist(p3);
        em.persist(p4);
        // خروجی (dump) که همگام‌سازی زمینه EM را با BD تحریک می‌کند
        try {
            dump();
        } catch (RuntimeException e3) {
            System.out.format("Erreur dans dump [%s,%s,%s,%s]%n", e3.getClass().getName(), e3.getMessage(), e3.getCause().getClass().getName(), e3
                    .getCause().getMessage());
        }
        // زمینهٔ جاری بسته می‌شود
        em.close();
        // dump
        dump();
}
  • آزمون ۱۲ همان فرآیند آزمون ۱۱ را اما خارج از یک تراکنش تکرار می‌کند. ما می‌خواهیم ببینیم در این حالت چه اتفاقی می‌افتد.
  • خطوط ۱–۶: نمایش نتایج آزمون با استفاده از نمونه‌های مختلف SGBD:
  • با چندین SGBD (Firebird، Oracle، MySQL5، Postgres)، به همان نتیجهٔ آزمون 11 می‌رسیم. این امر نشان می‌دهد که این نمونه‌های SGBD به طور خودکار تراکنشی را آغاز کرده‌اند که تمام دستورات SQL دریافت‌شده تا دستورالعملی که باعث خطا شد را پوشش می‌دهد، و خودشان یک rollback را آغاز کرده‌اند.
  • در نمونه‌های دیگر SGBD (سرور SQL، آپاچی دربی)، برنامه و/یا نمونه SGBD از کار می‌افتد.
  • در مورد SGBD و HSQLDB، به نظر می‌رسد که تراکنشی که توسط SGBD باز شده است در حالت autocommit قرار دارد: تغییر در تعداد فرزندان برای p1 و درج p3 دائمی می‌شوند. تنها درج p4 ناموفق است.

بنابراین نتیجه به SGBD وابسته است که باعث غیرقابل حمل بودن برنامه می‌شود. باید توجه داشت که عملیات روی زمینه پایداری (persistence context) باید همیشه در داخل یک تراکنش انجام شود.

2.1.14. تغییر به SGBD

بیایید به معماری تست پروژه فعلی خود بازگردیم:

برنامهٔ کلاینت [3] تنها رابط JPA [5] را می‌بیند. این برنامه نه پیاده‌سازی واقعی این رابط و نه هدف SGBD را می‌بیند. بنابراین باید بتوانیم این دو عنصر را در زنجیره بدون ایجاد هیچ تغییری در کلاینت [3] تغییر دهیم. این همان چیزی است که اکنون در تلاش برای پی بردن به آن هستیم، با شروع از تغییر SGBD. تا به حال، ما از MySQL5 استفاده می‌کردیم. ما شش مورد دیگر را که در ضمیمه‌ها (بند ۵) شرح داده شده‌اند، ارائه می‌دهیم، به این امید که در میان آن‌ها مورد علاقهٔ خواننده، SGBD، باشد.

در هر صورت، تغییری که باید در پروژهٔ Eclipse انجام شود ساده است (به زیر مراجعه کنید): فایل پیکربندی persistence.xml [1] برای لایه JPA را با یکی از فایل‌های موجود در پوشه conf [2] پروژه جایگزین کنید. درایورهای JDBC و SGBD از قبل در کتابخانه‌های [jpa-divers]، [3] و [4] موجود هستند.

2.1.14.1. اوراکل ۱۰g اکسپرس

Oracle 10g Express در ضمیمه‌ها در بخش 5.7 توضیح داده شده است. فایل Oracle با نام persistence.xml به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  ارائه‌دهنده -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
        <properties>
            <!--کلاس‌های پایدار -->
            <property name="hibernate.archive.autodetection" value="class, hbm" />
            <!-- گزارش‌ها SQL
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="use_sql_comments" value="true"/>
            -->
            <!-- اتصال JDBC -->
            <property name="hibernate.connection.driver_class" value="oracle.jdbc.OracleDriver" />
            <property name="hibernate.connection.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
            <!--  ایجاد خودکار طرحواره -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
            <!--  گویش -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.OracleDialect" />
            <!--  ویژگی‌ها DataSource c3p0 -->
            <property name="hibernate.c3p0.min_size" value="5" />
            <property name="hibernate.c3p0.max_size" value="20" />
            <property name="hibernate.c3p0.timeout" value="300" />
            <property name="hibernate.c3p0.max_statements" value="50" />
            <property name="hibernate.c3p0.idle_test_period" value="3000" />
        </properties>
    </persistence-unit>
</persistence>

این پیکربندی با پیکربندی SGBD و MySQL5 یکسان است، با تفاوت‌های جزئی زیر:

  • خطوط ۱۵–۱۸، که اتصال JDBC به پایگاه داده را پیکربندی می‌کنند
  • خط ۲۲: که گویش SQL را که باید استفاده شود مشخص می‌کند

برای مثال‌های بعدی، ما فقط خطوطی را که تغییر می‌کنند مشخص خواهیم کرد. برای توضیح پیکربندی، لطفاً به ضمیمه‌ای که به SGBD مورد استفاده اختصاص دارد، مراجعه کنید. در آنجا در هر مورد، مثالی از نحوه استفاده از اتصال JDBC در زمینه افزونه [SQL Explorer] ارائه شده است. با استفاده از اطلاعات موجود در ضمیمه، خواننده قادر خواهد بود فرآیند تأیید نتیجه اجرای برنامه [InitDB] را که در بند 2.1.10.2 انجام شده است، تکرار کند.

ما طبق آنچه در پاراگراف مذکور آمده است، عمل می‌کنیم:

  • Oracle SGBD را اجرا کنید
  • فایل conf/oracle/persistence.xml را در META-INF/persistence.xml قرار دهید
  • اپلیکیشن [InitDB] را اجرا کنید

نتایج زیر روی کنسول نمایش داده می‌شوند:

از این پس، دیگر این اسکرین‌شات را نمایش نمی‌دهیم، زیرا همیشه یکسان است. نمای Explorer در SQL از ارتباط بین JDBC و SGBD جالب‌تر است. ما رویه توضیح‌داده‌شده در بخش 2.1.8 را دنبال خواهیم کرد.

  • در [1]: اتصال به Oracle
  • به [2]: درخت اتصال پس از اجرای [InitDB]
  • در [3]: ساختار جدول [jpa01_personne]
  • در [4]: محتویات آن.

پس از انجام این کار، از خواننده خواسته می‌شود که برنامه [Main] را اجرا کرده و سپس SGBD را متوقف کند.

2.1.14.2. PostgreSQL 8.2

PostgreSQL نسخهٔ 8.2 در ضمیمه‌ها تحت بند 5.6 ارائه شده است. فایل آن persistence.xml به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!--ورود JDBC -->
            <property name="hibernate.connection.driver_class" value="org.postgresql.Driver" />
            <property name="hibernate.connection.url" value="jdbc:postgresql:jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!--گویش -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect" />
...
    </persistence-unit>
</persistence>

برای اجرای [InitDB]:

  • اجرای SGBD PostgreSQL
  • فایل conf/postgres/persistence.xml را در META-INF/persistence.xml قرار دهید
  • اپلیکیشن [InitDB] را اجرا کنید

نمای SQL Explorer از ارتباط بین JDBC و SGBD به شرح زیر است:

  • در [1]: اتصال با PostgreSQL
  • در [2]: درخت ارتباط پس از اجرای [InitDB]
  • در [3]: ساختار جدول [jpa01_personne]
  • در [4]: محتویات آن.

پس از انجام این کار، از خواننده دعوت می‌شود تا برنامه [Main] را اجرا کرده و سپس SGBD را متوقف سازد.

2.1.14.3. SQL سرور اکسپرس ۲۰۰۵

SQL Server Express 2005 در ضمیمه‌ها، بخش 5.8، در صفحهٔ 270 ارائه شده است. فایل آن persistence.xml به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!-- ورود JDBC -->
            <property name="hibernate.connection.driver_class" value="com.microsoft.sqlserver.jdbc.SQLServerDriver" />
            <property name="hibernate.connection.url" value="jdbc:sqlserver://localhost\\SQLEXPRESS:1433;databaseName=jpa" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- لهجه -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.SQLServerDialect" />
...
    </persistence-unit>
</persistence>

برای اجرای [InitDB]:

  • سرور SGBD SQL را اجرا کنید
  • فایل conf/sqlserver/persistence.xml را در META-INF/persistence.xml قرار دهید
  • برنامه [InitDB] را اجرا کنید

نمای SQL Explorer از ارتباط بین JDBC و SGBD به شرح زیر است:

  • در [1]: اتصال به سرور SQL
  • در [2]: درخت اتصال پس از اجرای [InitDB]
  • در [3]: ساختار جدول [jpa01_personne]
  • در [4]: محتویات آن.

پس از انجام این کار، از خواننده دعوت می‌شود که برنامه [Main] را اجرا کرده و سپس SGBD را متوقف کند.

2.1.14.4. فایربرد ۲.۰

Firebird 2.0 در ضمیمه‌ها در بخش 5.4 توصیف شده است. فایل آن persistence.xml به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!--اتصال JDBC -->
            <property name="hibernate.connection.driver_class" value="org.firebirdsql.jdbc.FBDriver" />
            <property name="hibernate.connection.url" value="jdbc:firebirdsql:localhost/3050:C:\data\2006-2007\eclipse\dvp-jpa\annexes\firebird\jpa.fdb" />
            <property name="hibernate.connection.username" value="sysdba" />
            <property name="hibernate.connection.password" value="masterkey" />
...
            <!-- گویش -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.FirebirdDialect" />
...
    </persistence-unit>
</persistence>

برای اجرای [InitDB]:

  • برنامه Firebird SGBD را اجرا کنید
  • فایل conf/firebird/persistence.xml را در META-INF/persistence.xml قرار دهید
  • برنامه [InitDB] را اجرا کنید

نمای SQL Explorer از پیوند بین JDBC و SGBD به شرح زیر است:

  • در [1]: اتصال به Firebird
  • در [2]: درخت اتصال پس از اجرای [InitDB]
  • در [3]: ساختار جدول [jpa01_personne]
  • در [4]: محتویات آن.

پس از انجام این کار، از خواننده دعوت می‌شود تا برنامه [Main] را اجرا کرده و سپس SGBD را متوقف کند.

2.1.14.5. آپاچی دربی

Apache Derby در ضمیمه‌ها در بخش 5.10 توصیف شده است. فایل آن persistence.xml به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!--اتصال JDBC -->
            <property name="hibernate.connection.driver_class" value="org.apache.derby.jdbc.ClientDriver" />
            <property name="hibernate.connection.url" value="jdbc:derby://localhost:1527//data/2006-2007/eclipse/dvp-jpa/annexes/derby/jpa;create=true" />
            <property name="hibernate.connection.username" value="jpa" />
            <property name="hibernate.connection.password" value="jpa" />
...
            <!-- گویش -->
...
    </persistence-unit>
</persistence>

برای اجرای [InitDB]:

  • SGBD Apache Derby را اجرا کنید
  • فایل conf/derby/persistence.xml را در META-INF/persistence.xml قرار دهید
  • برنامه [InitDB] را اجرا کنید

نمای SQL Explorer از پیوند بین JDBC و SGBD به شرح زیر است:

  • در [1]: اتصال به Apache Derby
  • در [2]: درخت اتصال پس از اجرای [InitDB]. به جدول [HIBERNATE_UNIQUE_KEY] توجه کنید که توسط JPA / Hibernate ایجاد شده است تا مقادیر متوالی را به طور خودکار برای کلید اصلی ID تولید کند. ما قبلاً اشاره کرده‌ایم که این مکانیزم اغلب اختصاصی است. این موضوع در اینجا کاملاً مشهود است. به لطف JPA، توسعه‌دهنده نیازی به بررسی جزئیات SGBD ندارد.
  • در [3]: ساختار جدول [jpa01_personne]
  • در [4]: محتویات آن.

پس از انجام این کار، از خواننده خواسته می‌شود برنامه [Main] را اجرا کرده و سپس SGBD را متوقف کند.

2.1.14.6. HSQLDB

HSQLDB در ضمیمه‌ها تحت بند 5.9 ارائه شده است. فایل آن persistence.xml به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
...
            <!--اتصال JDBC -->
            <property name="hibernate.connection.driver_class" value="org.hsqldb.jdbcDriver" />
            <property name="hibernate.connection.url" value="jdbc:hsqldb:hsql://localhost" />
            <property name="hibernate.connection.username" value="sa" />
            <!-- 
                <property name="hibernate.connection.password" value="" />
            -->
...
            <!--  گویش -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
...
        </properties>
    </persistence-unit>
</persistence>

برای اجرای [InitDB]:

  • SGBD HSQL را اجرا کنید
  • فایل conf/hsql/persistence.xml را در META-INF/persistence.xml قرار دهید
  • برنامه [InitDB] را اجرا کنید

نمای SQL Explorer از ارتباط بین JDBC و SGBD به شرح زیر است:

  • در [1]: اتصال با HSQL
  • در [2]: درخت ارتباط پس از اجرای [InitDB].
  • در [3]: ساختار جدول [jpa01_personne]
  • در [4]: محتویات آن.

پس از انجام این کار، از خواننده خواسته می‌شود برنامه [Main] را اجرا کرده و سپس SGBD را متوقف کند.

2.1.15. تغییر پیاده‌سازی به JPA

بیایید بار دیگر به معماری تست پروژه فعلی‌مان نگاهی بیندازیم:

مطالعه قبلی نشان داد که ما توانستیم SGBD را با [7] بدون ایجاد هیچ تغییری در کد کلاینت [3] تعویض کنیم. اکنون پیاده‌سازی JPA [6] را تغییر می‌دهیم و بار دیگر نشان می‌دهیم که این کار می‌تواند به‌صورت شفاف برای کد مشتری [3] انجام شود. یک پیاده‌سازی از TopLink و [http://www.oracle.com/technology/products/ias/toplink/jpa/index.html] را در نظر می‌گیریم:

2.1.15.1. پروژه اکلیپس

برای نشان دادن تغییر در پیاده‌سازی JPA، ما یک پروژه جدید اکلیپس ایجاد می‌کنیم تا پروژه موجود شلوغ نشود. دلیل این کار آن است که پروژه جدید از کتابخانه‌های پایداری استفاده می‌کند که ممکن است با کتابخانه‌های Hibernate تداخل داشته باشند:

  • در [1]: پوشه [<exemples>/toplink/direct/personnes-entites] حاوی پروژه Eclipse است. این پروژه را وارد کنید.
  • به [2]: پروژه واردشده [toplink-personnes-entites]. این پروژه، به جز دو مورد جزئی، با پروژه [hibernate-personne-entites] یکسان است (از آن کپی شده است):
    • فایل [META-INF/persistence.xml] [3] اکنون یک لایه JPA / Toplink را پیکربندی می‌کند
    • کتابخانه [jpa-hibernate] با کتابخانه‌های [jpa-toplink]، [4] و [5] جایگزین شده است (به بند 1.5 مراجعه کنید).
  • در [6]: پوشه [conf] شامل نسخه‌ای از فایل [persistence.xml] برای هر SGBD است.
  • در [7]: پوشه [ddl] که شامل اسکریپت‌های SQL برای تولید طرحواره پایگاه داده است.

ما می‌دانیم که لایه JPA توسط فایل [META-INF/persistence.xml] پیکربندی می‌شود. این فایل اکنون پیاده‌سازی JPA / Toplink را پیکربندی می‌کند. محتوای آن برای لایه JPA که با SGBD و MySQL5 رابطه‌مند است، به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  ارائه‌دهنده -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- کلاس‌های پایدار -->
        <class>entites.Personne</class>
        <!-- ویژگی‌های واحد پایداری -->
        <properties>
            <!-- اتصال JDBC -->
            <property name="toplink.jdbc.driver" value="com.mysql.jdbc.Driver" />
            <property name="toplink.jdbc.url" value="jdbc:mysql://localhost:3306/jpa" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="MySQL4" />
            <!--  سرور برنامه -->
            <property name="toplink.target-server" value="None" />
            <!--تولید طرحواره -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- لاگ‌ها -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>
  • خط ۳: بدون تغییر
  • خط ۵: ارائه‌دهنده اکنون Toplink است. کلاس نام‌برده در اینجا در کتابخانه [jpa-toplink] ([1] در زیر) یافت می‌شود:
  • خط ۷: تگ <class> برای فهرست کردن تمام کلاس‌های @Entity در پروژه استفاده می‌شود؛ در اینجا، فقط کلاس Personne. Hibernate قبلاً یک گزینه پیکربندی داشت که به این معنی بود که ما مجبور نبودیم این کلاس‌ها را فهرست کنیم. این گزینه، classpath پروژه را برای یافتن کلاس‌های @Entity اسکن می‌کرد.
  • خط ۹: تگ <properties> که ویژگی‌های خاص پیاده‌سازی JPA را معرفی می‌کند، در این مورد TopLink.
  • خطوط ۱۱–۱۴: پیکربندی اتصال JDBC با SGBD و MySQL5
  • خطوط ۱۵–۱۸: پیکربندی استخر اتصالات JDBC که به‌طور بومی توسط Toplink مدیریت می‌شود:
  • خطوط ۱۵ و ۱۶: حداکثر و حداقل تعداد اتصالات در استخر اتصالات خواندن. پیش‌فرض (۲، ۲)
  • خطوط 17 و 18: حداکثر و حداقل تعداد اتصالات در استخر اتصالات نوشتن. پیش‌فرض (10, 2)
  • خط ۲۰: SGBD هدف. فهرست SGBDهای قابل استفاده در بسته [oracle.toplink.essentials.platform.database] موجود است (به [2] بالا مراجعه کنید). SGBD در لیست [2] وجود ندارد، بنابراین MySQL4 انتخاب شد. Toplink از تعداد کمی SGBD کمتر از Hibernate پشتیبانی می‌کند. بنابراین، از میان هفت SGBD مورد استفاده در مثال‌های ما، Firebird پشتیبانی نمی‌شود. همچنین Oracle در این فهرست یافت نمی‌شود. در واقع این تگ در یک بستهٔ متفاوت ([3] بالا) قرار دارد. اگر در این دو بسته، هدف SGBD توسط کلاس <Sgbd>Platform.class مشخص شود، تگ به صورت زیر نوشته خواهد شد:

            <property name="toplink.target-database" value="<Sgbd>" />
  • خط ۲۲: سرور برنامه را تنظیم می‌کند اگر برنامه روی چنین سروری در حال اجرا باشد. مقادیر ممکن فعلی (None, OC4J_10_1_3, SunAS9). پیش‌فرض (None).
  • خطوط ۲۴–۲۸: وقتی لایه JPA инициалиزه می‌شود، به آن دستور داده می‌شود تا پایگاه داده تعریف‌شده توسط اتصال JDBC در خطوط ۱۱–۱۴ را پاک کند. این کار تضمین می‌کند که با یک پایگاه داده خالی شروع کنیم.
    • خط ۲۴: به TopLink دستور داده می‌شود که ابتدا یک drop و سپس یک create را روی جداول در طرح‌بندی پایگاه داده اجرا کند.
    • خط ۲۵: ما به TopLink دستور می‌دهیم تا اسکریپت‌های SQL را برای عملیات drop و create تولید کند. application-location پوشه‌ای را مشخص می‌کند که این اسکریپت‌ها در آن تولید خواهند شد. پیش‌فرض: (پوشهٔ جاری).
    • خط ۲۶: نام اسکریپت SQL برای عملیات create.. پیش‌فرض: createDDL.jdbc.
    • خط ۲۷: نام اسکریپت SQL برای عملیات drop.. پیش‌فرض: dropDDL.jdbc.
    • خط ۲۸: حالت تولید طرح‌واره (پیش‌فرض: both):
      • both: اسکریپت‌ها و پایگاه داده
      • database: فقط پایگاه داده
      • sql-script: فقط اسکریپت‌ها
  • خط ۳۰: لاگ‌های Toplink غیرفعال هستند (OFF). سطوح ورود به سیستم مختلف موجود به شرح زیر هستند: OFF, SEVERE, WARNING, INFO, CONFIG, FINE, FINER, FINEST. پیش‌فرض: INFO.

لطفاً برای تعریف جامع تگ‌های <property> که می‌توان با Toplink استفاده کرد، به URL [http://www.oracle.com/technology/products/ias/toplink/JPA/essentials/toplink-jpa-extensions.html] مراجعه کنید.

2.1.15.3. آزمایش [InitDB]

کار دیگری باقی نمانده است. ما برای اجرای اولین تست، [InitDB] آماده هستیم:

  • اجرای SGBD، در اینجا MySQL5
  • اجرای [InitDB]
  • در [1]: خروجی کنسول. ما نتایج حاصل‌شده با JPA / Hibernate را مشاهده می‌کنیم.
  • در [3]: نمای [SQL Explorer] را باز کنید، سپس اتصال [mysql5-jpa] را باز کنید
  • در [4]: درخت پایگاه داده JPA. می‌توانیم ببینیم که اجرای [InitDB] دو جدول ایجاد کرد: [jpa01_personne] که مورد انتظار بود، و جدول [sequence] که کمتر مورد انتظار بود.
  • در [5]: ساختار جدول [jpa01_personne] و در [6] محتویات آن
  • در [7]: ساختار جدول [sequence] و در [8] محتویات آن.

فایل پیکربندی [persistence.xml] تولید اسکریپت‌ها از DDL را درخواست کرد:


            <!--  تولید طرحواره -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/mysql5" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
<property name="toplink.ddl-generation.output-mode" value="both" />

بیایید نگاهی بیندازیم به آنچه در پوشه [ddl/mysql5] تولید شده است:

 

create.sql


CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • خط ۱: DDL از جدول [jpa01_personne]. مشاهده می‌شود که Toplink از ویژگی autoincrement برای کلید اصلی ID استفاده نکرده است. در نتیجه، این کلید هنگام درج ردیف‌ها به‌طور خودکار افزایش نمی‌یابد.
  • خط ۲: DDL از جدول [sequence]. به نظر می‌رسد نام آن نشان می‌دهد که Toplink از این جدول برای تولید مقادیر کلید اصلی ID استفاده می‌کند.
  • ردیف ۳: درج یک ردیف در [SEQUENCE]

drop.sql


DROP TABLE jpa01_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
  • ردیف ۱: حذف جدول [jpa01_personne]
  • خط ۲: حذف یک ردیف خاص از جدول [SEQUENCE]. خود جدول حذف نمی‌شود و هیچ‌یک از ردیف‌های دیگر آن نیز حذف نمی‌شوند.

برای کسب اطلاعات بیشتر درباره نقش جدول [SEQUENCE]، لاگ‌های Toplink را در [persistence.xml] در سطح FINE فعال کنید، سطحی که دستورات SQL صادرشده توسط Toplink را ثبت می‌کند:


            <!-- لاگ‌ها -->
<property name="toplink.logging.level" value="FINE" />

ما InitDB را مجدداً اجرا می‌کنیم. در زیر، تنها نمای جزئی از خروجی کنسول را آورده‌ایم:


...
[TopLink Config]: 2007.05.28 12:07:52.796--ServerSession(12910198)--اتصال(30708295)--رشته(رشته[main,5,main])--اتصال: jdbc:mysql://localhost:3306/jpa
    User: jpa@localhost
    Database: MySQL  Version: 5.0.37-community-nt
    Driver: MySQL-AB JDBC Driver  Version: mysql-connector-java-3.1.9 ( $Date: 2005/05/19 15:52:23 $, $Revision: 1.1.2.2 $ )
...
[TopLink Fine]: 2007.05.28 12:07:53.093--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.265--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (IJ))
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(۵۰) NOT NULL, SEQ_COUNT DECIMAL(۳۸), PRIMARY KEY (SEQ_NAME))
[TopLink Warning]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Thread(Thread[main,5,main])--استثناء [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (ساخت b41-بتا2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Table 'sequence' already exists
Error Code: 1050
Call: CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
Query: DataModifyQuery()
[TopLink Fine]: 2007.05.28 12:07:53.468--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connection(19255406)--Thread(Thread[main,5,main])--SELECT * FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'
[TopLink Fine]: 2007.05.28 12:07:53.609--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) مقادیر ('SEQ_GEN', 1)
[TopLink Fine]: 2007.05.28 12:07:53.734--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--حذف از jpa01_personne
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--UPDATE SEQUENCE SET SEQ_COUNT = SEQ_COUNT + ؟ WHERE SEQ_NAME = ؟
    bind => [50, SEQ_GEN]
[TopLink Fine]: 2007.05.28 12:07:53.750--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ؟
    bind => [SEQ_GEN]
[personnes]
[TopLink Fine]: 2007.05.28 12:07:53.906--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?,?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Fine]: 2007.05.28 12:07:53.921--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?,?)
    bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
[TopLink Fine]: 2007.05.28 12:07:53.937--ClientSession(15308417)--Connection(14069849)--Thread(Thread[main,5,main])--SELECT QZXW2HTMLCSuqZQX, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS FROM jpa01_personne ORDER BY NOM ASC
[3,1,Durant,Sylvie,05/07/2001,false,0]
[2,1,Martin,Paul,31/01/2000,true,2]
[TopLink Config]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Connection(30708295)--Thread(Thread[main,5,main])--disconnect
[TopLink Info]: 2007.05.28 12:07:54.062--ServerSession(12910198)--Thread(Thread[main,5,main])--file:/C:/data/2006-2007/eclipse/dvp-jpa/toplink/direct/personnes-entites/bin/-jpa خروج با موفقیت انجام شد
...
terminé ...
  • خطوط ۲–۵: یک اتصال به SGBD با پارامترهای آن. در واقع، لاگ‌ها نشان می‌دهند که Toplink در عمل سه اتصال به SGBD ایجاد می‌کند. ارزش دارد بررسی شود که آیا این تعداد با یکی از مقادیر پیکربندی استخر اتصال JDBC مرتبط است یا خیر:

            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
<property name="toplink.jdbc.write-connections.min" value="2" />
  • خط ۷: حذف جدول [jpa01_personne]. این امر قابل انتظار است، زیرا فایل [persistence.xml] درخواست می‌کند که پایگاه داده JPA پاک شود.
  • خط ۸: ایجاد جدول [jpa01_personne]. می‌بینیم که کلید اصلی ID فاقد ویژگی autoincrement است.
  • خط ۹: ایجاد جدول [SEQUENCE] که قبلاً در اجرای قبلی ایجاد شده و موجود است.
  • خطوط ۱۰–۱۳: Toplink هنگام ایجاد جدول [SEQUENCE] خطایی را گزارش می‌کند.
  • خطوط ۱۵–۱۸: Toplink جدول [SEQUENCE] را پاکسازی می‌کند. پس از این پاکسازی، جدول [SEQUENCE] شامل یک سطر (SEQ_NAME, SEQ_COUNT) با مقادیر ('SEQ_GEN', 1) است.
  • خط ۱۸: جدول [jpa01_personne] خالی می‌شود.
  • خطوط ۱۹–۲۰: Toplink یک ردیف واحد را که در جدول [SEQUENCE]، با شرط SEQ_NAME = 'SEQ_GEN' مطابقت دارد، از روی مقدار ('SEQ_GEN', 1) به مقدار ('SEQ_GEN', 51)
  • خط ۲۱: Toplink مقدار ۵۱ را از سطر ('SEQ_GEN', 51) در جدول [SEQUENCE] بازیابی می‌کند.
  • خطوط ۲۴–۲۷: Toplink دو شخص «Martin» و «Durant» را در جدول [jpa01_personne] وارد می‌کند. در اینجا یک معما وجود دارد: کلیدهای اصلی این دو سطر مقادیر ۲ و ۳ را دریافت کرده‌اند، بدون اینکه روشن باشد این مقادیر چگونه به دست آمده‌اند. مشخص نیست که آیا مقدار SEQ_COUNT (۵۱) که در خط ۲۱ به دست آمده است، هیچ کاربردی داشته است یا خیر. توجه داشته باشید که مقدار نسخه برای این ردیف‌ها ۱ است، در حالی که Hibernate از ۰ شروع می‌کند.
  • خط ۲۸: TopLink مقدار SELECT را برای بازیابی تمام سطرها از جدول [jpa01_personne] تولید می‌کند.
  • رده‌های ۲۹–۳۰: ردیف‌های نمایش‌داده‌شده توسط کلاینت جاوا
  • خطوط ۳۱–۳۲: TopLink یک اتصال را می‌بندد. این عملیات را برای هر یک از اتصالات بازشده در ابتدا تکرار خواهد کرد.

در نهایت، نقش دقیق جدول [SEQUENCE] نامشخص است، اما به نظر می‌رسد که در تولید مقادیر کلید اصلی ID نقشی دارد. با تنظیم سطح لاگ به دقیق‌ترین مقدار، یعنی FINEST، اطلاعات بیشتری درباره نقش جدول [SEQUENCE] به دست می‌آوریم.


            <!-- لاگ‌ها -->
            <property name="toplink.logging.level" value="FINEST" />

در زیر، تنها لاگ‌های مربوط به درج دو فرد در جدول را آورده‌ایم. در اینجا می‌توانیم مکانیزم تولید مقادیر کلید اصلی را ببینیم:

[TopLink Finest]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Thread(Thread[main,5,main])--اجرای پرس‌وجو ValueReadQuery()
[TopLink Fine]: 2007.05.28 03:05:04.046--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--SELECT SEQ_COUNT FROM SEQUENCE WHERE SEQ_NAME = ؟
    bind => [SEQ_GEN]
[TopLink Finest]: 2007.05.28 03:05:04.062--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--واگذاری مقدماتی توالی محلی برای SEQ_GEN: اشیاء: 50, اول: 2, آخر: 51
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--رشته(رشته[main,5,main])--واگذاری توالی به شیء (2 -> [null,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--اجرای پرس‌وجو DoesExistQuery()
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--PERSIST عملیاتی که روی: [null,0,Durant,Sylvie,05/07/2001,false,0] فراخوانی شده است.
[TopLink Finest]: 2007.05.28 03:05:04.062--UnitOfWork(19864560)--Thread(Thread[main,5,main])--assign sequence to the object (3 -> [null,0,Durant,Sylvie,05/07/2001,false,0])
[personnes]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--اجرای پرس‌وجو InsertObjectQuery([3,0,Durant,Sylvie,05/07/2001,false,0])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--رشته(رشته [main,5,main])-- تخصیص سطر بازگشت DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?,?)
    bind => [3, Sylvie, 2001-07-05, Durant, false, 1, 0]
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--Thread(Thread[main,5,main])--اجرای پرس‌وجو InsertObjectQuery([2,0,Martin,Paul,31/01/2000,true,2])
[TopLink Finest]: 2007.05.28 03:05:04.203--UnitOfWork(19864560)--رشته(رشته [main,5,main])-- تخصیص سطر بازگشت DatabaseRecord(
    jpa01_personne.VERSION => 1)
[TopLink Fine]: 2007.05.28 03:05:04.203--ClientSession(30617157)--Connection(13301441)--Thread(Thread[main,5,main])--INSERT INTO jpa01_personne (ID, PRENOM, DATENAISSANCE, NOM, MARIE, VERSION, NBENFANTS) VALUES (?, ?, ?, ?, ?, ?,?)
bind => [2, Paul, 2000-01-31, Martin, true, 1, 2]
  • ردیف ۴: می‌توانیم ببینیم که عدد ۵۱ استخراج‌شده از جدول [SEQUENCE] در ردیف ۲ برای تعریف محدوده‌ای از مقادیر برای کلید اصلی استفاده می‌شود: [2,51]
  • خط ۵: برای شخص اول مقدار ۲ به‌عنوان کلید اصلی اختصاص داده شده است
  • خط ۸: برای شخص دوم مقدار ۳ به‌عنوان کلید اصلی اختصاص داده شده است
  • خط ۱۲: مدیریت نسخه را برای شخص اول نشان می‌دهد
  • ردیف ۱۷: همین امر برای شخص دوم نیز صدق می‌کند

سطح لاگ [FINEST] همچنین مرزهای تراکنش‌های صادرشده توسط Toplink را نشان می‌دهد. تحلیل این لاگ‌ها آشکار می‌کند که Toplink چه می‌کند و راهی عالی برای درک پل شیء-رابطه‌ای است.

نکات کلیدی از موارد فوق:

  • که پیاده‌سازی‌های مختلف JPA اسکیماهای پایگاه‌داده متفاوتی تولید می‌کنند. در این مثال، Hibernate و Toplink اسکیماهای یکسانی تولید نکردند.
  • که سطوح لاگ Toplink FINE، FINER و FINEST باید هر زمان که نیاز به توضیح دقیق در مورد عملکرد Toplink باشد، استفاده شوند.

2.1.15.4. آزمون [Main]

ما اکنون در حال اجرای تست [Main] هستیم:

  • در [1]: تمام تست‌ها به جز تست ۱۱، [2]، با موفقیت انجام شدند
  • در [3]: خط ۳۷۶، خط کدی که استثنا در آن رخ داده است

کدی که خطا را ایجاد می‌کند به شرح زیر است:


} catch (RuntimeException e1) {
            //ما با مشکلی مواجه شدیم
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,%s,%s]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage(), e1.getCause().getCause().getClass().getName(), e1.getCause().getCause()
                            .getMessage());
            try {
            ...
  • خط [3]: خطی که در آن خطا رخ داده است. ما یک NullPointerException داریم، که نشان می‌دهد یکی از متدهای getCause در خطوط ۴ و ۵ یک نشانگر null را بازگردانده است. یک عبارت مانند [e1.getCause().getCause()] فرض می‌کند که زنجیره استثنا سه عنصر دارد: [e1.getCause().getCause(), e1.getCause(), e1]. اگر فقط دو عنصر داشته باشد، عبارت اول باعث ایجاد استثنا خواهد شد.

ما کد قبلی را تغییر می‌دهیم تا تنها دو استثنای آخر در زنجیرهٔ استثناها نمایش داده شود:


        } catch (RuntimeException e1) {
            //ما با مشکلی مواجه شدیم
            System.out.format("Erreur dans transaction [%s,%s,%s,%s,]%n", e1.getClass().getName(), e1.getMessage(),
                    e1.getCause().getClass().getName(), e1.getCause().getMessage());
            try {
...

هنگام اجرا، این نتیجه زیر را تولید می‌کند:


...
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]
main : ----------- test11
[personnes]
Erreur dans transaction [javax.persistence.OptimisticLockException,Exception [TOPLINK-5006] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.OptimisticLockException
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],oracle.toplink.essentials.exceptions.OptimisticLockException,
Exception Description: The object [[2,6,Martin,Paul,31/01/2000,false,7]] cannot be updated because it has changed or been deleted since it was last read. 
Class> entites.Personne Primary Key> [2],]
[personnes]
[2,5,Martin,Paul,31/01/2000,false,6]

این بار، تست ۱۱ با موفقیت اجرا می‌شود. خروجی استثنا (خطوط ۶–۱۰) توسط کد جاوا (خط ۳ کد بالا) ایجاد شده است. باید توجه داشت که تست ۱۱ در یک تراکنش واحد چندین عملیات SQL را به صورت زنجیره‌ای به هم متصل کرده بود که یکی از آن‌ها با شکست مواجه شد و انتظار می‌رفت باعث رول‌بک تراکنش شود. وضعیت جدول [jpa01_personne] قبل از تست (خط ۳) و بعد از آن (خط ۱۲) در واقع یکسان است، که نشان می‌دهد رول‌بک انجام شده است.

شایان ذکر است که یک نکته مهم: پیاده‌سازی‌های JPA / Hibernate و JPA / Toplink صددرصد قابل تعویض نیستند. در این مثال، برای جلوگیری از NullPointerException، باید کد کلاینت JPA را تغییر دهیم. ما بعداً دوباره با این مشکل مواجه خواهیم شد، این بار در زمینه یک استثنا.

بیایید نگاهی دیگر به معماری تست برای پروژه فعلی خود بیندازیم:

قبلاً، SGBD که در [7] استفاده می‌شد، MySQL5 بود. ما با استفاده از Oracle نشان خواهیم داد که چگونه از SGBD تغییر دهیم. در هر صورت، تغییری که باید در پروژه Eclipse انجام شود ساده است (به زیر مراجعه کنید): فایل پیکربندی persistence.xml را برای لایه [1] با یکی از فایل‌های موجود در پوشه conf جایگزین کنید ([2] و [3]) در پروژه.

2.1.16.1. اوراکل ۱۰g اکسپرس

Oracle 10g Express در ضمیمه‌ها در بخش 5.7 توضیح داده شده است. فایل Oracle persistence.xml برای Toplink به شرح زیر است:


<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0" xmlns="http://java.sun.com/xml/ns/persistence">
    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  ارائه‌دهنده -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- کلاس‌های پایدار -->
        <class>entites.Personne</class>
        <!-- ویژگی‌های واحد پایداری -->
        <properties>
            <!--اتصال JDBC -->
            <property name="toplink.jdbc.driver" value="oracle.jdbc.OracleDriver" />
            <property name="toplink.jdbc.url" value="jdbc:oracle:thin:@localhost:1521:xe" />
            <property name="toplink.jdbc.user" value="jpa" />
            <property name="toplink.jdbc.password" value="jpa" />
            <property name="toplink.jdbc.read-connections.max" value="3" />
            <property name="toplink.jdbc.read-connections.min" value="1" />
            <property name="toplink.jdbc.write-connections.max" value="5" />
            <property name="toplink.jdbc.write-connections.min" value="2" />
            <!-- SGBD -->
            <property name="toplink.target-database" value="Oracle" />
            <!--  سرور برنامه -->
            <property name="toplink.target-server" value="None" />
            <!--تولید طرحواره -->
            <property name="toplink.ddl-generation" value="drop-and-create-tables" />
            <property name="toplink.application-location" value="ddl/oracle" />
            <property name="toplink.create-ddl-jdbc-file-name" value="create.sql" />
            <property name="toplink.drop-ddl-jdbc-file-name" value="drop.sql" />
            <property name="toplink.ddl-generation.output-mode" value="both" />
            <!-- لاگ‌ها -->
            <property name="toplink.logging.level" value="OFF" />
        </properties>
    </persistence-unit>
</persistence>

این پیکربندی با پیکربندی SGBD و MySQL5 یکسان است، با تفاوت‌های جزئی زیر:

  • خطوط ۱۱–۱۴، که اتصال JDBC به پایگاه داده را پیکربندی می‌کنند
  • خط ۲۰: که هدف SGBD را مشخص می‌کند
  • خط ۲۵: که پوشهٔ تولید اسکریپت‌های SQL از DDL را مشخص می‌کند

برای اجرای تست [InitDB]:

  • Oracle SGBD را اجرا کنید
  • فایل conf/oracle/persistence.xml را در META-INF/persistence.xml قرار دهید
  • برنامه [InitDB] را اجرا کنید

نتایج زیر در کنسول و در نمای [SQL Explorer] نمایش داده می‌شوند:

  • [1]: خروجی کنسول
  • [2]: اتصال [oracle-jpa] در SQL Explorer
  • [3]: پایگاه داده jpa
  • [4]: InitDB دو جدول به نام‌های JPA01_PERSONNE و SEQUENCE ایجاد کرده است، همانند MySQL5. گاهی در [4]، جداول با نام [BIN*] ظاهر می‌شوند. این جداول مربوط به جداول حذف‌شده هستند. برای مشاهده این پدیده، کافی است [InitDB] را مجدداً اجرا کنید. مرحلهٔ راه‌اندازی لایهٔ JPA شامل پاک‌سازی پایگاه‌دادهٔ jpa است که در طی آن جدول [JPA01_PERSONNE] حذف می‌شود:

در [A]، جدولی با نام [BIN] ظاهر می‌شود. Oracle یک جدول را که عملیات drop روی آن انجام شده است، به‌طور دائم حذف نمی‌کند، بلکه آن را در سطل بازیافتی به نام [Recycle Bin] قرار می‌دهد. این سطل بازیافت با نام [B] از طریق ابزار توسعه‌دهنده SQL که در بخش 5.7.4 توضیح داده شده است، قابل مشاهده است. در [B]، می‌توانید جدول [JPA01_PERSONNE] را که در سطل بازیافت قرار دارد، پاک کنید. این کار سطل بازیافت [C] را خالی می‌کند. اگر در SQL Explorer جداول را تازه‌سازی کنید (کلیک راست / Refresh)، خواهید دید که جدول BIN دیگر وجود ندارد ([D]).

  • [5, 6]: ساختار و محتوای جدول [JPA01_PERSONNE]
  • [7, 8]: ساختار و محتویات جدول [SEQUENCE]

خب، این هم از این! اکنون از خواننده دعوت می‌شود تا برنامه [Main] را روی Oracle اجرا کند.

2.1.16.2. سایر SGBD

درباره سایر فایل‌های SGBD کمتر صحبت خواهیم کرد. شما کافی است همان رویه‌ای را که برای Oracle انجام دادید دنبال کنید. به نکات زیر توجه کنید:

  • صرف‌نظر از SGBD، Toplink همیشه از همان تکنیک برای تولید مقادیر کلید اصلی ID برای جدول [JPA01_PERSONNE] استفاده می‌کند: این تکنیک، استفاده از جدول [SEQUENCE] است که در بالا توضیح داده شد.
  • Toplink موتور Firebird SGBD را شناسایی نمی‌کند. برای چنین مواردی یک پایگاه داده عمومی وجود دارد:
                <property name="toplink.target-database" value="Auto" />

با این پایگاه عمومی با نام [Auto]، تست‌ها با Firebird به‌دلیل خطاهای نحوی SQL شکست می‌خورند. Toplink از نوع ID برای کلید اصلی استفاده می‌کند که از نوع SQL Number(10) است، نوعی که توسط Firebird شناسایی نمی‌شود. بنابراین باید یک SGBD با انواع مشابه SQL در Firebird (برای این مثال) انتخاب کنید. این مورد در Apache Derby نیز صادق است:


            <!-- اتصال JDBC -->
            <property name="toplink.jdbc.driver" value="org.firebirdsql.jdbc.FBDriver" />
...
            <!-- SGBD -->
            <!-- 
            TopLink ne reconnaît pas Firebird pour l'instant (05/07). Derby convient pour remplacer.
            -->
            <property name="toplink.target-database" value="Derby" />
...
  • Toplink قادر به تولید طرحواره پایگاه داده اصلی برای SGBD HSQLDB نیست. به عبارت دیگر، دستور:

            <!--  تولید اسکیما -->
<property name="toplink.ddl-generation" value="drop-and-create-tables" />

برای HSQLDB شکست می‌خورد. علت آن یک خطای نحوی هنگام ایجاد جدول [jpa01_personne] است:


[TopLink Fine]: 2007.05.29 09:44:18.515--ServerSession(12910198)--Connection(29775659)--Thread(Thread[main,5,main])--DROP TABLE jpa01_personne
[TopLink Fine]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Connection(29775659)--Thread(Thread[main,5,main])--CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(۳۰) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, MARIE TINYINT NOT NULL, VERSION INTEGER NOT NULL, NBENFANTS INTEGER NOT NULL, PRIMARY KEY (IJ))
[TopLink Warning]: 2007.05.29 09:44:18.531--ServerSession(12910198)--Thread(Thread[main,5,main])--استثناء [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (30/03/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: java.sql.SQLException: Unexpected token: UNIQUE in statement [CREATE TABLE jpa01_personne (ID INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, NOM VARCHAR(30) UNIQUE]

خط ۴، نحوی NOM VARCHAR(30) UNIQUE NOT NULL توسط HSQL پذیرفته نمی‌شود. Hibernate از سینتکس زیر استفاده کرده بود: NOM VARCHAR(30) NOT NULL, UNIQUE(NOM).

به‌طور کلی، Hibernate در شناسایی فایل‌های SGBD مورد استفاده در آزمایش‌های توصیف‌شده در این سند، مؤثرتر از Toplink بود.

2.1.17. نتیجه‌گیری

مطالعه @Entity [Personne] در اینجا به پایان می‌رسد. از دیدگاه مفهومی، مطالب بسیار کمی پوشش داده شده است: ما پل شیء-رابطه‌ای را در ساده‌ترین شکل آن بررسی کرده‌ایم: یک شیء @Entity <--> یک جدول. با این حال، این بررسی به ما امکان داده است تا ابزارهایی را که در سرتاسر این سند از آنها استفاده خواهیم کرد، معرفی کنیم. این امر به ما اجازه می‌دهد تا از این پس، با بررسی سایر موارد پل شیء-رابطه‌ای که مطالعه خواهیم کرد، کمی سریع‌تر پیش برویم:

  • به @Entity قبلی [Personne]، یک فیلد adresse را که با کلاسی به نام [Adresse] مدل شده است، اضافه خواهیم کرد. در سمت پایگاه داده، به دو پیاده‌سازی ممکن نگاه خواهیم کرد. اشیاء [Personne] و [Adresse] منجر به
  • یک جدول واحد، [personne]، حاوی آدرس
  • دو جدول، [personne] و [adresse]، که توسط یک رابطه کلید خارجی یک‌به‌یک به هم متصل شده‌اند.
  • یک مثال از رابطه یک‌به‌چند که در آن جدول [article] از طریق یک کلید خارجی به جدول [categorie] متصل است
  • یک مثال از رابطه چند به چند که در آن دو جدول، [personne] و [activite]، از طریق یک جدول پیوندی به نام [personne_activite] به هم متصل شده‌اند.

2.2. مثال ۲: رابطه یک‌به‌یک از طریق یک شمول

2.2.1. شمای پایگاه داده

 
1
2

    drop table if exists jpa02_personne;

    create table jpa02_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

  • به [1]: پایگاه داده (افزونه Azurri Clay)
  • در [2]: DDL تولیدشده توسط Hibernate برای MySQL5

جدول [jpa02_personne] همان جدول [jpa01_personne] است که قبلاً بررسی شد و یک آدرس به آن اضافه شده است (خطوط ۱۲–۱۸ از DDL).

2.2.2. ابجکت‌های @Entity که نمایندهٔ پایگاه داده هستند

آدرس یک شخص توسط کلاس زیر [Adresse] نمایش داده می‌شود:


package entites;

...
@SuppressWarnings("serial")
@Embeddable
public class Adresse implements Serializable {

    // fields
    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    // سازنده‌ها
    public Adresse() {

    }

    public Adresse(String adr1, String adr2, String adr3, String codePostal, String ville, String cedex, String pays) {
...
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...

    //toString
    public String toString() {
        return String.format("A[%s,%s,%s,%s,%s,%s,%s]", getAdr1(), getAdr2(), getAdr3(), getCodePostal(), getVille(), getCedex(), getPays());
    }
}
  • نوآوری اصلی در anotation @Embeddable در خط ۵ نهفته است. کلاس [Adresse] برای ایجاد جدول در نظر گرفته نشده است، بنابراین anotation @Entity را ندارد. آناوتیشن @Embeddable نشان می‌دهد که این کلاس برای جاسازی شدن در داخل یک شیء @Entity و در نتیجه در جدول مرتبط با آن در نظر گرفته شده است. به همین دلیل، در طرحواره پایگاه داده، کلاس [Adresse] به عنوان یک جدول جداگانه ظاهر نمی‌شود، بلکه به عنوان بخشی از جدول مرتبط با @Entity [Personne] در نظر گرفته می‌شود.

کلاس @Entity [Personne] تغییر چندانی نسبت به نسخه قبلی خود نداشته است: صرفاً یک فیلد adresse به آن اضافه شده است:


package entites;

...
@Entity
@Table(name = "jpa02_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @Embedded
    private Adresse adresse;

    // سازنده‌ها
    public Personne() {
    }
...
}
  • این تغییر در خطوط ۳۳–۳۴ رخ می‌دهد. شیء [Personne] اکنون دارای یک فیلد adresse از نوع Adresse است. این فیلد مربوط به POJO است. توضیحیه @Embedded برای پل شیء-رابطه‌ای در نظر گرفته شده است. این نشان می‌دهد که فیلد [Adresse adresse] باید در همان جدول با شیء [Personne] قرار گیرد.

2.2.3. محیط آزمون

ما قصد داریم آزمایش‌هایی بسیار مشابه با آنچه قبلاً مورد بحث قرار گرفت انجام دهیم. این آزمایش‌ها در زمینهٔ زیر انجام خواهند شد:

پیاده‌سازی مورد استفاده JPA / Hibernate [6] است. پروژه Eclipse برای آزمایش‌ها به شرح زیر است:

پروژه اکلیپس [1] تنها در کد جاوا خود [2] با پروژه قبلی متفاوت است. محیط (کتابخانه‌ها – persistence.xml – DBMS – پوشه‌های پیکربندی و DDL – اسکریپت Ant) همان محیطی است که قبلاً، به‌ویژه در بخش 2.1.5، مورد بحث قرار گرفته است. این وضعیت برای پروژه‌های آینده Hibernate نیز ادامه خواهد داشت و، مگر در موارد استثنایی، دیگر به این محیط باز نخواهیم گشت. به‌ویژه، فایل‌های persistence.xml که لایه JPA/Hibernate را برای فایل‌های مختلف SGBD پیکربندی می‌کنند، همان فایل‌هایی هستند که قبلاً مورد بحث قرار گرفته‌اند و در پوشه <conf> قرار دارند.

اگر خواننده در مورد رویه‌های لازم برای دنبال کردن مطمئن نیست، دعوت می‌شود به رویه‌های به‌کار رفته در مطالعه قبلی مراجعه کند.

پروژه Eclipse، [3]، در پوشه مثال‌ها، [4]، قرار دارد. ما آن را وارد خواهیم کرد.

2.2.4. ایجاد پایگاه داده DDL

طبق دستورالعمل‌های بخش 2.1.7، خروجی DDL که برای SGBD و MySQL5 به دست آمده، به شرح زیر است:


    drop table if exists jpa02_hb_personne;

    create table jpa02_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
) ENGINE=InnoDB;

Hibernate به‌درستی تشخیص داد که آدرس شخص باید در جدولی که با @Entity Personne مرتبط است، درج شود (خطوط ۱۱–۱۷).

2.2.5. InitDB

کد مربوط به [InitDB] به شرح زیر است:


package tests;
...

public class InitDB {

    //ثابت‌ها
    private final static String TABLE_NAME = "jpa02_hb_personne";

    public static void main(String[] args) throws ParseException {

        // زمینه پایداری
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        //یک EntityManager از EntityManagerFactory قبلی بازیابی می‌شود
        em = emf.createEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // درخواست
        Query sql1;
        // حذف رکوردها از جدول PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_NAME);
        sql1.executeUpdate();
        // ایجاد افراد
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // ایجاد آدرس‌ها
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // ارتباطات شخص <--> آدرس
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // پایداری اشخاص
        em.persist(p1);
        em.persist(p2);
        //نمایش افراد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        //پایان تراکنش
        tx.commit();
        // پایان EntityManager
        em.close();
        // پایان EntityManagerFactory
        emf.close();
        // ثبت
        System.out.println("terminé...");

    }
}

در این کد چیز جدیدی وجود ندارد. همه چیز قبلاً پوشش داده شده است. اجرای [InitDB] در کنار MySQL5 نتایج زیر را تولید می‌کند:

  • [1]: خروجی کنسول
  • [2]: جدول [jpa02_hb_personne] در نمای کاوشگر SQL
  • [3] و [4]: ساختار و محتوای آن.

2.2.6. اصلی

کلاس [Main] به شرح زیر است:


package tests;

...
import entites.Adresse;
import entites.Personne;

@SuppressWarnings( { "unused", "unchecked" })
public class Main {

    // ثوابت
    private final static String TABLE_NAME = "jpa02_hb_personne";

    // زمینه پایداری
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = null;

    // ابجکت‌های مشترک
    private static Personne p1, p2, newp1;

    private static Adresse a1, a2, a3, a4, newa1, newa4;

    public static void main(String[] args) throws Exception {
        //یک EntityManager از EntityManagerFactory بازیابی می‌شود
        em = emf.createEntityManager();

        //پاکسازی پایگاه داده
        log("clean");clean();

        // جدول تخلیه
        dumpPersonne();

        // test1
        log("test1"); test1();

        // test2
        log("test2"); test2();

        // test3
        log("test3"); test3();

        // test4
        log("test4"); test4();

        // test5
        log("test5");test5();

        //پایان زمینه پایداری
        if (em != null && em.isOpen())
            em.close();

        // EntityManagerFactory بسته شد
        emf.close();
    }

    //بازیابی EntityManager
    private static EntityManager getEntityManager() {
...
    }

    // بازیابی یک QZXW2HTMLCRW50aXR5TWFuYWdlc جدید ZQX
    private static EntityManager getNewEntityManager() {
...
    }

    // نمایش محتویات جدول Person
    private static void dumpPersonne() {
...
    }

    //پاک کردن BD
    private static void clean() {
    ...
    }

    // گزارش‌ها
    private static void log(String message) {
...
    }

    // ایجاد شیء
    public static void test1() throws ParseException {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // ایجاد افراد
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // ایجاد آدرس
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        // ارتباطات شخص <--> آدرس
        p1.setAdresse(a1);
        p2.setAdresse(a2);
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // پایداری اشخاص
        em.persist(p1);
        em.persist(p2);
        //پایان تراکنش
        tx.commit();
        // خروجی
        dumpPersonne();
    }

    // تغییر یک شیء زمینه
    public static void test2() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // افزایش تعداد فرزندان p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // تغییر وضعیت تأهل آن
        p1.setMarie(false);
        // شیء p1 به‌طور خودکار ذخیره می‌شود (بررسی کثیف)
        // در طول همگام‌سازی بعدی (commit یا select)
        //پایان تراکنش
        tx.commit();
        // جدول جدید نمایش داده می‌شود
        dumpPersonne();
    }

    // حذف یک شیء متعلق به زمینه پایداری
    public static void test4() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // شیء پیوست p2 حذف شد
        em.remove(p2);
        //پایان تراکنش
        tx.commit();
        // نمایش جدول جدید
        dumpPersonne();
    }

    //جدا کردن، دوباره متصل کردن و تغییر دادن
    public static void test5() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // p1 به زمینه جدید دوباره متصل می‌شود
        p1 = em.find(Personne.class, p1.getId());
        //پایان تراکنش
        tx.commit();
        // آدرس p1 تغییر می‌کند
        p1.getAdresse().setVille("Paris");
        // نمایش جدول جدید
        dumpPersonne();
    }

}

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

main : ----------- clean
[personnes]
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,0,Martin,Paul,31/01/2000,true,2,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,A[Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Angers,null,France]]
main : ----------- test5
[personnes]
P[1,2,Martin,Paul,31/01/2000,false,3,A[8 rue Boileau,null,null,49000,Paris,null,France]]

از خوانندگان دعوت می‌شود تا ارتباط بین نتایج و کد را برقرار کنند.

ما اکنون از پیاده‌سازی JPA / Toplink استفاده می‌کنیم:

پروژه تست جدید Eclipse به شرح زیر است:

کد جاوا با پروژه قبلی Hibernate یکسان است. محیط (کتابخانه‌ها – persistence.xml – DBMS – پوشه‌های conf و ddl – اسکریپت Ant) همان است که قبلاً در بخش 2.1.15.2 مورد بحث قرار گرفته است. این وضعیت برای پروژه‌های آینده Toplink نیز ادامه خواهد داشت و مگر در موارد استثنایی، دیگر به این محیط باز نخواهیم گشت. به‌ویژه، فایل‌های persistence.xml که لایه JPA/Toplink را برای فایل‌های مختلف SGBD پیکربندی می‌کنند، همان‌هایی هستند که قبلاً مورد بحث قرار گرفته‌اند و در پوشه <conf> قرار دارند.

اگر خواننده در مورد رویه‌های لازم برای دنبال کردن مطمئن نیست، دعوت می‌شود به رویه‌های به‌کار رفته در مطالعه قبلی مراجعه کند.

پروژه Eclipse با نام [3] در پوشه مثال‌ها با نام [4] قرار دارد. ما آن را وارد خواهیم کرد.

اجرای [InitDB] با SGBD و MySQL5 نتایج زیر را تولید می‌کند:

  • [1]: خروجی کنسول
  • [2]: جداول [jpa02_tl_personne] و [SEQENCE] در نمای SQL Explorer
  • [3] و [4]: ساختار و محتوای [jpa02_tl_personne].

اسکریپت‌های SQL تولیدشده در ddl/mysql5 [5] به شرح زیر هستند:

create.sql


CREATE TABLE jpa02_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR3 VARCHAR(30), CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


DROP TABLE jpa02_tl_personne
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.3. مثال ۳: رابطه یک‌به‌یک از طریق کلید خارجی

2.3.1. : طرحواره پایگاه داده

1
2

    alter table jpa03_hb_personne 
        drop 
        foreign key FKFBBBFDD05FE379D0;

    drop table if exists jpa03_hb_adresse;

    drop table if exists jpa03_hb_personne;

    create table jpa03_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa03_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa03_hb_personne 
        add index FKFBBBFDD05FE379D0 (adresse_id), 
        add constraint FKFBBBFDD05FE379D0 
        foreign key (adresse_id) 
references jpa03_hb_adresse (id);
  • به [1]: پایگاه داده. این بار، آدرس شخص در یک جدول اختصاصی، [adresse]، ذخیره می‌شود. جدول [personne] از طریق یک کلید خارجی به این جدول متصل است.
  • در [2]: DDL تولیدشده توسط Hibernate برای MySQL5:
    • خطوط ۹–۲۰: جدول [adresse] که به کلاس [Adresse] متصل خواهد شد، اکنون یک شیء @Entity است.
    • خط ۱۰: کلید اصلی جدول [adresse]
    • خط ۳۰: به جای یک آدرس کامل، جدول [personne] اکنون حاوی شناسه‌ی [adresse_id] برای آن آدرس است.
    • خطوط 34–38: شخص (adresse_id) یک کلید خارجی روی آدرس (id) است.

2.3.2. ابجکت‌های @Entity که نمایندهٔ پایگاه داده هستند

یک شخص دارای آدرس اکنون با کلاس زیر [Personne] نمایش داده می‌شود:


package entites;
...
@Entity
@Table(name = "jpa03_hb_personne")
public class Personne implements Serializable{

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;
...
}
  • خطوط ۳۲–۳۴: آدرس شخص
    • خط ۳۲: تگ @OneToOne نشان‌دهنده یک رابطه یک‌به‌یک است: یک شخص حداقل یک و حداکثر یک آدرس دارد. ویژگی `cascade = CascadeType.ALL` به این معنی است که هر عملیاتی (persist، merge، remove) روی @Entity [Personne] باید به @Entity [Adresse] نیز به صورت آبشاری اعمال شود. از دیدگاه زمینه پایداری، این به معنای زیر است. اگر p یک شخص باشد و یک آدرس داشته باشد:
      • یک عملیات صریح em.persist(p) منجر به یک عملیات ضمنی em.persist(a) خواهد شد
      • یک عملیات صریح em.merge(p) منجر به یک عملیات ضمنی em.merge(a) خواهد شد
      • یک عملیات صریح em.remove(p) منجر به یک عملیات ضمنی em.remove(a) خواهد شد

تجربه نشان می‌دهد که این آبشاری‌های ضمنی درمان همه مشکلات نیستند. توسعه‌دهندگان در نهایت آنچه انجام می‌دهند را فراموش می‌کنند. ممکن است ترجیح داده شود که از عملیات صریح در کد استفاده شود. انواع مختلفی از آبشاری وجود دارد. حاشیه‌نویسی @OneToOne می‌توانست به شکل زیر نوشته شود:


//@OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
@OneToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.REMOVE}, fetch=FetchType.LAZY)

ویژگی cascade به عنوان مقدار خود آرایه‌ای از ثابت‌ها را می‌گیرد که انواع آبشاری مورد نظر را مشخص می‌کنند.

ویژگی fetch=FetchType.LAZY به Hibernate دستور می‌دهد وابستگی را در آخرین لحظه ممکن بارگذاری کند. هنگام افزودن فهرستی از افراد به زمینه پایداری، لزوماً نمی‌خواهیم آدرس‌های آن‌ها را نیز شامل شویم. برای مثال، ممکن است شما فقط برای یک شخص خاص که توسط کاربر از طریق رابط وب انتخاب شده است، به این آدرس نیاز داشته باشید. از سوی دیگر، ویژگی fetch=FetchType.EAGER به Hibernate دستور می‌دهد که وابستگی‌ها را فوراً بارگذاری کند.

  • (ادامه)
    • خط ۳۳: تگ @JoinColumn، کلید خارجی را که جدول @Entity با شناسه [Personne] روی جدول @Entity با شناسه [Adresse] دارد، تعریف می‌کند. ویژگی name نام ستونی را که به‌عنوان کلید خارجی عمل می‌کند، تعریف می‌کند. ویژگی unique=true یک رابطه یک‌به‌یک را اعمال می‌کند: یک مقدار یکسان نمی‌تواند دو بار در ستون [adresse_id] ظاهر شود. ویژگی nullable=false تضمین می‌کند که یک شخص باید حتماً یک آدرس داشته باشد.

آدرس یک شخص اکنون با @Entity زیر [Adresse] نمایش داده می‌شود:


package entites;

...
@Entity
@Table(name = "jpa03_hb_adresse")
public class Adresse implements Serializable {

    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    @Column(length = 30)
    private String adr2;

    @Column(length = 30)
    private String adr3;

    @Column(length = 5, nullable = false)
    private String codePostal;

    @Column(length = 20, nullable = false)
    private String ville;

    @Column(length = 3)
    private String cedex;

    @Column(length = 20, nullable = false)
    private String pays;

    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
    private Personne personne;

    // سازنده‌ها
    public Adresse() {

    }
...
}
  • خط ۴: کلاس [Adresse] به یک شیء @Entity تبدیل می‌شود. بنابراین، این کلاس مبنای یک جدول در پایگاه داده خواهد بود.
  • خطوط ۹–۱۲: مانند هر شیء @Entity، [Adresse] دارای یک کلید اصلی است. این کلید «Id» نامگذاری شده و دارای همان نشانه‌گذاری‌های استاندارد (annotations) مشابه کلید اصلی Id از @Entity [Personne] است.
  • خطوط ۳۹–۴۰: رابطه یک‌به‌یک با @Entity [Personne]. در اینجا چند نکته ظریف وجود دارد:
    • اولاً، فیلد personne الزامی نیست. این فیلد به ما امکان می‌دهد تا از یک آدرس، فرد واحدی را که آن آدرس را دارد ردیابی کنیم. اگر ما این سهولت را نمی‌خواستیم، فیلد personne وجود نداشت و همه چیز همچنان کار می‌کرد.
    • رابطه یک‌به‌یک بین دو موجودیت [Personne] و [Adresse] قبلاً در @Entity [Personne] پیکربندی شده است:

    @OneToOne(cascade = CascadeType.ALL, fetch=FetchType.LAZY)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
private Adresse adresse;

برای اطمینان از اینکه دو پیکربندی یک‌به‌یک با یکدیگر تداخل ندارند، یکی به‌عنوان principale و دیگری به‌عنوان inverse در نظر گرفته می‌شوند. این رابطه که با نام principale شناخته می‌شود، توسط پل شیء-رابطه‌ای مدیریت می‌شود. رابطه دیگر، با نام inverse، به طور مستقیم مدیریت نمی‌شود: این رابطه به طور غیرمستقیم از طریق رابطه principale مدیریت می‌شود. در @Entity [Adresse]:


@OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

این ویژگی mappedBy است که رابطه یک‌به‌یک فوق را تعریف می‌کند، رابطه inverse از رابطه یک‌به‌یک principale، که توسط فیلد adresse از @Entity [Personne] تعریف شده است.

2.3.3. پروژه Eclipse / Hibernate 1

پیاده‌سازی JPA که در اینجا استفاده شده، مربوط به Hibernate است. پروژه آزمایشی Eclipse به شرح زیر است:

این پروژه در پوشهٔ مثال‌ها [4] قرار دارد. آن را وارد خواهیم کرد.

2.3.4. ایجاد DDL از پایگاه داده

طبق دستورالعمل‌های بخش 2.1.7، فایلی که برای فایل‌های SGBD و MySQL5 به دست آمده است، DDL، همان فایلی است که در ابتدای این بخش نشان داده شده است.

2.3.5. InitDB

کد برای [InitDB] به شرح زیر است:


package tests;
...
import entites.Adresse;
import entites.Personne;

public class InitDB {

    // ثوابت
    private final static String TABLE_PERSONNE = "jpa03_hb_personne";

    private final static String TABLE_ADRESSE = "jpa03_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // زمینه پایداری
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        //یک EntityManager از EntityManagerFactory قبلی بازیابی می‌شود
        em = emf.createEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // درخواست
        Query sql1;
        // حذف رکوردها از جدول PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        // حذف ردیف‌ها از جدول ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        //ایجاد اشخاص
        Personne p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // ایجاد آدرس‌ها
        Adresse a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        Adresse a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        Adresse a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // ارتباطات بین افراد و آدرس‌ها
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // پایداری اشخاص و به تبع آن، آدرس‌هایشان
        em.persist(p1);
        em.persist(p2);
        // و آدرس‌های a3 و a4 که به افراد متصل نیستند
        em.persist(a3);
        em.persist(a4);
        //نمایش افراد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        //نمایش آدرس‌ها
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }

        //پایان تراکنش
        tx.commit();
        // پایان EntityManager
        em.close();
        //پایان EntityManagerFactory
        emf.close();
        // لاگ
        System.out.println("terminé...");

    }
}

ما تنها در مورد نکاتی که نسبت به آنچه قبلاً بررسی شده بینش جدیدی ارائه می‌دهند، توضیح خواهیم داد:

  • خطوط ۳۱–۳۲: دو نفر ایجاد می‌شوند
  • خطوط ۳۴–۳۷: چهار آدرس ایجاد می‌شوند
  • خطوط ۳۹–۴۲: افراد (p1، p2) به آدرس‌ها (a1، a2) متصل شده‌اند. آدرس‌های (a3، a4) یتیم هستند. هیچ فردی به آن‌ها متصل نیست. کد DDL این امکان را فراهم می‌کند. در حالی که یک فرد باید حتماً یک آدرس داشته باشد، برعکس این موضوع صادق نیست.
  • خطوط ۴۴–۴۵: ما افراد (p1، p2) را ذخیره می‌کنیم. از آنجا که ویژگی cascade را روی CascadeType.ALL در رابطه یک‌به‌یک که یک شخص را به آدرسش پیوند می‌دهد تنظیم کرده‌ایم، آدرس‌های (a1, a2) این دو شخص نیز باید تحت persist باشند. این چیزی است که می‌خواهیم تأیید کنیم. برای آدرس‌های یتیم (a3, a4)، ما موظفیم این مورد را صراحتاً رسیدگی کنیم (خطوط 47–48).
  • خطوط ۵۱–۵۳: نمایش جدول اشخاص
  • خطوط ۵۶–۵۷: نمایش جدول آدرس‌ها

اجرای [InitDB] با MySQL5 نتایج زیر را تولید می‌کند:

  • [1]: خروجی کنسول
  • [2]: جداول [jpa03_hb_*] در نمای کاوشگر SQL
  • [3]: جدول افراد
  • [4]: جدول آدرس‌ها. همگی در آنجا هستند. همچنین به ارتباط بین ستون [adresse_id] در [3] و ستون [id] در [4] (کلید خارجی) توجه کنید.

2.3.6. اصلی

کلاس [Main] شامل شش تست است که اکنون آن‌ها را بررسی خواهیم کرد.

2.3.6.1. Test1

این آزمون به شرح زیر است:


// ایجاد شیء
    public static void test1() throws ParseException {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // ایجاد شخص
        p1 = new Personne("Martin", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        p2 = new Personne("Durant", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // ایجاد آدرس‌ها
        a1 = new Adresse("8 rue Boileau", null, null, "49000", "Angers", null, "France");
        a2 = new Adresse("Apt 100", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        a3 = new Adresse("x", "x", "x", "x", "x", "x", "x");
        a4 = new Adresse("y", "y", "y", "y", "y", "y", "y");
        // ارتباطات شخص <--> آدرس
        p1.setAdresse(a1);
        a1.setPersonne(p1);
        p2.setAdresse(a2);
        a2.setPersonne(p2);
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // پایداری اشخاص
        em.persist(p1);
        em.persist(p2);
        // و آدرس‌های a3 و a4 که به افراد متصل نیستند
        em.persist(a3);
        em.persist(a4);
        //پایان تراکنش
        tx.commit();
        // جدول‌ها نمایش داده می‌شوند
        dumpPersonne();
        dumpAdresse();
    }

این کد از [InitDB] گرفته شده است. نتیجه آن به شرح زیر است:

1
2
3
4
5
6
7
8
9
main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]

هر دو جدول پر شده‌اند.

2.3.6.2. Test2

این تست به شرح زیر است:


    // ویرایش یک شیء در زمینه
    public static void test2() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        //افزایش تعداد فرزندان برای p1
        p1.setNbenfants(p1.getNbenfants() + 1);
        // تغییر وضعیت تأهل
        p1.setMarie(false);
        // شیء p1 به‌طور خودکار ذخیره می‌شود (بررسی کثیف)
        // در طول همگام‌سازی بعدی (commit یا select)
        // پایان تراکنش
        tx.commit();
        //جدول جدید نمایش داده می‌شود
        dumpPersonne();
}

نتیجه به شرح زیر است:

1
2
3
4
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
  • خط ۴: تعداد فرزندان شخص p1 به ۱ افزایش یافته و نسخهٔ او از ۰ به ۱ تغییر کرده است

2.3.6.3. Test4

این آزمون به شرح زیر است:


    // حذف یک شیء متعلق به زمینه پایداری
    public static void test4() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // شیء پیوست p2 حذف می‌شود
        em.remove(p2);
        // پایان تراکنش
        tx.commit();
        // نمایش جدول‌های جدید
        dumpPersonne();
        dumpAdresse();
}
  • خط ۹: شخص p2 حذف شده است. این شخص دارای رابطه آبشاری با آدرس a2 است. بنابراین، آدرس a2 نیز باید حذف شود.

نتیجه آزمون ۴ به شرح زیر است:

main : ----------- test1
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,0,Martin,Paul,31/01/2000,true,2,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[2,0,Apt 100,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test2
[personnes]
P[2,0,Durant,Sylvie,05/07/2001,false,0,2]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • شخص p2 که در خط ۳ آزمون ۱ موجود است، در آزمون ۴ دیگر موجود نیست
  • همین امر در مورد آدرس a2 آنها نیز صدق می‌کند که در خط 7 آزمون 1 ظاهر می‌شود اما در آزمون 4 غایب است.

2.3.6.4. آزمون ۵

این آزمون به شرح زیر است:


//جدا کردن، دوباره متصل کردن و تغییر دادن
    public static void test5() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // اتصال مجدد p1 به زمینه جدید
        p1 = em.find(Personne.class, p1.getId());
        // تغییر آدرس p1
        p1.getAdresse().setVille("Paris");
        //پایان تراکنش
        tx.commit();
        // نمایش جدول‌های جدید
        dumpPersonne();
        dumpAdresse();
    }
  • خط ۴: ما یک زمینه پایداری جدید داریم که در نتیجه خالی است.
  • خط ۹: ما شخص p1 را در آن وارد می‌کنیم. p1 در پایگاه داده جستجو می‌شود زیرا در زمینه وجود ندارد. با این حال، عناصری که به p1 (آدرس آن) وابسته هستند، از پایگاه داده بازیابی نمی‌شوند زیرا ما نوشته‌ایم:

    @OneToOne(..., fetch=FetchType.LAZY)

این مفهوم «بارگذاری تنبل» یا «بارگذاری در لحظه نیاز» است: وابستگی‌های یک شیء پایدار تنها زمانی که مورد نیاز باشند به حافظه بارگذاری می‌شوند.

  • خط ۱۱: ما فیلد «شهر» آدرس مربوط به p1 را تغییر می‌دهیم. به دلیل getAdresse، و اگر آدرس مربوط به p1 قبلاً در زمینه پایداری وجود نداشته باشد، از پایگاه داده بارگذاری خواهد شد.
  • خط ۱۳: تراکنش commit می‌شود که باعث همگام‌سازی کانکست پایداری با پایگاه داده می‌شود. کانکست پایداری تشخیص می‌دهد که آدرس شخص p1 تغییر کرده و آن را ذخیره می‌کند.

اجرای test5 نتایج زیر را تولید می‌کند:

main : ----------- test4
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,0,8 rue Boileau,null,null,49000,Angers,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
  • شخص p1 (خط ۳ از test4، خط ۱۰ از test5) در واقع شاهد تغییر شهر خود از آنژ (خط ۵ از test4) به پاریس (خط ۱۲ از test5) بوده است.

2.3.6.5. آزمون۶

این آزمون به شرح زیر است:


// حذف یک شیء آدرس
    public static void test6() {
        EntityTransaction tx = null;
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        tx = em.getTransaction();
        tx.begin();
        // اتصال مجدد آدرس a3 به زمینه جدید
        a3 = em.find(Adresse.class, a3.getId());
        System.out.println(a3);
        // حذف می‌شود
        em.remove(a3);
        // پایان تراکنش
        tx.commit();
        // خروجی گرفتن جدول «Address»
        dumpAdresse();
    }
  • خط ۵: ما در یک زمینه پایداری جدید هستیم که بنابراین خالی است.
  • خط ۱۰: آدرس a3 را در زمینه پایداری قرار می‌دهیم
  • خط ۱۳: ما آن را حذف می‌کنیم. این یک آدرس یتیم (متعلق به هیچ فردی نبود) بود. بنابراین حذف آن ممکن است.

نتیجهٔ اجرای کد به شرح زیر است:

main : ----------- test5
[personnes]
P[1,1,Martin,Paul,31/01/2000,false,3,1]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[3,0,x,x,x,x,x,x,x]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • آدرس a3 از تست 5 (خط 6) از میان آدرس‌های تست 6 (خطوط 11–12) ناپدید شده است.

2.3.6.6. آزمون ۷

این تست به شرح زیر است:


// بازگشت به عقب
    public static void test7() {
        EntityTransaction tx = null;
        try {
            // زمینه پایداری جدید
            EntityManager em = getNewEntityManager();
            // شروع تراکنش
            tx = em.getTransaction();
            tx.begin();
            // اتصال مجدد آدرس a1 به زمینه جدید
            newa1 = em.find(Adresse.class, a1.getId());
            // ارتباط مجدد آدرس a4 با زمینهٔ جدید
            newa4 = em.find(Adresse.class, a4.getId());
            // تلاش برای حذف آن‌ها – باید یک استثنا ایجاد شود زیرا آدرسی که به یک شخص متصل است قابل حذف نیست، که این مورد برای newa1 صدق می‌کند
            em.remove(newa4);
            em.remove(newa1);
            //پایان تراکنش
            tx.commit();
        } catch (RuntimeException e1) {
            //خطایی رخ داده است
            System.out.format("Erreur dans transaction [%s%n%s%n%s%n%s]%n", e1.getClass().getName(), e1.getMessage(), e1.getCause(), e1.getCause()
                    .getCause());
            try {
                if (tx.isActive())
                    tx.rollback();
            } catch (RuntimeException e2) {
                System.out.format("Erreur au rollback [%s]%n", e2.getMessage());
            }
            // زمینهٔ فعلی در حال رها شدن است
            em.clear();
        }
        // خروجی دیباگ – جدول آدرس‌ها نباید به دلیل بازگشت به عقب تغییر کرده باشد
        dumpAdresse();
    }
  • test7: در حال آزمایش بازگشت تراکنش هستیم
    • خط ۶: ما در یک زمینه پایداری جدید هستیم، که بنابراین خالی است.
    • خط ۱۱: ما آدرس a1 را در زمینه پایداری قرار می‌دهیم، زیر مرجع newa1
    • خط ۱۳: ما آدرس a4 را در زمینه پایداری قرار می‌دهیم، زیر مرجع newa4
    • رده‌های ۱۵–۱۶: دو آدرس newa1 و newa4 حذف شده‌اند. newa1 نشانی شخص p1 است و بنابراین در پایگاه داده، p1 از طریق یک کلید خارجی به newa1 ارجاع می‌دهد. بنابراین حذف newa1 شکست خواهد خورد و هنگام همگام‌سازی کنتکست پایداری در زمان commit تراکنش (خط ۱۸) یک استثنا ایجاد می‌کند. این امر یک rollback (خط ۲۵) را فعال می‌کند و در نتیجه هر دو عملیات در تراکنش بازگردانده می‌شوند. بنابراین باید مشاهده کنیم که آدرس newa4، که می‌توانست به طور قانونی حذف شود، حذف نشده است.

اجرا نتیجه زیر را تولید می‌کند:


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.ObjectDeletedException: deleted entity passed to persist: [entites.Adresse#<null>]
null]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • جدول آدرس برای تست ۷ (خطوط ۱۲–۱۳) با جدول تست ۶ (خطوط ۴–۵) یکسان است. به نظر می‌رسد رول‌بک انجام شده است. با این حال، پیام خطا در خط ۹ یک معما است و نیازمند بررسی بیشتر است. به نظر می‌رسد استثنایی که رخ داده، همان استثنای مورد انتظار نیست. ما باید لاگ‌های Hibernate را در log4j.properties به حالت DEBUG تبدیل کنیم تا تصویر واضح‌تری به دست آوریم:

# گزینهٔ لاگر ریشه
log4j.rootLogger=ERROR, stdout

#گزینه‌های گزارش‌گیری Hibernate (INFO فقط پیام‌های راه‌اندازی را نمایش می‌دهد)
log4j.logger.org.hibernate=DEBUG

سپس می‌توانیم ببینیم که وقتی آدرس a1 در زمینه پایداری قرار گرفت، Hibernate همچنین شخص p1 را نیز در آنجا قرار داد، احتمالاً به دلیل رابطه یک‌به‌یک @Entity [Adresse]:


    @OneToOne(mappedBy = "adresse", fetch=FetchType.LAZY)
private Personne personne;

اگرچه «LazyLoading» در اینجا درخواست شده بود، وابستگی [Personne] با این حال بلافاصله بارگذاری می‌شود. این احتمالاً به این معنی است که ویژگی fetch=FetchType.LAZY در اینجا بی‌معنی است. سپس می‌بینیم که هنگام commit تراکنش، Hibernate آماده حذف آدرس‌های a1 و a4 شده است، اما همچنین آماده ذخیره شخص p1 نیز هست. و در اینجا است که استثنا رخ می‌دهد: از آنجا که شخص p1 روی آدرس خود یک «کاسکید» (cascade) دارد، Hibernate نیز سعی می‌کند آدرس a1 را که همین‌الان حذف شده است، ذخیره کند. این Hibernate است که استثنا را پرتاب می‌کند، نه درایور JDBC. از این رو پیام موجود در خط ۹ بالا نمایش داده می‌شود. علاوه بر این، می‌توانیم ببینیم که rollback در خط ۲۵ هرگز اجرا نمی‌شود زیرا تراکنش غیرفعال شده است. بنابراین، تست موجود در خط ۲۴ از rollback جلوگیری می‌کند.

بنابراین ما به هدف مورد نظر دست نیافته‌ایم: نمایش یک رول‌بک. در واقع هیچ دستوری از نوع SQL به پایگاه داده صادر نشد. چند نکته شایان ذکر است:

  • ارزش فعال کردن ثبت جزئیات برای درک عملکرد دستور ORM
  • در حالی که ORM ممکن است زندگی توسعه‌دهنده را آسان‌تر کند، می‌تواند با پنهان کردن رفتاری که توسعه‌دهنده باید از آن آگاه باشد، مسائل را پیچیده سازد. در این مورد، نحوه بارگذاری وابستگی‌های یک @Entity است.

2.3.7. پروژه Eclipse / Hibernate 2

ما پروژه Eclipse / Hibernate را کپی و پیست می‌کنیم تا تغییری جزئی در پیکربندی اشیاء @Entity ایجاد کنیم:

پروژه [3] در پوشه مثال‌ها (examples) با آدرس [4] قرار دارد. آن را وارد می‌کنیم.

ما فقط @Entity [Adresse] را تغییر می‌دهیم تا دیگر رابطه معکوس یک‌به‌یک با @Entity [Personne] نداشته باشد:


package entites;
...
@Entity
@Table(name = "jpa04_hb_adresse")
public class Adresse implements Serializable {

    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;

    ...

    @Column(length = 20, nullable = false)
    private String pays;

//     @OneToOne(mappedBy = "address", fetch=FetchType.LAZY)
//     شخص خصوصی person;

    // سازنده‌ها
    public Adresse() {

    }
  • خطوط ۲۵–۲۶: رابطه معکوس @OneToOne حذف شده است. مهم است که درک کنیم یک رابطه معکوس هرگز ضروری نیست. تنها رابطه اصلی ضروری است. از رابطه معکوس می‌توان برای سهولت استفاده کرد. در اینجا، این رابطه راه ساده‌ای برای به دست آوردن مالک یک آدرس فراهم می‌کرد. یک رابطه معکوس را همیشه می‌توان با یک پرس‌وجوی JPQL جایگزین کرد. این چیزی است که در مثال زیر نشان خواهیم داد.

برنامه‌های تست دقیقاً همان‌طور که بودند بازتولید شده‌اند. مورد مورد توجه ما تنها تست ۷ است، تستی که در آن رابطه معکوس یک‌به‌یک را در عمل دیدیم. ما همچنین یک تست ۸ را اضافه می‌کنیم تا نشان دهیم چگونه، بدون رابطه معکوس آدرس → شخص، باز هم می‌توانیم شخص با آدرس داده‌شده را بازیابی کنیم.

آزمون ۷ بدون تغییر باقی می‌ماند. اجرای آن اکنون نتایج زیر را تولید می‌کند (سوابق ثبت‌شده غیرفعال است):


main : ----------- test6
A[3,0,x,x,x,x,x,x,x]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
main : ----------- test7
Erreur dans transaction [javax.persistence.RollbackException
Error while commiting the transaction
org.hibernate.exception.ConstraintViolationException: could not delete: [entites.Adresse#1]
java.sql.SQLException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa04_hb_personne`, CONSTRAINT `FKEA3F04515FE379D0` FOREIGN KEY (`adresse_id`) REFERENCES `jpa04_hb_adresse` (`id`))]
[adresses]
A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
A[4,0,y,y,y,y,y,y,y]
  • این بار، ما واقعاً استثناء مورد انتظار را دریافت می‌کنیم: استثناء‌ای که توسط درایور JDBC پرتاب می‌شود زیرا ما سعی کردیم یک سطر را در جدول [adresse] حذف کنیم که توسط یک کلید خارجی از یک سطر در جدول [personne] ارجاع شده بود. ردیف [10] به وضوح علت خطا را نشان می‌دهد.
  • رول‌بک واقعاً انجام شد: در پایان تست ۷، جدول [adresse] (رده‌های ۱۲–۱۳) با جدولی که در پایان تست ۶ (رده‌های ۴–۵) داشتیم یکسان است.

این با تست ۷ در پروژه قبلی Eclipse چه تفاوتی دارد؟ چرا در اینجا با یک استثنای JDBC مواجه می‌شویم که در آزمون قبلی با آن مواجه نشدیم؟ زیرا @Entity [Adresse] دیگر رابطه معکوس یک‌به‌یک با @Entity [Personne] ندارد؛ این توسط Hibernate به‌صورت ایزوله مدیریت می‌شود. هنگامی که آدرس newa1 به زمینه پایداری (persistence context) اضافه شد، Hibernate شخص p1 را که آن آدرس را دارد، به آن زمینه اضافه نکرد. بنابراین، حذف آدرس‌های newa1 و newa4 بدون حضور انتیت Personne در آن زمینه انجام شد.

حال چگونه می‌توانیم از آدرس newa1 برای شناسایی شخص p1 که آن آدرس را دارد استفاده کنیم؟ این یک سؤال مشروع است. آزمون ۸ زیر به آن پاسخ می‌دهد:


//رابطه معکوس یک‌به‌یک
    // پیاده‌سازی‌شده توسط یک پرس‌وجو JPQL
    public static void test8() {
        EntityTransaction tx = null;
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        tx = em.getTransaction();
        tx.begin();
        // آدرس a1 به زمینهٔ جدید متصل می‌شود
        newa1 = em.find(Adresse.class, a1.getId());
        //بازیابی مالک این آدرس
        Personne p1 = (Personne) em.createQuery("select p from Personne p join p.adresse a where a.id=:adresseId").setParameter("adresseId", newa1.getId())
                .getSingleResult();
        // نمایش آنها
        System.out.println("adresse=" + newa1);
        System.out.println("personne=" + p1);
        // پایان تراکنش
        tx.commit();
    }
  • خط ۶: شروع یک زمینه پایداری جدید و خالی
  • خطوط ۸–۹: شروع تراکنش
  • خط ۱۱: آدرس a1 وارد زمینه پایداری (persistence context) شده و توسط newa1 ارجاع داده می‌شود.
  • خط ۱۳: شخص p1 با آدرس newa1 از طریق پرس‌وجوی JPQL بازیابی می‌شود. ما می‌دانیم که [Personne] و [Adresse] توسط یک رابطه کلید خارجی به هم متصل هستند. در کلاس [Personne]، فیلد [adresse] که دارای انوتیشن @OneToOne است، این رابطه را نشان می‌دهد. دستور JPQL «select p from Person p join p.adresse a» یک پیوند بین جداول [personne] و [adresse] برقرار می‌کند. معادل SQL که در کنسول Hibernate تولید می‌شود (برای نمونه‌ها به بخش 2.1.12 مراجعه کنید) به شرح زیر است:
SQL #0 انواع: entites.Personne
-----------------
select
  personne0_.id as id1_,
  personne0_.version as version1_,
  personne0_.nom as nom1_,
  personne0_.prenom as prenom1_,
  personne0_.datenaissance as datenais5_1_,
  personne0_.marie as marie1_,
  personne0_.nbenfants as nbenfants1_,
  personne0_.adresse_id as adresse8_1_ 
 from
  jpa04_hb_personne personne0_ 
 inner join
  jpa04_hb_adresse adresse1_ 
on personne0_.adresse_id=adresse1_.id

پیوند بین دو جدول به وضوح قابل مشاهده است. هر شخص اکنون به آدرس خود متصل شده است. باید مشخص شود که ما فقط به آدرس newa1 علاقه‌مند هستیم. پرس‌وجو به صورت «select p from Person p join p.adresse a where a.id=:adresseId» درمی‌آید. به استفاده از نام‌های مستعار p و a توجه کنید. پرس‌وجوهای JPQL به طور گسترده از نام‌های مستعار استفاده می‌کنند. بنابراین، عبارت «from Person p join p.adresse a» به این معنی است که یک شخص با نام مستعار p و آدرس او (p.adresse) با نام مستعار a نمایش داده می‌شوند. عملیات محدودسازی «where a.id=:adresseId» ردیف‌های درخواست‌شده را به تنها کسانی محدود می‌کند که p مقدارشان:adresseId به‌عنوان شناسه‌ی آدرس آن‌ها a. :adresseId پارامتر نامیده می‌شود و عبارت JPQL یک عبارت پارامتریک JPQL است. در زمان اجرا، این پارامتر باید یک مقدار به آن اختصاص یابد. این کار با استفاده از متد انجام می‌شود

Query setParameter(String nomParamètre, Object valeurParamètre)

که امکان تخصیص مقدار به پارامتری را که با نامش شناسایی می‌شود، فراهم می‌آورد. توجه داشته باشید که setParameter یک شیء Query را بازمی‌گرداند، درست مانند متد createQuery. در نتیجه، فراخوانی‌های متد [em.createQuery(...).setParameter(...).getSingleResult(...)] را می‌توان پشت سر هم قرار داد، زیرا متدهای [setParameter, getSingleResult] متدهای رابط Query هستند. متد [getSingleResult] برای پرس‌وجوهای Select که تنها یک نتیجه بازمی‌گردانند، استفاده می‌شود. در اینجا نیز همین‌طور است.

  • خطوط ۱۶–۱۷: آدرس newa1 و شخص p1 مرتبط با آن آدرس برای تأیید نمایش داده می‌شوند.

نتیجهٔ به‌دست‌آمده به شرح زیر است:

1
2
3
main : ----------- test8
adresse=A[1,1,8 rue Boileau,null,null,49000,Paris,null,France]
personne=P[1,1,Martin,Paul,31/01/2000,false,3,1]

این صحیح است. درس قابل استخراج از این مثال این است که رابطه معکوس یک‌به‌یک از @entity [Adresse] به @entity [Personne] ضروری نبود. تجربه در اینجا نشان داد که حذف آن منجر به رفتار قابل پیش‌بینی‌تر کد شد. این امر اغلب صادق است.

2.3.8. کنسول Hibernate

آزمون ۸ قبلی از یک فرمان JPQL برای انجام یک پیوند بین موجودیت‌های Personne و Adresse استفاده کرد. اگرچه مشابه زبان SQL است، زبان‌های JPQL، JPA و HQL هایبرنیت نیاز به یادگیری دارند و کنسول هایبرنیت برای این منظور عالی است. ما قبلاً در بخش 2.1.12 از آن برای پرس‌وجوی یک جدول واحد استفاده کرده‌ایم. در اینجا نیز دوباره از آن برای پرس‌وجوی دو جدول مرتبط با یکدیگر از طریق یک رابطه کلید خارجی استفاده خواهیم کرد.

بیایید یک کنسول Hibernate برای پروژه فعلی Eclipse خود ایجاد کنیم:

  • [1]: ما به چشم‌انداز [Hibernate Console] (Window / Open Perspective / Other) سوئیچ می‌کنیم
  • [2]: یک پیکربندی جدید ایجاد می‌کنیم
  • با استفاده از دکمه [4]، پروژه جاوایی را که پیکربندی Hibernate برای آن ایجاد می‌شود، انتخاب می‌کنیم. نام آن در [3] نمایش داده می‌شود.
  • در [5]، نام دلخواه خود را برای این پیکربندی وارد می‌کنیم. در اینجا، ما از نام پروژه جاوا استفاده کرده‌ایم.
  • در [6]، مشخص می‌کنیم که از پیکربندی‌ای به نام JPA استفاده می‌کنیم تا ابزار بداند که باید فایل [META-INF/persistence.xml] را پردازش کند
  • در [7]: در این فایل، مشخص می‌کنیم که برای [META-INF/persistence.xml]، باید از واحد پایداری با نام jpa استفاده شود.
  • در [8]، پیکربندی را اعتبارسنجی می‌کنیم.

سپس باید SGBD اجرا شود. در اینجا، MySQL5 است.

  • در [1]: پیکربندی ایجادشده دارای ساختار درختی سه‌شاخه‌ای است
  • در [2]: شاخه [Configuration] اشیایی را که کنسول برای پیکربندی خود استفاده کرده است، فهرست می‌کند: در این مورد، @Entity Personne و Adresse.
  • در [3]: Session Factory یک مفهوم Hibernate است که مشابه EntityManager در JPA است. این مفهوم با استفاده از اشیاء موجود در شاخه [Configuration]، شکاف شیء-رابطه‌ای را پر می‌کند. در [3]، اشیاء زمینه پایداری (persistence context) ارائه شده‌اند؛ در اینجا، بار دیگر، کلاس‌های @Entity Personne و Adresse.
  • در [4]: پایگاه داده‌ای که از طریق پیکربندی موجود در [persistence.xml] دسترسی پیدا می‌کند. این شامل جداول [jpa04_hb_*] است که توسط پروژه فعلی Eclipse ما تولید شده‌اند.
  • در [1]، ما یک ویرایشگر HQL ایجاد می‌کنیم
  • در ویرایشگر HQL،
    • در [2]، پیکربندی Hibernate را که باید استفاده شود انتخاب می‌کنیم، اگر چندین مورد وجود داشته باشد (که در اینجا اینطور است)
    • در [3]، دستوری را که می‌خواهید اجرا کنید (JPQL) وارد کنید؛ در این مورد، دستور JPQL از تست ۸
    • در [4]، آن را اجرا کنید
    • در [5]، نتایج پرس‌وجو در پنجره [Hibernate Query Result] نمایش داده می‌شوند.
    • در [6]، پنجره [Hibernate Dynamic SQL preview] به شما امکان می‌دهد پرس‌وجوی SQL را که اجرا شده است مشاهده کنید.

راه دیگری برای دستیابی به همان نتیجه:

  • در [1]: دستور JPQL یک پیوند بین موجودیت‌های Personne و Adresse برقرار می‌کند. [ref1] به این فرم «پیوند ثیتا» (theta join) می‌گوید.
  • در [2]: معادل SQL
  • در [3]: نتیجه

شکل سومی که تنها توسط Hibernate (HQL) پذیرفته می‌شود:

  • به [1]: دستور HQL. JPQL نشانه p.adresse.id را قبول نمی‌کند. این تنها یک سطح غیرمستقیم‌سازی را می‌پذیرد.
  • در [2]: معادل SQL. می‌بینیم که از پیوستن بین جداول اجتناب می‌کند.
  • در [3]: نتیجه

در اینجا چند مثال دیگر آورده شده است:

  • در [1]: فهرست افراد به همراه آدرس‌هایشان
  • در [2]: معادل SQL.
  • در [3]: نتیجه
  • در [1]: فهرست آدرس‌ها به همراه مالک آن‌ها، در صورت وجود، و در غیر این صورت هیچ (پیوند بیرونی سمت راست: انتیت «Adresse»، که ردیف‌هایی را که با «Personne» هیچ ارتباطی ندارند ارائه می‌دهد، در سمت راست کلیدواژه «join» قرار دارد).
  • در [2]: معادل SQL.
  • در [3]: نتیجه

شایان ذکر است که تنها موجوده Personne با موجوده Adresse رابطه دارد. برعکس دیگر صادق نیست زیرا رابطه یک‌به‌یک معکوس با نام personne در موجوده Adresse حذف شده است. اگر این رابطه معکوس وجود داشت، می‌توانستیم بنویسیم:

  • در [1]: فهرست آدرس‌ها به همراه مالک آن‌ها، در صورت وجود، و در غیر این صورت هیچ (پیوند بیرونی سمت چپ: انتیتي Adresse، که سطرهایی را که هیچ ارتباطی با Personne ندارند ارائه می‌دهد، در سمت چپ کلمه کلیدی join قرار دارد).
  • در [2]: معادل SQL.
  • در [3]: نتیجه

ما قویاً خواننده را تشویق می‌کنیم تا با استفاده از زبان JPQL در کنسول Hibernate تمرین کند.

ما اکنون از پیاده‌سازی JPA / Toplink استفاده می‌کنیم:

پروژه تست جدید Eclipse به شرح زیر است:

کد جاوا با پروژه قبلی Hibernate یکسان است. محیط (کتابخانه‌ها – persistence.xml – DBMS – پوشه‌های conf و ddl – اسکریپت Ant) همان است که در بخش 2.1.15.2 توضیح داده شده است. پروژه Eclipse، [3]، در پوشه examples، [4]، قرار دارد. آن را وارد خواهیم کرد.

فایل <persistence.xml> از یک جنبه تغییر یافته است، یعنی انتیت‌های اعلام‌شده:


    <persistence-unit name="jpa" transaction-type="RESOURCE_LOCAL">
        <!--  ارائه‌دهنده -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!--کلاس‌های پایدار -->
        <class>entites.Personne</class>
        <class>entites.Adresse</class>
        <!-- ویژگی‌های واحد پایداری -->
...
  • خطوط ۵ و ۶: دو موجودیت مدیریت‌شده

اجرای [InitDB] با SGBD و MySQL5 نتایج زیر را تولید می‌کند:

در [1]، خروجی کنسول؛ در [2]، دو جدول تولیدشده [jpa04_tl]؛ و در [3]، اسکریپت‌های تولیدشده SQL. محتوای آن‌ها به شرح زیر است:

create.sql


CREATE TABLE jpa04_tl_personne (ID BIGINT NOT NULL, PRENOM VARCHAR(30) NOT NULL, DATENAISSANCE DATE NOT NULL, VERSION INTEGER NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NBENFANTS INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa04_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, ADR1 VARCHAR(30) NOT NULL, VILLE VARCHAR(20) NOT NULL, VERSION INTEGER NOT NULL, CEDEX VARCHAR(3), ADR2 VARCHAR(30), PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa04_tl_personne ADD CONSTRAINT FK_jpa04_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa04_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa04_tl_personne DROP FOREIGN KEY FK_jpa04_tl_personne_adresse_id
DROP TABLE jpa04_tl_personne
DROP TABLE jpa04_tl_adresse
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

2.4. مثال ۴: رابطه یک به چند

2.4.1. شمای پایگاه داده

1
2

    alter table jpa06_article 
        drop 
        foreign key FKFFBDD9D8ECCE8750;

    drop table if exists jpa06_article;

    drop table if exists jpa06_categorie;

    create table jpa06_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa06_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa06_article 
        add index FKFFBDD9D8ECCE8750 (categorie_id), 
        add constraint FKFFBDD9D8ECCE8750 
        foreign key (categorie_id) 
references jpa06_categorie (id);
  • در [1]، پایگاه داده، و در [2]، DDL آن (MySQL5)

یک آیتم A(id, version, name) دقیقاً به یک دسته C(id, version, name) تعلق دارد. یک دسته C ممکن است صفر، یک یا چند آیتم داشته باشد. یک رابطه یک‌به‌چند (Category → Item) و رابطه معکوس چند‌به‌یک (Item → Category) وجود دارد. این رابطه با کلید خارجی در جدول [article] که به جدول [categorie] (خطوط 24–28 از DDL) ارجاع می‌دهد، نشان داده شده است.

2.4.2. ابجکت‌های @Entity که نماینده پایگاه داده هستند

یک مقاله با @Entity زیر [Article] نمایش داده می‌شود:


package entites;

...
@Entity
@Table(name="jpa05_hb_article")
public class Article implements Serializable {

    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // رابطهٔ اصلی: مقاله (چند) -> دسته‌بندی (یک)
    // پیاده‌سازی شده از طریق یک کلید خارجی (categorie_id) در مقاله
    // ۱. یک مقاله باید یک دسته‌بندی داشته باشد (nullable=false)
    @ManyToOne(fetch=FetchType.LAZY)
    @JoinColumn(name = "categorie_id", nullable = false)
    private Categorie categorie;

    // سازنده‌ها
    public Article() {
    }

    // گیرنده و تنظیم‌کننده
    ...
    // toString
    public String toString() {
        return String.format("Article[%d,%d,%s,%d]", id, version, nom, categorie.getId());
    }

}
  • خطوط ۹–۱۱: کلید اصلی @Entity
  • خطوط ۱۳–۱۵: شماره نسخه آن
  • خطوط ۱۷–۱۸: نام آیتم
  • خطوط ۲۰–۲۵: یک رابطهٔ چند به یک که @Entity Article را به @Entity Categorie متصل می‌کند:
    • خط ۲۳: آنوتیشن ManyToOne. «Many» به @Entity Article که در آن قرار داریم اشاره دارد و «One» به @Entity Categorie (خط 25) اشاره می‌کند. یک دسته‌بندی (One) می‌تواند چندین آیتم (Many) داشته باشد.
    • خط 24: تگ ManyToOne ستون کلید خارجی را در جدول [article] تعریف می‌کند. این (name) categorie_id نام‌گذاری خواهد شد و هر سطر باید در این ستون (nullable=false) دارای یک مقدار باشد.
    • خط ۲۵: دسته‌ای که آیتم به آن تعلق دارد. هنگامی که یک آیتم در زمینه پایداری قرار می‌گیرد، درخواستی ارسال می‌شود تا دسته‌اش فوراً اضافه نشود (fetch=FetchType.LAZY, خط ۲۳). مشخص نیست که آیا این درخواست منطقی است یا خیر. خواهیم دید.

یک دسته‌بندی با @Entity زیر [Categorie] نشان داده می‌شود:


package entites;
...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    // رابطه معکوس دسته‌بندی (یک) -> مقاله (چند) از رابطه مقاله (چند) -> دسته‌بندی (یک)
    // درج آبشاری: دسته‌بندی -> درج مقالات
    // به‌روزرسانی آبشاری: دسته‌بندی -> به‌روزرسانی مقالات
    // حذف آبشاری: دسته‌بندی → مقالات
    @OneToMany(mappedBy = "categorie", cascade = { CascadeType.ALL })
    private Set<Article> articles = new HashSet<Article>();

    // سازنده‌ها
    public Categorie() {
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...
    // toString
    public String toString() {
        return String.format("Categorie[%d,%d,%s]", id, version, nom);
    }

    // ارتباط دوطرفه دسته‌بندی <--> مقاله
    public void addArticle(Article article) {
        // مورد به مجموعه موارد در دسته‌بندی اضافه می‌شود
        articles.add(article);
        // مورد دسته‌بندی خود را تغییر می‌دهد
        article.setCategorie(this);
    }
}
  • خطوط ۸–۱۱: کلید اصلی @Entity
  • خطوط ۱۲–۱۴: نسخهٔ آن
  • خطوط ۱۶–۱۷: نام دسته‌بندی
  • خطوط ۱۹–۲۴: مجموعهٔ آیتم‌ها در دسته‌بندی
    • خط ۲۳: تگ @OneToMany نشان‌دهنده یک رابطه یک‌به‌چند است. «One» به @Entity [Categorie] که در حال حاضر در آن هستیم اشاره دارد و «Many» به نوع [Article] در خط ۲۴ اشاره دارد: یک (One) دسته‌بندی مقالات متعددی (Many) دارد.
    • خط ۲۳: این حاشیه‌نویسی معکوس (mappedBy) حاشیه‌نویسی ManyToOne است که بر روی فیلد categorie از @Entity Article اعمال شده است: mappedBy=category. رابطه ManyToOne که بر روی فیلد categorie از @Entity Article تعریف شده است، رابطه اصلی است. این رابطه ضروری است. این رابطه نمایانگر رابطه کلید خارجی است که @Entity Article را به @Entity Categorie متصل می‌کند. رابطه OneToMany، که در فیلد articles از @Entity Categorie تعریف شده است، رابطه معکوس است. این رابطه ضروری نیست. این یک قابلیت تسهیلی برای بازیابی آیتم‌ها از یک دسته‌بندی است. بدون این قابلیت، این آیتم‌ها از طریق پرس‌وجوی JPQL بازیابی می‌شدند.
    • خط ۲۳: cascadeType.ALL مشخص می‌کند که عملیات (persist, merge, remove) که روی یک @Entity Categorie انجام می‌شود باید به مقالات آن نیز سرایت کند.
    • خط ۲۳: آیتم‌های یک دسته‌بندی در یک شی از نوع `Set<Article>` قرار داده می‌شوند. نوع `Set` تکرارها را مجاز نمی‌داند. بنابراین، یک آیتم یکسان نمی‌تواند دو بار در شی `Set<Article>` قرار گیرد. «آیتم یکسان» به چه معناست؟ برای نشان دادن اینکه آیتم a با آیتم b یکسان است، جاوا از عبارت a.equals(b) استفاده می‌کند. در کلاس Object، که والدین همه کلاس‌ها است، a.equals(b) اگر a==b باشد، درست است، c.a.d. اگر اشیاء a و b در همان مکان حافظه قرار داشته باشند. ممکن است بخواهیم بگوییم که آیتم‌های a و b اگر نام یکسانی داشته باشند، یکسان هستند. در این صورت، توسعه‌دهنده باید دو متد را در کلاس [Article] بازنویسی کند:
      • equals: که باید true بازگرداند اگر دو آیتم نام یکسانی داشته باشند
      • hashCode: باید برای دو شیء [Article] که متد equals آن‌ها را برابر می‌داند، یک مقدار صحیح یکسان بازگرداند. در اینجا، بنابراین مقدار بر اساس نام آیتم ساخته خواهد شد. مقدار بازگشتی hashCode می‌تواند هر عدد صحیحی باشد. این مقدار در انواع مخازن اشیاء، به‌ویژه دیکشنری‌ها (Hashtable)، استفاده می‌شود.

رابطه OneToMany ممکن است از انواع دیگری غیر از Set برای ذخیره کردن «بسیاری» (Many) استفاده کند—برای مثال، اشیاء List. ما در این سند این موارد را پوشش نخواهیم داد. خواننده می‌تواند آن‌ها را در [ref1] بیابد.

  • خط ۳۸: متد [addArticle] به ما امکان می‌دهد یک مقاله را به یک دسته‌بندی اضافه کنیم. این متد تضمین می‌کند که هر دو انتهای رابطه OneToMany که [Categorie] را به [Article] متصل می‌کند، به‌روزرسانی شوند.

2.4.3. پروژه Eclipse / Hibernate 1

پیاده‌سازی JPA که در اینجا استفاده شده، متعلق به Hibernate است. پروژه آزمایشی Eclipse به شرح زیر است:

این پروژه در [3] در پوشهٔ مثال‌ها [4] قرار دارد. ما آن را وارد خواهیم کرد.

2.4.4. ایجاد DDL از پایگاه داده

طبق دستورالعمل‌های بخش 2.1.7، فایل DDL که برای SGBD و MySQL5 به دست آمده است، همان فایلی است که در ابتدای این مثال، در بخش 2.4.1 نشان داده شده است.

2.4.5. InitDB

کد برای [InitDB] به شرح زیر است:


package tests;

...
public class InitDB {

    // ثوابت
    private final static String TABLE_ARTICLE = "jpa05_hb_article";

    private final static String TABLE_CATEGORIE = "jpa05_hb_categorie";

    public static void main(String[] args) {
        // زمینه پایداری
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        //یک EntityManager از EntityManagerFactory قبلی بازیابی می‌شود
        em = emf.createEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // درخواست
        Query sql1;
        // حذف رکوردها از جدول ARTICLE
        sql1 = em.createNativeQuery("delete from " + TABLE_ARTICLE);
        sql1.executeUpdate();
        // حذف آیتم‌ها از جدول CATEGORIE
        sql1 = em.createNativeQuery("delete from " + TABLE_CATEGORIE);
        sql1.executeUpdate();
        // ایجاد سه دسته‌بندی
        Categorie categorieA = new Categorie();
        categorieA.setNom("A");
        Categorie categorieB = new Categorie();
        categorieB.setNom("B");
        Categorie categorieC = new Categorie();
        categorieC.setNom("C");
        //سه مقاله ایجاد کنید
        Article articleA1 = new Article();
        articleA1.setNom("A1");
        Article articleA2 = new Article();
        articleA2.setNom("A2");
        Article articleB1 = new Article();
        articleB1.setNom("B1");
        // آنها را به دسته‌شان پیوند دهید
        categorieA.addArticle(articleA1);
        categorieA.addArticle(articleA2);
        categorieB.addArticle(articleB1);
        // دسته‌بندی‌ها را ذخیره کرده و سپس به‌صورت آبشاری (واردسازی) آیتم‌ها را اضافه کنید
        em.persist(categorieA);
        em.persist(categorieB);
        em.persist(categorieC);
        //نمایش دسته‌بندی‌ها
        System.out.println("[categories]");
        for (Object p : em.createQuery("select c from Categorie c order by c.nom asc").getResultList()) {
            System.out.println(p);
        }
        // نمایش آیتم‌ها
        System.out.println("[articles]");
        for (Object p : em.createQuery("select a from Article a order by a.nom asc").getResultList()) {
            System.out.println(p);
        }
        // پایان تراکنش
        tx.commit();
        // پایان EntityManager
        em.close();
        // پایان EntityMangerFactory
        emf.close();
        // لاگ
        System.out.println("terminé...");

    }
}
  • خطوط ۲۲–۲۷: جداول [article] و [categorie] پاک می‌شوند. توجه داشته باشید که باید از جدولی که شامل کلید خارجی است شروع کنیم. اگر از جدول [categorie] شروع می‌کردیم، دسته‌بندی‌هایی را که در سطرهای جدول [article] به آن‌ها ارجاع شده بود، حذف می‌کردیم و این کار توسط SGBD رد می‌شد.
  • خطوط ۲۹–۳۴: سه دسته، A، B و C، ایجاد می‌شوند
  • خطوط ۳۶–۴۱: سه آیتم ایجاد می‌شوند: A1، A2، B1 (حرف نشان‌دهنده دسته است)
  • خطوط ۴۳–۴۵: سه آیتم در دسته‌بندی‌های مربوطه قرار می‌گیرند
  • خطوط ۴۷–۴۹: سه دسته در زمینه پایداری قرار می‌گیرند. به دلیل آبشاری بودن رابطه دسته → آیتم، آیتم‌های مرتبط با آن‌ها نیز در آنجا قرار می‌گیرند. بنابراین، تمام اشیاء ایجاد شده اکنون در زمینه پایداری قرار دارند.
  • خطوط ۵۰–۵۹: از کانتکست پایداری درخواست می‌شود تا فهرست دسته‌بندی‌ها و مقالات را بازیابی کند. می‌دانیم که این کار باعث همگام‌سازی کانتکست با پایگاه داده می‌شود. در این مرحله دسته‌بندی‌ها و مقالات در جداول مربوطه ذخیره می‌شوند.

اجرای [InitDB] در کنار MySQL5 نتایج زیر را تولید می‌کند:

  • [1]: خروجی کنسول
  • [2]: جداول [jpa05_hb_*] در نمای SQL Explorer
  • [3]: جدول دسته‌بندی‌ها
  • [4]: جدول مقالات. به لینک از [categorie_id] در [4] به [id] در [3] (کلید خارجی) توجه کنید.

2.4.6. اصلی

کلاس [Main] شامل مجموعه‌ای از تست‌ها است که در حال بررسی آن‌ها هستیم، به استثنای تست‌های ۱ و ۲ که برای راه‌اندازی پایگاه داده از کد [InitDB] استفاده مجدد می‌کنند.

2.4.6.1. آزمون ۳

این تست به شرح زیر است:


    // جستجو برای یک عنصر خاص
    public static void test3() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // معامله
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // بارگذاری دسته‌بندی
        Categorie categorie = em.find(Categorie.class, categorieA.getId());
        // نمایش دسته‌بندی و آیتم‌های مرتبط با آن
        System.out.format("Articles de la catégorie %s :%n", categorie);
        for (Article a : categorie.getArticles()) {
            System.out.println(a);
        }
        //پایان تراکنش
        tx.commit();
}
  • خط ۴: ما یک زمینه پایداری جدید داریم که در نتیجه خالی است
  • خطوط ۶–۷: شروع تراکنش
  • خط ۹: دسته A از پایگاه داده به کانتکست پایداری بازیابی می‌شود
  • خط ۱۱: دسته A نمایش داده می‌شود
  • خطوط ۱۲–۱۴: ما آیتم‌های دسته A را نمایش می‌دهیم. این نشان‌دهنده مزیت رابطه معکوس OneToMany با آیتم‌های @Entity Categorie است. وجود آن ما را از نیاز به ارسال پرس‌وجو روی JPQL برای بازیابی آیتم‌های دسته A نجات می‌دهد. برای به‌دست آوردن آن‌ها، از متد get روی فیلد articles استفاده می‌کنیم.

نتایج به شرح زیر است:

main : ----------- test1
[categories]
Categorie[1,0,A]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[1,0,A1,1]
Article[2,0,A2,1]
Article[3,0,B1,2]
main : ----------- test2
3 categorie(s) trouvée(s) :
A
B
C
3 article(s) trouvé(s) :
A1
A2
B1
main : ----------- test3
Articles de la catégorie Categorie[1,0,A] :
Article[2,0,A2,1]
Article[1,0,A1,1]
  • خط ۲۰: دسته A
  • خطوط 21–22: دو آیتم در دسته A

2.4.6.2. Test4

این آزمون به شرح زیر است:


    // حذف یک آیتم
    @SuppressWarnings("unchecked")
    public static void test4() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // معامله
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // بارگذاری آیتم A1
        Article newarticle1 = em.find(Article.class, articleA1.getId());
        // حذف آیتم A1 (در حال حاضر هیچ دسته‌ای بارگذاری نشده است)
        em.remove(newarticle1);
        //toplink: آیتم باید از دسته‌بندی خود حذف شود، در غیر این صورت test6 از کار می‌افتد
        // hibernate: این ضروری نیست
        newarticle1.getCategorie().getArticles().remove(newarticle1);
        // پایان تراکنش
        tx.commit();
        // دومپ مقالات
        dumpArticles();
}
  • آزمون ۴ آیتم A1 را حذف می‌کند
  • خط ۵: ما با یک زمینهٔ جدید و خالی شروع می‌کنیم
  • خط ۱۰: آیتم A1 به زمینه پایداری اضافه می‌شود. در آنجا با نام newarticle1 ارجاع داده خواهد شد.
  • خط ۱۲: از زمینه حذف می‌شود
  • خط ۱۵: دسته‌بندی‌های A، B و C و آیتم‌های A1، A2 و B1، اگرچه دیگر پایدار نیستند، با این حال همچنان در حافظه باقی مانده‌اند. آنها صرفاً از زمینه پایداری جدا شده‌اند. مورد A1 که بخشی از موارد در دسته A است، از آن جدا می‌شود. این کار امکان الحاق مجدد دسته A به زمینه پایداری را در مرحله بعد فراهم می‌کند. اگر این کار انجام نشود، دسته A با مجموعه‌ای از موارد، که یکی از آنها حذف شده است، مجدداً الحاق خواهد شد. به نظر نمی‌رسد این امر برای Hibernate مشکلی ایجاد کند، اما باعث کرش Toplink می‌شود.
  • خط ۱۹: ما همه آیتم‌ها را نمایش می‌دهیم تا بررسی کنیم که A1 ناپدید شده است.

نتایج به شرح زیر است:

1
2
3
4
main : ----------- test4
[articles]
Article[2,0,A2,1]
Article[3,0,B1,2]

مقاله A1 واقعاً ناپدید شده است.

2.4.6.3. Test5

این تست به شرح زیر است:


// تغییر ۱ آیتم
    public static void test5() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // معامله
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // اصلاح articleA2
        articleA2.setNom(articleA2.getNom() + "-");
        // articleA2 دوباره در زمینه پایداری قرار می‌گیرد
        em.merge(articleA2);
        //پایان تراکنش
        tx.commit();
        // خروجی آیتم
        dumpArticles();
    }
  • آزمون ۵ نام آیتم A2 را تغییر می‌دهد
  • خط ۴: ما با یک زمینهٔ جدید و خالی شروع می‌کنیم
  • خط ۹: نام آیتم جداشده A2 به «A2-» تغییر می‌کند.
  • خط ۱۱: آیتم جداشده A2 دوباره به زمینه پایداری متصل می‌شود. توجه داشته باشید که A2 همچنان یک شی جداشده باقی می‌ماند. این شیء em.merge (articleA2) است که اکنون بخشی از زمینه پایداری (persistence context) شده است. این شیء طبق معمول در یک متغیر ذخیره نشده است. بنابراین به آن دسترسی وجود ندارد.
  • خط ۱۳: همگام‌سازی زمینه پایداری با پایگاه داده. آیتم A2 قرار است در پایگاه داده اصلاح شود و شماره نسخه آن از N به N+1 تغییر کند. نسخه حافظه جداشده articleA2 دیگر معتبر نیست. همین امر در مورد شیء جداشده‌ای که نمایندهٔ دستهٔ A است نیز صدق می‌کند، زیرا در میان آیتم‌های آن، articleA2 وجود دارد.
  • خط ۱۵: تمام آیتم‌ها برای تأیید تغییر نام آیتم A2 نمایش داده می‌شوند

نتایج به شرح زیر است:

1
2
3
4
main : ----------- test5
[articles]
Article[2,1,A2-,1]
Article[3,0,B1,2]

مورد A2 واقعاً نام‌گذاری مجدد شده است.

2.4.6.4. Test6

این تست به شرح زیر است:


// تغییر یک دسته و آیتم‌های آن
    public static void test6() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // معامله
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // بارگذاری دسته‌بندی
        categorieA = em.find(Categorie.class, categorieA.getId());
        //فهرست اقلام در دسته A
        for (Article a : categorieA.getArticles()) {
            a.setNom(a.getNom() + "-");
        }
        //تغییر نام دسته‌بندی
        categorieA.setNom(categorieA.getNom() + "-");
        //پایان تراکنش
        tx.commit();
        // خروجی فهرست دسته‌بندی‌ها و اقلام
        dumpCategories();
        dumpArticles();
}
  • آزمون ۶ نام دسته‌بندی A و تمام آیتم‌های آن را تغییر می‌دهد
  • خط ۴: ما با یک زمینهٔ جدید و خالی شروع می‌کنیم
  • خط ۹: ما دسته A را از پایگاه داده بازیابی می‌کنیم. ما merge را روی شی جداشده categorieA انجام نمی‌دهیم زیرا می‌دانیم که این شی به آیتم A2 ارجاع دارد که منسوخ شده است. بنابراین از ابتدا شروع می‌کنیم.
  • خطوط ۱۱–۱۲: ما نام تمام آیتم‌های دسته A را تغییر می‌دهیم. بار دیگر، از رابطه معکوس OneToMany از طریق متد getArticles استفاده می‌کنیم.
  • خط ۱۵: نام دسته‌بندی نیز تغییر می‌کند
  • خط ۱۷: پایان تراکنش. زمینه با پایگاه داده همگام‌سازی می‌شود. تمام اشیاء در زمینه که تغییر کرده‌اند در پایگاه داده به‌روزرسانی خواهند شد.
  • خطوط ۲۱–۲۲: آیتم‌ها و دسته‌بندی‌ها برای تأیید نمایش داده می‌شوند

نتایج به شرح زیر است:

1
2
3
4
5
6
7
8
main : ----------- test6
[categories]
Categorie[1,2,A-]
Categorie[2,0,B]
Categorie[3,0,C]
[articles]
Article[2,2,A2--,1]
Article[3,0,B1,2]

مورد A2 بار دیگر نام خود را تغییر داده است، و همچنین دسته A نیز همین کار را انجام داده است.

2.4.6.5. Test7

این آزمون به شرح زیر است:


// حذف یک دسته‌بندی
    public static void test7() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // معامله
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // پایداری catégorieB و آبشاری (ادغام) موارد مرتبط
        Categorie mergedcategorieB = em.merge(categorieB);
        // حذف دسته‌بندی و، به‌صورت آبشاری (حذف)، آیتم‌های مرتبط
        em.remove(mergedcategorieB);
        //پایان تراکنش
        tx.commit();
        // خروجی فهرست دسته‌بندی‌ها و آیتم‌ها
        dumpCategories();
        dumpArticles();
    }
  • آزمون ۷ دستهٔ B و در نتیجه آیتم‌های آن را حذف می‌کند
  • خط ۴: ما با یک زمینهٔ جدید و خالی شروع می‌کنیم
  • خط ۹: دسته‌بندی B در حافظه به‌عنوان یک شی جداشده از زمینه پایداری وجود دارد. این شی دوباره (merge) به زمینه پایداری ادغام می‌شود. در نتیجه، آیتم‌های آن (آیتم B1) تحت عملیات merge قرار گرفته و بدین ترتیب دوباره به زمینه پایداری بازگردانده می‌شوند.
  • خط ۱۱: اکنون که دسته‌بندی B در زمینه قرار دارد، می‌توان آن را حذف کرد (remove). در نتیجه، مقالات آن نیز تحت عملیات remove قرار خواهند گرفت. این عملیات ممکن است زیرا عملیات merge در خط ۹ آنها را مجدداً به زمینه پایداری بازگرداند.
  • خط ۱۳: پایان تراکنش. زمینه همگام‌سازی خواهد شد. اشیاء موجود در زمینه که عملیات remove روی آن‌ها انجام شده است، از پایگاه داده حذف خواهند شد.
  • خطوط ۱۵–۱۶: آیتم‌ها و دسته‌بندی‌ها برای تأیید نمایش داده می‌شوند

نتایج به شرح زیر است:

1
2
3
4
5
6
main : ----------- test7
[categories]
Categorie[1,2,A-]
Categorie[3,0,C]
[articles]
Article[1,2,A2--,1]

کategori B و آیتم B1 در واقع حذف شده‌اند.

2.4.6.6. Test8

این آزمون به شرح زیر است:


// پرس‌وجوها
    @SuppressWarnings("unchecked")
    public static void test8() {
        // زمینه پایداری جدید
        EntityManager em = getNewEntityManager();
        // معامله
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // فهرست اقلام در دسته‌بندی A
        List articles = em
                .createQuery(
                        "select a from Categorie c join c.articles a where c.nom like 'A%' order by a.nom asc")
                .getResultList();
        //نماهای محصول
        System.out.println("Articles de la catégorie A");
        for (Object a : articles) {
            System.out.println(a);
        }
        //پایان تراکنش
        tx.commit();
    }
  • آزمون ۷ نشان می‌دهد چگونه می‌توان آیتم‌ها را از یک دسته‌بندی بدون استفاده از رابطه معکوس بازیابی کرد. این نشان می‌دهد که رابطه معکوس بنابراین ضروری نیست.
  • خط ۴: ما با یک زمینهٔ جدید و خالی شروع می‌کنیم
  • خط ۱۰: یک پرس‌وجو JPQL که تمام مقالات در یک دسته‌بندی را که نامشان با A شروع می‌شود، بازیابی می‌کند
  • خطوط ۱۵–۱۷: نمایش نتیجهٔ پرس‌وجو.

نتایج به شرح زیر است:

1
2
3
main : ----------- test8
Articles de la catégorie A
Article[2,2,A2--,1]

2.4.7. پروژه ۲ اکلیپس/هایبرنت

ما پروژه Eclipse / Hibernate را کپی و پیست می‌کنیم تا نکته‌ای در مورد مفهوم رابطهٔ اصلی و رابطهٔ معکوس که در اطراف @ManyToOne ایجاد کرده‌ایم را روشن سازیم. (اصلی) از @Entity [Article] و رابطه معکوس @OneToMany (معکوس) از @Entity [Categorie]. ما می‌خواهیم نشان دهیم که اگر این رابطه دوم به‌عنوان معکوس رابطه اول اعلام نشود، طرح‌واره تولیدشده برای پایگاه داده کاملاً با طرح‌واره تولیدشده قبلی متفاوت خواهد بود.

در [1]، پروژه جدید اکلیپس. در [2] کد جاوا قرار دارد؛ در [3] اسکریپت ant قرار دارد که طرحواره پایگاه داده SQL را تولید خواهد کرد. پروژه در پوشهٔ examples قرار دارد. آن را وارد خواهیم کرد.

ما فقط @Entity [Categorie] را تغییر می‌دهیم تا رابطه @OneToMany آن با @کلاس Entity [Article] دیگر به‌عنوان معکوس رابطه @ManyToOne که کلاس @Entity [Article] با کلاس @Entity [Categorie] دارد، اعلام نشده است:


...
@Entity
@Table(name="jpa05_hb_categorie")
public class Categorie implements Serializable {

    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @SuppressWarnings("unused")
    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    //رابطه غیرمعکوس OneToMany (بدون 'mappedby') دسته‌بندی (یک) -> آیتم (چند)
    // پیاده‌سازی شده توسط یک جدول الحاق Categorie_Article به گونه‌ای که، از یک دسته‌بندی شروع شود
    // می‌توان به مقالات آن دسته دسترسی داشت
    @OneToMany(cascade=CascadeType.ALL, fetch=FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

    // تولیدکنندگان
...
  • خطوط ۱۸–۲۲: ما همچنان می‌خواهیم قابلیت یافتن مقالات در یک دسته‌بندی معین را با استفاده از رابطه @OneToMany در خط ۲۱ حفظ کنیم. با این حال، ما می‌خواهیم تأثیر ویژگی mappedBy را درک کنیم که یک رابطه را معکوس رابطهٔ اصلی تعریف‌شده در جای دیگری، در یک @Entity دیگر، می‌سازد. در اینجا، mappedBy حذف شده است.

ما وظیفه ant-DLL (به بخش 2.1.7 مراجعه کنید) را با SGBD و MySQL5 اجرا می‌کنیم. اسکیمای حاصل به شرح زیر است:

لطفاً به نکات زیر توجه کنید:

  • جدول جدیدی به نام [categorie_article] [1] ایجاد شده است. این جدول قبلاً وجود نداشت.
  • این یک جدول الحاق بین جداول [categorie] [2] و [article] [3] است. اگر اشیاء مقاله a1 و a2 به دسته c1 تعلق داشته باشند، ردیف‌های زیر در جدول الحاق یافت می‌شوند:
[c1,a1]
[c1,a2]

که در آن c1، a1 و a2 کلیدهای اصلی اشیاء متناظر هستند.

  • جدول الحاق [categorie_article] [1] توسط Hibernate ایجاد شد تا با داشتن یک شیء Category c، بتوان اشیاء Article a متعلق به c را بازیابی کرد. این رابطه @OneToMany بود که ایجاد این جدول را تحریک کرد. از آنجا که این رابطه به عنوان معکوس رابطه اصلی @ManyToOne برای @Entity Article اعلام نشده بود، Hibernate نمی‌دانست که می‌تواند از این رابطه اصلی برای بازیابی مقالات یک دسته‌بندی c استفاده کند. بنابراین راه دیگری یافت.
  • این مثال به روشن شدن مفاهیم روابط principale و inverse کمک می‌کند. یکی (معکوس) از ویژگی‌های دیگری (اصلی) استفاده می‌کند.

شمای SQL در این پایگاه داده برای MySQL5 به شرح زیر است:


    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D26D17756;

    alter table jpa05_hb_categorie_jpa06_hb_article 
        drop 
        foreign key FK79D4BA1D424C61C9;

    alter table jpa06_hb_article 
        drop 
        foreign key FK4547168FECCE8750;

    drop table if exists jpa05_hb_categorie;

    drop table if exists jpa05_hb_categorie_jpa06_hb_article;

    drop table if exists jpa06_hb_article;

    create table jpa05_hb_categorie (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa05_hb_categorie_jpa06_hb_article (
        jpa05_hb_categorie_id bigint not null,
        articles_id bigint not null,
        primary key (jpa05_hb_categorie_id, articles_id),
        unique (articles_id)
    ) ENGINE=InnoDB;

    create table jpa06_hb_article (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30),
        categorie_id bigint not null,
        primary key (id)
    ) ENGINE=InnoDB;

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D26D17756 (jpa05_hb_categorie_id), 
        add constraint FK79D4BA1D26D17756 
        foreign key (jpa05_hb_categorie_id) 
        references jpa05_hb_categorie (id);

    alter table jpa05_hb_categorie_jpa06_hb_article 
        add index FK79D4BA1D424C61C9 (articles_id), 
        add constraint FK79D4BA1D424C61C9 
        foreign key (articles_id) 
        references jpa06_hb_article (id);

    alter table jpa06_hb_article 
        add index FK4547168FECCE8750 (categorie_id), 
        add constraint FK4547168FECCE8750 
        foreign key (categorie_id) 
references jpa05_hb_categorie (id);
  • رده‌های ۱۹–۲۴: ایجاد جدول [categorie]؛ و رده‌های ۳۳–۳۹: ایجاد جدول [article]. توجه داشته باشید که این‌ها با موارد موجود در مثال قبلی یکسان هستند.
  • سطور 26–31: ایجاد جدول الحاق [categorie_article] به دلیل وجود رابطه غیرمعکوس @OneToMany از @Entity Categorie. رده‌های این جدول از نوع [c,a] هستند، که در آن c کلید اصلی یک دسته‌بندی c و a کلید اصلییک آیتم a متعلق به دسته c. کلید اصلی این جدول الحاقی از دو کلید اصلی [c,a] تشکیل شده که با هم الحاق شده‌اند (ردیف 29).
  • خطوط ۴۱–۴۵: قید کلید خارجی از جدول [categorie_article] به جدول [categorie]
  • خطوط ۴۷–۵۱: محدودیت کلید خارجی از جدول [categorie_article] به جدول [article]
  • خطوط ۵۳–۵۷: قید کلید خارجی از جدول [article] به جدول [categorie]

از خوانندگان دعوت می‌شود تا تست‌های [InitDB] و [Main] را اجرا کنند. این تست‌ها همان نتایج قبلی را تولید می‌کنند. با این حال، طرحواره پایگاه داده اضافی است و عملکرد در مقایسه با نسخه قبلی کاهش خواهد یافت. احتمالاً ارزش دارد که این مسئله روابط معکوس و اصلی را با جزئیات بیشتری بررسی کنیم تا ببینیم آیا پیکربندی جدید ممکن است به تضادهایی نیز منجر شود که از این واقعیت ناشی می‌شوند که دو رابطه مستقل وجود دارند که یک چیز را نشان می‌دهند: رابطه چند به یک بین جدول [article] و جدول [categorie].

اکنون از پیاده‌سازی JPA / Toplink استفاده می‌کنیم:

پروژه Eclipse با Toplink کپی‌ای از پروژه Eclipse با Hibernate، نسخهٔ ۱ است:

کد جاوا با پروژه قبلی Hibernate – نسخه ۱ – یکسان است. محیط (کتابخانه‌ها – persistence.xml – DBMS – پوشه‌های conf و ddl – اسکریپت Ant) همان است که در بخش 2.1.15.2 مورد بحث قرار گرفته است. پروژه اکلیپس [3] در پوشه مثال‌ها [4] قرار دارد. ما آن را وارد خواهیم کرد.

فایل <persistence.xml> [2] در یک جنبه تغییر یافته است، یعنی انتیت‌های اعلام‌شده:


        ...
        <!--کلاس‌های پایدار -->
        <class>entites.Categorie</class>
        <class>entites.Article</class>
...
  • خطوط ۳ و ۴: دو موجودیت مدیریت‌شده

اجرای [InitDB] با SGBD و MySQL5 نتایج زیر را تولید می‌کند:

در [1]، خروجی کنسول؛ در [2]، دو جدول تولیدشده [jpa05_tl]؛ و در [3]، اسکریپت‌های تولیدشده SQL. محتوای آن‌ها به شرح زیر است:

create.sql


CREATE TABLE jpa05_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa05_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
ALTER TABLE jpa05_tl_article ADD CONSTRAINT FK_jpa05_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa05_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

drop.sql


ALTER TABLE jpa05_tl_article DROP FOREIGN KEY FK_jpa05_tl_article_categorie_id
DROP TABLE jpa05_tl_article
DROP TABLE jpa05_tl_categorie
DELETE FROM SEQUENCE WHERE SEQ_NAME = 'SEQ_GEN'

اجرای [Main] بدون خطا به پایان رسید.

این پروژهٔ Eclipse با کلون کردن پروژهٔ قبلی ایجاد شده است. از آنجا که با استفاده از Hibernate ساخته شده است، ویژگی mappedBy از رابطهٔ @OneToMany در @Entity Categorie حذف شده است.


@Entity
@Table(name = "jpa06_tl_categorie")
public class Categorie implements Serializable {

    // fields
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Version
    private int version;

    @Column(length = 30)
    private String nom;

    //رابطه غیرمعکوس OneToMany (بدون 'mappedby') Category (یک) ->
    // مقاله (بسیار)
    // پیاده‌سازی شده توسط یک جدول پیوند Categorie_Article به طوری که، از
    //یک دسته‌بندی
    // امکان دسترسی به چندین آیتم وجود دارد
    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private Set<Article> articles = new HashSet<Article>();

اسکیمای SQL که برای MySQL5 تولید شده است، به شرح زیر است:

create.sql


CREATE TABLE jpa06_tl_categorie (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), PRIMARY KEY (ID))
CREATE TABLE jpa06_tl_categorie_jpa06_tl_article (Categorie_ID BIGINT NOT NULL, articles_ID BIGINT NOT NULL, PRIMARY KEY (Categorie_ID, articles_ID))
CREATE TABLE jpa06_tl_article (ID BIGINT NOT NULL, VERSION INTEGER, NOM VARCHAR(30), categorie_id BIGINT NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID FOREIGN KEY (articles_ID) REFERENCES jpa06_tl_article (ID)
ALTER TABLE jpa06_tl_categorie_jpa06_tl_article ADD CONSTRAINT jpa06_tl_categorie_jpa06_tl_article_Categorie_ID FOREIGN KEY (Categorie_ID) REFERENCES jpa06_tl_categorie (ID)
ALTER TABLE jpa06_tl_article ADD CONSTRAINT FK_jpa06_tl_article_categorie_id FOREIGN KEY (categorie_id) REFERENCES jpa06_tl_categorie (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)
  • خط ۲: جدول الحاقی که رابطه غیرمعکوس قبلی @OneToMany را نشان می‌دهد.

اجرای [InitDB] بدون خطا پیش می‌رود، اما اجرای [Main] در تست ۷ با لاگ‌های زیر (FINEST) دچار خرابی می‌شود:

main : ----------- test7
[TopLink Finer]: 2007.06.01 01:41:48.734--ServerSession(15290002)--Thread(Thread[main,5,main])--client acquired
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--ادغام کلون با ارجاعات Category[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--ثبت شیء موجود Article[6,1,B1]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--ثبت شیء موجود Category[5,1,B]
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--عملیات حذف بر روی: Categorie[5,1,B] انجام شد
[TopLink Finest]: 2007.06.01 01:41:48.734--UnitOfWork(26285048)--Thread(Thread[main,5,main])--عملیات حذف بر روی: Article[6,1,B1] انجام شده است
[TopLink Finer]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--آغاز commit واحد کار
[TopLink Finer]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Connection(6330655)--Thread(Thread[main,5,main])--begin transaction
[TopLink Finest]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--اجرای پرس‌وجو DeleteObjectQuery(مقاله [6,1,B1])
[TopLink Fine]: 2007.06.01 01:41:48.750--ClientSession(15014700)--Connection(6330655)--Thread(Thread[main,5,main])--DELETE FROM jpa06_tl_article WHERE ((QZXW2HTMLCSuqZQX = ?) AND (VERSION = ؟))
    bind => [6, 1]
[TopLink Warning]: 2007.06.01 01:41:48.750--UnitOfWork(26285048)--Thread(Thread[main,5,main])--Local Exception Stack: 
Exception [TOPLINK-4002] (Oracle TopLink Essentials - 2.0 (Build b41-beta2 (03/30/2007))): oracle.toplink.essentials.exceptions.DatabaseException
Internal Exception: com.mysql.jdbc.exceptions.MySQLIntegrityConstraintViolationException: Cannot delete or update a parent row: a foreign key constraint fails (`jpa/jpa06_tl_categorie_jpa06_tl_article`, CONSTRAINT `FK_jpa06_tl_categorie_jpa06_tl_article_articles_ID` FOREIGN KEY (`articles_ID`) REFERENCES `jpa06_tl_article` (`ID`))
Error Code: 1451
Call: DELETE FROM jpa06_tl_article WHERE ((ID = ?) AND (VERSION = ?))
    bind => [6, 1]
  • خط ۳: merge برای دسته B
  • خط ۴: آیتم وابسته B1 در زمینه قرار می‌گیرد
  • خط ۵: همین موضوع در مورد خود دسته‌بندی B نیز صدق می‌کند
  • خط ۶: remove در دسته B
  • خط ۷: remove برای آیتم B1 (به صورت آبشاری)
  • خط ۸: commit از تراکنش توسط کد جاوا درخواست می‌شود
  • خط ۹: یک تراکنش آغاز می‌شود – بنابراین ظاهراً هنوز آغاز نشده بود.
  • خط ۱۰: آیتم B1 قرار است توسط عملیاتی با شناسه DELETE در جدول [article] حذف شود. مشکل از همینجاست. جدول الحاقی [categorie_article] به سطر B1 در جدول [article] ارجاع می‌دهد. حذف B1 از [article] منجر به نقض قید کلید خارجی می‌شود.
  • خطوط ۱۳ به بعد: استثنا رخ می‌دهد

چه نتیجه‌ای می‌توان گرفت؟

  • بار دیگر، یک مشکل قابل حمل بودن بین Hibernate و Toplink وجود دارد: Hibernate این تست را قبلاً گذرانده بود
  • TopLink زمانی که دو رابطه در واقع معکوس یکدیگر هستند، اما یکی به عنوان رابطه اصلی و دیگری به عنوان معکوس اعلام نشده است، به خوبی با این موضوع برخورد نمی‌کند. این موضوع قابل قبول است زیرا این سناریو در واقع نشان‌دهنده یک خطای پیکربندی است. در مثال ما، جدول [article] هیچ ارتباطی با جدول الحاقی [categorie_article] ندارد. بنابراین طبیعی به نظر می‌رسد که هنگام انجام عملیاتی روی جدول [articleToplink نباید تلاش کند تا با جدول [categorie_article] کار کند.

2.5. مثال ۵: رابطه چند به چند با یک جدول الحاق صریح

2.5.1. شمای پایگاه داده

  • در [1]، پایگاه داده MySQL5

ما پیش از این با جداول [personne]، [2]، [adresse] و [3] آشنا هستیم. این جداول در بخش 2.3.1 مورد بحث قرار گرفتند. ما نسخه‌ای را در نظر می‌گیریم که در آن آدرس شخص در یک جدول جداگانه ذخیره می‌شود: [adresse] و [3]. در جدول [personne]، رابطهٔ متصل‌کنندهٔ شخص به آدرسش با قید کلید خارجی نشان داده شده است.

یک فرد در فعالیت‌هایی مشارکت می‌کند. این فعالیت‌ها در جداول [activite] و [4] ذخیره می‌شوند. یک فرد ممکن است در چندین فعالیت مشارکت کند و یک فعالیت ممکن است توسط چندین فرد انجام شود. بنابراین یک رابطه چند به چند، جداول [personne] و [activite] را به هم متصل می‌کند. این رابطه توسط جدول الحاقی [personne_activite] [5] پیاده‌سازی شده است.

2.5.2. ابجکت‌های @Entity که نمایندهٔ پایگاه داده هستند

جدول‌های فوق با anotationهای @Entity زیر نمایش داده می‌شوند:

  • @Entity Personne نمایانگر جدول [personne] خواهد بود
  • @Entity Adresse نمایانگر جدول [adresse] خواهد بود
  • @Entity Activite نمایانگر جدول [activite] خواهد بود
  • @Entity PersonneActivite نمایانگر جدول [personne_activite] خواهد بود

روابط بین این اشیاء به شرح زیر است:

  • یک رابطه یک‌به‌یک بین موجوده Personne و موجوده Adresse برقرار است: یک شخص p دارای یک آدرس a است. نهاد Personne که کلید خارجی را در خود دارد، رابطه اصلی را خواهد داشت؛ نهاد Adresse رابطه معکوس را خواهد داشت.
  • یک رابطهٔ چندبه‌چند، موجودیت‌های Personne و Activite را به هم مرتبط می‌کند: یک شخص چندین فعالیت دارد و یک فعالیت توسط چندین شخص انجام می‌شود. این رابطه می‌تواند مستقیماً با استفاده از یک @ManyToMany annotation در هر یک از دو انتیتي پیاده‌سازی شود، به گونه‌ای که یکی به عنوان معکوس دیگری اعلام گردد. این راه‌حل بعداً بررسی خواهد شد. در اینجا، ما رابطه چند به چند را با استفاده از دو رابطه یک به چند پیاده‌سازی می‌کنیم:
    • یک رابطه یک‌به‌چند که موجودیت Personne را به موجودیت PersonneActivite پیوند می‌دهد: یک (One) ردیف در جدول [personne] توسط چندین (Many) ردیف در جدول [personne_activite] ارجاع داده می‌شود. جدول [personne_activite] که کلید خارجی را در خود جای داده است، رابطه اصلی @ManyToOne را خواهد داشت، در حالی که موجودیت Personne رابطه معکوس @OneToMany را خواهد داشت.
    • یک رابطه یک‌به‌چند که موجوده Activite را به موجوده PersonneActivite پیوند می‌دهد: یک (One) ردیف در جدول [activite] توسط چندین (Many) ردیف در جدول [personne_activite] ارجاع داده می‌شود. جدول [personne_activite] که کلید خارجی را در خود جای داده است، رابطه اصلی @ManyToOne را خواهد داشت، در حالی که موجودیت Activite رابطه معکوس @OneToMany را خواهد داشت.

@Entity Personne به شرح زیر است:


@Entity
@Table(name = "jpa07_hb_personne")
public class Personne implements Serializable {

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // رابطهٔ اصلی Person (یک) -> Address (یک)
    // پیاده‌سازی‌شده توسط کلید خارجی Person (adresse_id) -> Address
    // افزودن آبشاری Person -> افزودن Address
    // به‌روزرسانی آبشاری: Person -> Address
    // حذف آبشاری Person -> حذف Address
    //یک شخص باید یک آدرس داشته باشد (nullable=false)
    // یک آدرس فقط متعلق به یک شخص است (unique=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // شخص (یک) -> PersonneActivite (چند)
    //معکوس رابطه موجود PersonneActivite (چند) → Person (یک)
    // حذف آبشاری: Person -> حذف PersonneActivite
    @OneToMany(mappedBy = "personne", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> activites = new HashSet<PersonneActivite>();

    //سازنده‌ها

این @Entity شناخته‌شده است. ما تنها در مورد روابطی که با سایر انتیته‌ها دارد، توضیح می‌دهیم:

  • سطور ۳۰–۳۹: یک رابطه یک‌به‌یک @OneToOne با @Entity Adresse، که با یک کلید خارجی [adresse_id] (خط ۳۸) نشان داده شده است که جدول [personne] بر روی جدول [adresse] خواهد داشت.
  • خطوط ۴۱–۴۵: یک رابطه یک‌به‌چند @OneToMany با @Entity PersonneActivite. یک شخص توسط چندین سطر در جدول الحاق [personne_activite] ارجاع می‌شود که توسط @Entity PersonneActivite نمایانده می‌شود. این اشیاء PersonneActivite در یک نوع Set<PersonneActivite> قرار داده خواهند شد، که در آن PersonneActivite نوعی است که به زودی تعریف خواهیم کرد.
  • خط ۴۴: رابطه یک‌به‌چند که در اینجا تعریف شده است، معکوس یک رابطه اصلی است که روی فیلد personne از @Entity PersonneActivite (کلیدواژه mappedBy) تعریف شده است. برای حذف‌ها یک آبشاری Person -> Activity وجود دارد: حذف یک شخص p منجر به حذف اشیاء پایدار از نوع PersonneActivite موجود در مجموعه p.activites می‌شود.

@Entity Adresse به شرح زیر است:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • خطوط ۲۸–۲۹: رابطه @OneToOne، که معکوس رابطه @OneToOne است، به @Entity Personne (خطوط ۳۷–۳۸ از Personne) اشاره می‌کند.

@Entity Activite به شرح زیر است


@Entity
@Table(name = "jpa07_hb_activite")
public class Activite implements Serializable {

    // fields
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    // رابطه فعالیت (یک) -> PersonneActivite (چند)
    //معکوس رابطه موجود PersonneActivite (چند) → Activity (یک)
    // حذف آبشاری: Activity → حذف PersonneActivite
    @OneToMany(mappedBy = "activite", cascade = { CascadeType.REMOVE })
    private Set<PersonneActivite> personnes = new HashSet<PersonneActivite>();

  • خطوط ۶–۹: کلید اصلی فعالیت
  • خطوط ۱۱–۱۳: شماره نسخه فعالیت
  • خطوط ۱۵–۱۶: نام فعالیت
  • رده‌های ۱۸–۲۲: رابطه یک‌به‌چند که @Entity Activite را به @Entity PersonneActivite متصل می‌کند: یک فعالیت (One) توسط چندین (Many) ردیف در جدول پیوندی [personne_activite] که توسط @Entity PersonneActivite نمایانده می‌شود، ارجاع داده شده است. این اشیاء PersonneActivite در یک Set<PersonneActivite> قرار داده خواهند شد.
  • خط ۲۲: رابطه یک‌به‌چند که در اینجا تعریف شده، معکوس یک رابطه اصلی است که روی فیلد activite در @Entity PersonneActivite (کلید mappedBy) تعریف شده است. یک آبشستگی «Activity → PersonneActivite» در هنگام حذف وجود دارد: حذف جدول [activite] ازیک فعالیت a منجر به حذف جدول الحاق [personne_activite] عناصر پایدار از نوع PersonneActivite یافت‌شده در مجموعه a.personnes خواهد شد.

@Entity PersonneActivite به شرح زیر است:


@Entity
//جدول پیوند
@Table(name = "jpa07_hb_personne_activite")
public class PersonneActivite {

    @Embeddable
    public static class Id implements Serializable {
        // اجزای کلید مرکب
        // به یک Person اشاره می‌کند
        @Column(name = "PERSONNE_ID")
        private Long personneId;

        // اشاره به یک فعالیت می‌کند
        @Column(name = "ACTIVITE_ID")
        private Long activiteId;

        // سازنده‌ها
...

        // گیرنده‌ها و تنظیم‌کننده‌ها
...
        //toString
        public String toString() {
            return String.format("[%d,%d]", getPersonneId(), getActiviteId());
        }
    }

    // fields of class Personne_Activite
    // کلید مرکب
    @EmbeddedId
    private Id id = new Id();

    // رابطهٔ اصلی QZXW2HTMLCUGVyc29ubVBY3Rpdml0ZQZQX (چند) -> Person (یک)
    // پیاده‌سازی شده توسط کلید خارجی: personneId (PersonneActivite (بسیار) -> Person (یک)
    // personneId همچنین جزئی از کلید اصلی مرکب است
    // JPA نباید این کلید خارجی را مدیریت کند (insertable = false, updatable = false) زیرا این مورد توسط خود برنامه در سازنده‌اش (constructor) مدیریت می‌شود
    @ManyToOne
    @JoinColumn(name = "PERSONNE_ID", insertable = false, updatable = false)
    private Personne personne;

    // رابط اصلی PersonneActivite -> Activity
    //توسط کلید خارجی activiteId پیاده‌سازی شده است (PersonneActivite (بسیار) → Activity (یک)
    // activiteId همچنین جزئی از کلید اصلی مرکب است
    // JPA نباید این کلید خارجی را مدیریت کند (insertable = false, updatable = false) زیرا این مورد توسط خود برنامه در سازندش (constructor) مدیریت می‌شود
    @ManyToOne()
    @JoinColumn(name = "ACTIVITE_ID", insertable = false, updatable = false)
    private Activite activite;

    //سازنده‌ها
    public PersonneActivite() {

    }

    public PersonneActivite(Personne p, Activite a) {
        // کلیدهای خارجی توسط برنامه تعریف می‌شوند
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // ارتباطات دوطرفه
        this.setPersonne(p);
        this.setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
    }

    // گیرنده و تنظیم‌کننده
...
    //toString
    public String toString() {
        return String.format("[%s,%s,%s]", getId(), getPersonne().getNom(), getActivite().getNom());
    }
}

این کلاس پیچیده‌تر از موارد قبلی است.

  • جدول [personne_activite] شامل سطرهایی به شکل [p,a] است، که در آن p کلید اصلی برای یک شخص و a کلید اصلی برای یک فعالیت است. هر جدول باید یک کلید اصلی داشته باشد و [personne_activite] نیز از این قاعده مستثنی نیست. تا کنون، ما کلیدهای اصلی‌ای را تعریف کرده‌ایم که به‌صورت پویا توسط SGBD تولید می‌شوند. می‌توانیم همین کار را اینجا نیز انجام دهیم. ما قصد داریم از یک تکنیک متفاوت استفاده کنیم که در آن خودِ برنامه مقادیر کلید اصلی جدول را تعریف می‌کند. در اینجا، یک سطر [p1,a1] نشان می‌دهد که یک شخص p1 در فعالیت a1 مشارکت می‌کند. این سطر یکسان نمی‌تواند برای بار دوم در جدول ظاهر شود. بنابراین، جفت (p, a) یک نامزد خوب برای کلید اصلی است. این به عنوان کلید اصلی مرکب شناخته می‌شود.
  • رده‌های ۳۰–۳۱: کلید اصلی مرکب. انوتیشن @EmbeddedId (که قبلاً @Id بود) مشابه انوتیشن @Embedded است که به فیلد Adresse یک شخص اعمال می‌شود. در مورد دوم، این بدان معنا بود که فیلد Adresse متعلق به یک کلاس خارجی بود اما باید در همان جدولی که شخص در آن قرار داشت، درج می‌شد. در اینجا نیز معنا یکسان است؛ با این حال، برای نشان دادن اینکه با کلید اصلی سر و کار داریم، نشانه به @EmbeddedId تغییر می‌کند.
  • خط ۳۱: به محض ایجاد شیء [PersonneActivite]، یک شیء خالی نماینده کلید اصلی id ایجاد می‌شود. کلاسی که نماینده کلید اصلی است، در خطوط ۷ تا ۲۶ به عنوان یک کلاس عمومی، ایستا و داخلی در داخل کلاس [PersonneActivite] تعریف شده است. عمومی و ایستا بودن آن توسط Hibernate الزامی است. اگر `public static` را با `private,` جایگزین کنیم، یک استثنا پرتاب می‌شود و پیام خطای مربوطه نشان می‌دهد که Hibernate تلاش کرده است عبارت `new PersonneActivite$Id` را اجرا کند. بنابراین کلاس `Id` باید هم استاتیک و هم پابلیک باشد.
  • خط ۶: کلاس Id کلید اصلی، با @Embeddable تعریف شده است. به یاد داشته باشید که کلید اصلی id در خط ۳۱ با @EmbeddedId تعریف شده بود. بنابراین، کلاس متناظر باید دارای anotation @Embeddable باشد.
  • ما بیان کرده‌ایم که کلید اصلی جدول [personne_activite] شامل جفت (p, a) است، که در آن p کلید اصلی یک شخص و a کلید اصلی یک فعالیت است. دو عنصر (p, a) کلید مرکب در خط ۱۱ (personneId) و خط ۱۵ (activiteId) یافت می‌شوند. ستون‌های مرتبط با این دو فیلد به نام‌های PERSONNE_ID برای شخص و ACTIVITE_ID برای فعالیت نامگذاری شده‌اند.
  • ردیف ۳۱: کلید اصلی با استفاده از دو ستون آن (PERSONNE_ID, ACTIVITE_ID) تعریف شده است. هیچ ستون دیگری در جدول [personne_activite] وجود ندارد. تنها کاری که باقی مانده تعریف روابط بین @Entity PersonneActivite که در حال حاضر در حال توصیف آن هستیم و سایر @Entityها در طرحواره رابطه‌ای است. این روابط منعکس‌کننده محدودیت‌های کلید خارجی است که جدول [personne_activite] با سایر جدول‌ها دارد.
  • خطوط ۳۳–۳۹: کلید خارجی از جدول [personne_activite] به جدول [personne] را تعریف می‌کند
  • خط ۳۷: این رابطه از نوع @ManyToOne است: یک (One) ردیف در جدول [personne] توسط چندین (Many) ردیف در جدول [personne_activite] ارجاع داده می‌شود.
  • خط ۳۸: ستون کلید خارجی نام‌گذاری شده است. از همان نامی استفاده شده که برای مؤلفه «شخص» (person) کلید خارجی (خط ۱۰) تعیین شده است. ویژگی‌های `insertable=false` و `updatable=false` برای جلوگیری از مدیریت کلید خارجی توسط Hibernate قرار داده شده‌اند. این در واقع بخشی از یک کلید اصلی است که توسط برنامه محاسبه می‌شود و Hibernate نباید در آن مداخله کند.
  • خطوط ۴۱–۴۷: کلید خارجی از جدول [personne_activite] به جدول [activite] تعریف می‌شود. توضیحات مشابه موارد قبلی است.
  • خطوط ۵۴–۶۳: سازنده (Constructor) برای یک شیء PersonneActivite بر اساس یک شخص p و یک فعالیت a. به یاد داشته باشید که وقتی یک شیء PersonneActivite ایجاد شد، کلید اصلی id در خط ۳۱ به یک شیء خالی Id اشاره می‌کرد. خطوط ۵۶–۵۷ برای هر یک از فیلدهای (personneId, activiteId) شیء Id یک مقدار اختصاص می‌دهند. این مقادیر به ترتیب کلیدهای اصلی شخص p و فعالیت a هستند که به‌عنوان پارامتر به سازنده ارسال شده‌اند. بنابراین کلید اصلی «id» (خط ۳۱) اکنون دارای مقدار است.
  • خط ۵۹: مقدار p به فیلد personne در خط ۳۹ اختصاص داده می‌شود
  • خط ۶۰: مقدار a به فیلد activite در خط ۴۷ اختصاص داده می‌شود.
  • یک شیء [PersonneActivite] اکنون ایجاد و مقداردهی اولیه شده است. ما روابط معکوس بین @Entity Personne (خط ۶۱) و Activite (خط ۶۲) و @Entity PersonneActivite که همین حالا ایجاد شده است را به‌روزرسانی می‌کنیم.

اکنون توصیف موجودیت‌های پایگاه داده را تکمیل کرده‌ایم. ما در یک وضعیت پیچیده اما متأسفانه رایج قرار داریم. خواهیم دید که پیکربندی دیگری برای لایه JPA وجود دارد که بخشی از این پیچیدگی را پنهان می‌کند: جدول پیوندی ضمنی شده و توسط لایه JPA ساخته و مدیریت می‌شود. ما در اینجا پیچیده‌ترین راه‌حل را انتخاب کرده‌ایم، اما راه‌حلی که به اسکیمای رابطه‌ای اجازه می‌دهد تکامل یابد. این امر بدین ترتیب امکان افزودن ستون به جدول الحاق را فراهم می‌کند، امری که در پیکربندی‌ای که در آن جدول الحاق یک @Entity صریح نیست، امکان‌پذیر نیست. [ref1] راه‌حلی را که در حال حاضر در حال بررسی آن هستیم، توصیه می‌کند. اطلاعاتی که توسعه این راه‌حل را ممکن ساخت، در [ref1] یافت شد.

2.5.3. پروژه اکلیپس / هایبرنت

پیاده‌سازی JPA که در اینجا استفاده شده، مربوط به Hibernate است. پروژه Eclipse برای تست‌ها به شرح زیر است:

 

Image

در [1] پروژه اکلیپس قرار دارد؛ در [2] کد جاوا قرار دارد. پروژه در [3] در پوشه مثال‌ها [4] قرار دارد. ما آن را وارد خواهیم کرد.

2.5.4. ایجاد پایگاه داده DDL

طبق دستورالعمل‌های بخش 2.1.7، خروجی DDL که برای SGBD و MySQL5 به دست آمده، به شرح زیر است:


alter table jpa07_hb_personne 
        drop 
        foreign key FKB5C817D45FE379D0;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B06CD852024;

    alter table jpa07_hb_personne_activite 
        drop 
        foreign key FKD3E49B0668C7A284;

    drop table if exists jpa07_hb_activite;

    drop table if exists jpa07_hb_adresse;

    drop table if exists jpa07_hb_personne;

    drop table if exists jpa07_hb_personne_activite;

    create table jpa07_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa07_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa07_hb_personne 
        add index FKB5C817D45FE379D0 (adresse_id), 
        add constraint FKB5C817D45FE379D0 
        foreign key (adresse_id) 
        references jpa07_hb_adresse (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B06CD852024 (ACTIVITE_ID), 
        add constraint FKD3E49B06CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa07_hb_activite (id);

    alter table jpa07_hb_personne_activite 
        add index FKD3E49B0668C7A284 (PERSONNE_ID), 
        add constraint FKD3E49B0668C7A284 
        foreign key (PERSONNE_ID) 
        references jpa07_hb_personne (id);
  • رده‌های ۲۱–۲۶: جدول [activite]
  • رده‌های ۲۸–۳۹: جدول [adresse]
  • خطوط ۴۱–۵۱: جدول [personne]
  • خطوط ۵۳–۵۷: جدول الحاق [personne_activite]. توجه کنید به کلید مرکب (خط ۵۶)
  • خطوط 59–63: کلید خارجی از جدول [personne] به جدول [adresse]
  • خطوط ۶۵–۶۹: کلید خارجی از جدول [personne_activite] به جدول [activite]
  • خطوط ۷۱–۷۵: کلید خارجی از جدول [personne_activite] به جدول [personne]

2.5.5. InitDB

کد برای [InitDB] به شرح زیر است:


package tests;

...
public class InitDB {

    //ثابت‌ها
    private final static String TABLE_PERSONNE_ACTIVITE = "jpa07_hb_personne_activite";

    private final static String TABLE_PERSONNE = "jpa07_hb_personne";

    private final static String TABLE_ACTIVITE = "jpa07_hb_activite";

    private final static String TABLE_ADRESSE = "jpa07_hb_adresse";

    public static void main(String[] args) throws ParseException {
        // زمینه پایداری
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");
        EntityManager em = null;
        //یک EntityManager از EntityManagerFactory بازیابی می‌شود
        // قبلی
        em = emf.createEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // درخواست
        Query sql1;
        // حذف رکوردها از جدول PERSONNE_ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE_ACTIVITE);
        sql1.executeUpdate();
        // حذف موارد از جدول PERSONNE
        sql1 = em.createNativeQuery("delete from " + TABLE_PERSONNE);
        sql1.executeUpdate();
        //حذف رکوردها از جدول ACTIVITE
        sql1 = em.createNativeQuery("delete from " + TABLE_ACTIVITE);
        sql1.executeUpdate();
        // حذف رکوردها از جدول ADRESSE
        sql1 = em.createNativeQuery("delete from " + TABLE_ADRESSE);
        sql1.executeUpdate();
        //ایجاد فعالیت‌ها
        Activite act1 = new Activite();
        act1.setNom("act1");
        Activite act2 = new Activite();
        act2.setNom("act2");
        Activite act3 = new Activite();
        act3.setNom("act3");
        // فعالیت‌های ماندگار
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // ایجاد افراد
        Personne p1 = new Personne("p1", "Paul", new SimpleDateFormat("dd/MM/yy").parse("31/01/2000"), true, 2);
        Personne p2 = new Personne("p2", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        Personne p3 = new Personne("p3", "Sylvie", new SimpleDateFormat("dd/MM/yy").parse("05/07/2001"), false, 0);
        // ایجاد آدرس‌ها
        Adresse adr1 = new Adresse("adr1", null, null, "49000", "Angers", null, "France");
        Adresse adr2 = new Adresse("adr2", "Les Mimosas", "15 av Foch", "49002", "Angers", "03", "France");
        Adresse adr3 = new Adresse("adr3", "x", "x", "x", "x", "x", "x");
        Adresse adr4 = new Adresse("adr4", "y", "y", "y", "y", "y", "y");
        // ارتباطات بین افراد و آدرس‌ها
        p1.setAdresse(adr1);
        adr1.setPersonne(p1);
        p2.setAdresse(adr2);
        adr2.setPersonne(p2);
        p3.setAdresse(adr3);
        adr3.setPersonne(p3);
        // پایداری افراد و در نتیجه آدرس‌های مرتبط
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        // پایداری آدرس a4 که به فردی متصل نیست
        em.persist(adr4);
        //نمایش افراد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        //نمایش آدرس‌ها
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        // ارتباطات بین افراد و فعالیت‌ها
        PersonneActivite p1act1 = new PersonneActivite(p1, act1);
        PersonneActivite p1act2 = new PersonneActivite(p1, act2);
        PersonneActivite p2act1 = new PersonneActivite(p2, act1);
        PersonneActivite p2act3 = new PersonneActivite(p2, act3);
        // پایداری ارتباطات فرد <--> فعالیت
        em.persist(p1act1);
        em.persist(p1act2);
        em.persist(p2act1);
        em.persist(p2act3);
        // نمایش افراد
        System.out.println("[personnes]");
        for (Object p : em.createQuery("select p from Personne p order by p.nom asc").getResultList()) {
            System.out.println(p);
        }
        // نمایش آدرس‌ها
        System.out.println("[adresses]");
        for (Object a : em.createQuery("select a from Adresse a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[activites]");
        for (Object a : em.createQuery("select a from Activite a").getResultList()) {
            System.out.println(a);
        }
        System.out.println("[personnes/activites]");
        for (Object pa : em.createQuery("select pa from PersonneActivite pa").getResultList()) {
            System.out.println(pa);
        }
        // پایان تراکنش
        tx.commit();
        // پایان EntityManager
        em.close();
        //پایان EntityManagerFactory
        emf.close();
        // لاگ
        System.out.println("terminé...");

    }
}
  • خطوط ۲۷–۳۸: جداول [personne_activite]، [personne]، [adresse] و [activite] خالی می‌شوند. توجه داشته باشید که باید از جدول‌هایی شروع کنید که شامل کلیدهای خارجی هستند.
  • خطوط ۴۰–۴۵: سه فعالیت ایجاد می‌شوند: act1، act2 و act3
  • خطوط ۴۷–۴۹: این‌ها در زمینه پایداری قرار داده می‌شوند.
  • خطوط ۵۱–۵۳: سه نفر، p1، p2 و p3، ایجاد می‌شوند.
  • خطوط ۵۵–۵۸: چهار آدرس از adr1 تا adr4 ایجاد می‌شوند.
  • خطوط 60–65: آدرس‌های adri با اشخاص pi مرتبط می‌شوند. در هر مورد دو عملیات باید انجام شود، زیرا رابطه شخص <-> آدرس دوطرفه است.
  • خطوط ۶۷–۶۹: افراد p1 تا p3 در زمینه پایداری قرار می‌گیرند. به دلیل آبشاری Person -> Address، این موضوع همچنین برای آدرس‌های adr1 تا adr3 نیز اعمال می‌شود.
  • خط ۷۱: چهارمین آدرس، adr4، که با هیچ شخصی مرتبط نیست، صراحتاً در زمینه پایداری قرار می‌گیرد.
  • خطوط ۷۳–۸۵: از کانتکست پایداری پرس‌وجو می‌شود تا فهرست اِنتیتِی‌های نوع [Personne]، [Adresse] و [Activite] بازیابی شود. ما می‌دانیم که این پرس‌وجوها همگام‌سازی زمینه با پایگاه داده را تحریک می‌کنند: اشیاء ایجادشده وارد پایگاه داده شده و کلیدهای اصلی‌شان به آن‌ها اختصاص می‌یابد. درک این موضوع برای آنچه در ادامه می‌آید اهمیت دارد.
  • خطوط ۸۷–۹۰: چهار ارتباط Person <-> Activity ایجاد می‌شود. نام آن‌ها نشان می‌دهد که کدام شخص به کدام فعالیت مرتبط است. ممکن است به یاد داشته باشید که کلید اصلی یک موجودیت مانند PersonneActivite یک کلید مرکب است که از کلیدهای اصلی یک شخص و یک فعالیت تشکیل شده است. بنابراین، این عملیات به این دلیل ممکن است که موجودات Personne و Activite در طی همگام‌سازی قبلی، کلیدهای اصلی خود را دریافت کرده‌اند.
  • خطوط ۹۲–۹۵: این چهار انجمن در زمینه پایداری قرار می‌گیرند.
  • خطوط ۸۶–۸۷: از زمینه پایداری پرس‌وجو می‌شود تا فهرست موجودیت‌های از انواع [Personne]، [Adresse]، [Activite] و [PersonneActivite] بازیابی شود. ما می‌دانیم که این پرس‌وجوها باعث همگام‌سازی زمینه با پایگاه داده می‌شوند: انتیت‌های ایجادشده توسط PersonneActivite در پایگاه داده درج خواهند شد.

اجرای [InitDB] در کنار MySQL5 خروجی کنسول زیر را تولید می‌کند:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
terminé...

شاید تعجب‌آور باشد که در خطوط ۱۵–۱۶، افراد p1 و p2 دارای شماره نسخه ۱ هستند و همین امر در مورد سه فعالیت در خطوط ۲۴–۲۶ نیز صدق می‌کند. بیایید سعی کنیم بفهمیم چرا.

در خطوط ۲–۴، شماره‌های نسخه برای افراد روی ۰ تنظیم می‌شوند و در خطوط ۱۱–۱۳، شماره‌های نسخه برای فعالیت‌ها روی ۰ تنظیم می‌شوند. این نمایش‌ها قبل از ایجاد روابط Person <-> Activity رخ می‌دهند. خطوط ۸۷–۹۰ کد جاوا: روابط بین افراد p1 و p2 و فعالیت‌های act1، act2، act3. این روابط با استفاده از سازندهٔ @Entity PersonneActivite ایجاد می‌شوند (رجوع کنید به بخش 2.5.2). بررسی کد این سازنده نشان می‌دهد که وقتی یک شخص p به یک فعالیت a متصل می‌شود:

  • فعالیت a به مجموعه p.activites اضافه می‌شود
  • شخص p به مجموعه a.personnes اضافه می‌شود

بنابراین، هنگامی که new PersonneActivite(p,a) را می‌نویسیم، شخص p و فعالیت a در حافظه تغییر می‌کنند. وقتی خطوط ۹۷–۱۱۳ از [InitDB] اجرا می‌شوند، زمینه پایداری با پایگاه داده همگام‌سازی می‌شود، JPA / هایبرنت تشخیص می‌دهد که موجودیت‌های پایدار p1، p2، act1، act2 و act3 تغییر کرده‌اند. این تغییرات باید در پایگاه داده اعمال شوند. در واقع، آن‌ها در جدول الحاق [personne_activite] ثبت شده‌اند، اما JPA / Hibernate همچنان شماره نسخه هر یک از اشیاء پایدار تغییر یافته را افزایش می‌دهد.

در نمای SQL Explorer، نتایج به شرح زیر است:

  • [2]: جداول [jpa07_hb_*]
  • [3]: جدول افراد
  • [4]: جدول آدرس‌ها.
  • [5]: جدول فعالیت‌ها
  • [6]: جدول پیوند شخص <-> فعالیت

2.5.6. اصلی

کلاس [Main] شامل مجموعه‌ای از تست‌ها است که به بررسی آن‌ها خواهیم پرداخت، به استثنای تست ۱ که برای راه‌اندازی پایگاه داده از کد [InitDB] استفاده مجدد می‌کند.

2.5.6.1. آزمون ۲

این تست به شرح زیر است:


// حذف Person p1
    public static void test2() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // حذف وابستگی‌ها در p1: برای Hibernate ضروری نیست اما
        // ضروری برای TopLink
        act1.getPersonnes().remove(p1act1);
        act2.getPersonnes().remove(p1act2);
        // حذف شخص p1
        em.remove(p1);
        // پایان تراکنش
        tx.commit();
        // جدول‌های جدید نمایش داده می‌شوند
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • خط ۴: از کانتکست پایداری test1 استفاده می‌شود، که در آن شخص p1 یک شیء در این کانتکست است.
  • خط ۱۳: حذف شخص p1. به دلیل ویژگی:
    • cascadeType.ALL بر روی Adresse، آدرس شخص p1 حذف خواهد شد
    • cascadeType.REMOVE بر روی PersonneActivite، فعالیت‌های شخص p1 حذف خواهند شد.
  • سطور ۱۰–۱۱: وابستگی‌هایی که سایر موجودیت‌ها به شخص p1 دارند، که در خط ۱۳ حذف می‌شود، حذف شده‌اند. فعالیت‌های act1 و act2 توسط شخص p1 انجام می‌شوند. این پیوندها توسط سازنده موجودیت PersonneActivite ایجاد شده‌اند، که کد آن به شرح زیر است:

    public PersonneActivite(Personne p, Activite a) {
        // کلیدهای خارجی توسط برنامه تعریف می‌شوند
        getId().setPersonneId(p.getId());
        getId().setActiviteId(a.getId());
        // ارتباطات دوطرفه
        setPersonne(p);
        setActivite(a);
        p.getActivites().add(this);
        a.getPersonnes().add(this);
}

در خط ۹، فعالیت a یک عنصر اضافی از نوع PersonneActivite را در مجموعه personnes خود دریافت می‌کند. این عنصر از نوع (p,a) است تا نشان دهد که شخص p فعالیت a را انجام می‌دهد. در test1 از [Main]، دو لینک (p1,act1) و (p1,act2) بدین ترتیب ایجاد شدند. خطوط ۱۰ و ۱۱ از test2 این وابستگی‌ها را حذف می‌کنند. باید توجه داشت که Hibernate برای شخص p1 بدون حذف این وابستگی‌ها کار می‌کند، اما Toplink این کار را انجام نمی‌دهد.

  • خطوط 17–20: تمام جداول نمایش داده شده‌اند

نتایج به شرح زیر است:

main : ----------- test1
[personnes]
P[1,1,p1,Paul,31/01/2000,true,2,1]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[1,1],p1,act1]
[[2,1],p2,act1]
[[1,2],p1,act2]
[[2,3],p2,act3]
main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
  • فرد p1 که در test1 (خط ۳) ظاهر می‌شود، دیگر در انتهای test2 (خطوط ۲۲–۲۳) ظاهر نمی‌شود
  • آدرس adr1 برای شخص p1، که در test1 ظاهر می‌شود (خط ۱۱) پس از test2 (خطوط ۲۹–۳۱) دیگر معتبر نیست
  • فعالیت‌ها (p1,act1) (خط 16) و (p1,act2) (خط ۱۸) برای شخص p1، که در test1 حضور داشتند، دیگر در پایان test2 (خطوط ۳۳–۳۴) حضور ندارند

2.5.6.2. Test3

این آزمون به شرح زیر است:


// حذف فعالیت act1
    public static void test3() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // حذف وابستگی‌ها به act1: توسط Hibernate مورد نیاز نیست اما
        // ضروری برای TopLink
        p2.getActivites().remove(p2act1);
        // حذف فعالیت act1
        em.remove(act1);
        //پایان تراکنش
        tx.commit();
        //نمایش جدول‌های جدید
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • خط ۴: از زمینه پایداری test2 استفاده می‌شود
  • خط ۱۲: حذف فعالیت act1. به دلیل ویژگی:
    • cascadeType.REMOVE بر روی PersonneActivite، سطرهای (p, act1) در جدول [personne_activite] حذف خواهند شد.
  • خط ۱۰: قبل از حذف act1 از زمینه پایداری، هرگونه وابستگی که سایر موجودیت‌ها ممکن است به این شیء پایدار داشته باشند، حذف می‌شود. پس از حذف شخص p1 در آزمون قبلی، تنها شخص p2 در حال انجام فعالیت act1 است.
  • خطوط ۱۳–۱۶: تمام جداول نمایش داده شده‌اند

نتایج به شرح زیر است:

main : ----------- test2
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,1],p2,act1]
[[2,3],p2,act3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,1,act2]
Ac[3,1,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[[2,3],p2,act3]
  • در test2، فعالیت act1 وجود دارد (خط ۶). در test3، دیگر وجود ندارد (خطوط ۲۱–۲۲)
  • در test2، لینک (p2,act1)) وجود دارد (خط 14). در test3، دیگر وجود ندارد (خط 28)

2.5.6.3. Test4

این تست به شرح زیر است:


//بازیابی فعالیت‌های یک شخص
    public static void test4() {
        // زمینه پایداری
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        //بازیابی شخص p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("1 - Activités de la personne p2 (JPQL) :%n");
        // اسکن فعالیت‌های آن‌ها
        for (Object pa : em.createQuery("select a.nom from Activite a join a.personnes pa where pa.personne.nom='p2'").getResultList()) {
            System.out.println(pa);
        }
        // پیمایش رابطه معکوس p2
        p2 = em.find(Personne.class, p2.getId());
        System.out.format("2 - Activités de la personne p2 (relation inverse) :%n");
        // اسکن فعالیت‌های آنها
        for (PersonneActivite pa : p2.getActivites()) {
            System.out.println(pa.getActivite().getNom());
        }
        //پایان تراکنش
        tx.commit();
    }
  • آزمون ۴ فعالیت‌های شخص p2 را نمایش می‌دهد.
  • خط ۴: ما با یک زمینهٔ جدید و خالی شروع می‌کنیم
  • خطوط ۱۲–۱۴: نام فعالیت‌های انجام‌شده توسط شخص p2 با استفاده از پرس‌وجوی JPQL نمایش داده می‌شوند.
    • یک پیوست بین Activite (a) و PersonneActivite (pa) انجام می‌شود (پیوست a.personnes)
    • در سطرهای این پیوند (a,pa)، نام فعالیت (a.nom) برای شخص p2 (pa.personne.nom='p2') نمایش داده می‌شود.
  • رده‌های ۱۶–۲۱: ما همانند قبل عمل می‌کنیم، اما با استفاده از رابطه OneToMany p2.activites برای شخص p2. پرس‌وجوی JPQL توسط JPA تولید خواهد شد. این امر مزیت رابطه معکوس OneToMany را نشان می‌دهد: این رابطه نیاز به پرس‌وجوی JPQL را از بین می‌برد.

نتایج به شرح زیر است:

1
2
3
4
5
main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation inverse) :
act3

2.5.6.4. Test5

این تست به شرح زیر است:


//بازیابی افرادی که یک فعالیت معین را انجام می‌دهند
    public static void test5() {
        // زمینه پایداری
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        //درخواست فعالیت‌های p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites pa where pa.activite.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        // ادامه از طریق رابطه معکوس act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (PersonneActivite pa : act3.getPersonnes()) {
            System.out.println(pa.getPersonne().getNom());
        }
        //پایان تراکنش
        tx.commit();
    }
  • آزمون ۶ افرادی را که در حال انجام فعالیت act3 هستند نمایش می‌دهد. روش کار مشابه آزمون ۶ است. ارتباط بین این دو کد را به خواننده واگذار می‌کنیم.

نتایج به شرح زیر است:

1
2
3
4
5
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2

آزمایش‌های ۴ و ۵ با هدف نشان دادن بار دیگر این موضوع انجام شدند که یک رابطه معکوس هرگز ضروری نیست و همیشه می‌توان آن را با یک پرس‌وجو JPQL جایگزین کرد.

ما اکنون از پیاده‌سازی JPA / Toplink استفاده می‌کنیم:

پروژه Eclipse با Toplink کپی‌ای از پروژه Eclipse با Hibernate است:

کد جاوا به جز چند جزئیات جزئی که به آن خواهیم پرداخت، با پروژه قبلی Hibernate یکسان است. محیط (کتابخانه‌ها – persistence.xml – DBMS – پوشه‌های conf و ddl – اسکریپت Ant) همان است که در بخش 2.1.15.2 شرح داده شده است. پروژه Eclipse در آدرس [3] در پوشه مثال‌ها [4] قرار دارد. ما آن را وارد خواهیم کرد.

فایل <persistence.xml> [2] در یک جنبه تغییر یافته است، یعنی انتیت‌های اعلام‌شده:


        <!-- کلاس‌های پایدار -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
<class>entites.PersonneActivite</class>
  • خطوط ۲–۵: چهار موجودیت مدیریت‌شده

اجرای [InitDB] با SGBD و MySQL5 نتایج زیر را تولید می‌کند:

در [1]، خروجی کنسول؛ در [2]، جداول تولیدشده [jpa07_tl]؛ و در [3]، اسکریپت‌های تولیدشده SQL. محتوای آن‌ها به شرح زیر است:

create.sql


CREATE TABLE jpa07_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa07_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa07_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa07_tl_activite (ID)
ALTER TABLE jpa07_tl_personne_activite ADD CONSTRAINT FK_jpa07_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa07_tl_personne (ID)
ALTER TABLE jpa07_tl_personne ADD CONSTRAINT FK_jpa07_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa07_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

اجرای [InitDB] و [Main] بدون خطا به پایان رسید.

2.6. مثال ۶: رابطهٔ چندبه‌چند با جدول الحاق ضمنی

ما به مثال ۴ بازمی‌گردیم، اما این بار آن را با استفاده از یک جدول پیوند ضمنی که توسط لایه JPA خود تولید شده است، پردازش می‌کنیم.

2.6.1. شمای پایگاه داده

  • در [1]، پایگاه داده MySQL5 – در [2]: جدول [personne] – به [3]: جدول مرتبط [adresse] – به [4]: جدول فعالیت‌ها [activite] – به [5]: جدول الحاق [personne_activite] که افراد و فعالیت‌ها را به هم متصل می‌کند.

2.6.2. ابجکت‌های @Entity که نمایندهٔ پایگاه داده هستند

جدول‌های بالا با anotationهای @Entity زیر نمایش داده می‌شوند:

  • @Entity Personne نمایانگر جدول [personne] خواهد بود
  • @Entity Adresse نمایانگر جدول [adresse] خواهد بود
  • @Entity Activite نمایانگر جدول [activite] خواهد بود
  • جدول [personne_activite] دیگر توسط یک @Entity نمایش داده نمی‌شود

روابط بین این اشیاء به شرح زیر است:

  • یک رابطه یک‌به‌یک بین موجوده Personne و موجوده Adresse برقرار است: یک شخص p دارای یک آدرس a است. انتیتی Personne که کلید خارجی را در خود دارد، رابطه اصلی را خواهد داشت؛ انتیتی Adresse رابطه معکوس را خواهد داشت.
  • یک رابطه چندبه‌چند، موجودیت‌های Personne و Activite را به هم مرتبط می‌کند: یک شخص چندین فعالیت دارد و یک فعالیت توسط چندین شخص انجام می‌شود. این رابطه در هر یک از این دو موجودیت با یک @ManyToMany annotation نشان داده می‌شود، که یکی معکوس دیگری اعلام شده است.

@Entity Personne به شرح زیر است:


@Entity
@Table(name = "jpa08_hb_personne")
public class Personne implements Serializable {

    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // SQL Server TopLink: @GeneratedValue (strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    @Column(length = 30, nullable = false)
    private String prenom;

    @Column(nullable = false)
    @Temporal(TemporalType.DATE)
    private Date datenaissance;

    @Column(nullable = false)
    private boolean marie;

    @Column(nullable = false)
    private int nbenfants;

    // رابطهٔ اصلی Person (یک) -> Address (یک)
    // پیاده‌سازی شده از طریق کلید خارجی Person (adresse_id) -> Address
    // افزودن آبشاری: Person -> افزودن Address
    // به‌روزرسانی آبشاری: Person -> Address
    // حذف آبشاری Person -> حذف Address
    //یک شخص باید یک آدرس داشته باشد (nullable=false)
    // یک آدرس فقط متعلق به یک شخص است (unique=true)
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "adresse_id", unique = true, nullable = false)
    private Adresse adresse;

    // رابطه Person (بسیار) -> Activity (بسیار) از طریق جدول پیوند personne_activite
    // personne_activite(PERSONNE_ID) یک کلید خارجی روی Person(id) است
    //personne_activite(ACTIVITE_ID) یک کلید خارجی روی Activity(id) است
    // cascade=CascadeType.PERSIST: پایداری یک فرد منجر به پایداری فعالیت‌های او می‌شود
    @ManyToMany(cascade={CascadeType.PERSIST})
    @JoinTable(name="jpa08_hb_personne_activite",joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
    private Set<Activite> activites = new HashSet<Activite>();

    // سازندگان
    public Personne() {
    }

ما فقط در مورد رابطه @ManyToMany در خطوط ۴۶–۴۸ نظر می‌دهیم، که @Entity Personne را به @Entity Activite پیوند می‌دهد:

  • خط ۴۸: یک شخص فعالیت‌هایی دارد. فیلد «فعالیت‌ها» این موارد را نشان خواهد داد. در نسخه قبلی، نوع عناصر در مجموعه activites، PersonneActivite بود. در اینجا، این Activite است. بنابراین ما مستقیماً به فعالیت‌های یک شخص دسترسی پیدا می‌کنیم، در حالی که در نسخه قبلی لازم بود از طریق موجوده میانجی PersonneActivite اقدام شود.
  • خط ۴۶: رابطه‌ای که @Entity Personne مورد بررسی ما را به @Entity Activite در مجموعه activites در خط ۴۸ متصل می‌کند، از نوع چند به چند است (ManyToMany):
    • یک نفر (یک) چندین فعالیت (چند) دارد
    • یک فعالیت (یک) توسط چندین نفر (چندین) انجام می‌شود
    • در نهایت، @Entity Personne و Activite توسط رابطه ManyToMany به هم متصل هستند. همانند رابطه OneToOne، در این رابطه بین این انتیته‌ها تقارن وجود دارد. ما آزاد هستیم که انتخاب کنیم کدام @Entity رابطهٔ اصلی را در خود نگه دارد و کدام یک رابطهٔ معکوس را. در اینجا تصمیم می‌گیریم که @Entity با شناسهٔ Personne رابطهٔ اصلی را در خود نگه دارد.
    • همان‌طور که در مثال قبلی دیدیم، رابطه @ManyToMany به یک جدول الحاق نیاز دارد. در حالی که قبلاً این را با استفاده از @Entity تعریف کرده بودیم، جدول الحاق در اینجا با استفاده از تگ @JoinTable در خط ۴۷ تعریف شده است.
      • ویژگی 'name' به جدول یک نام می‌دهد.
      • جدول الحاقی از کلیدهای خارجی جدول‌هایی که آن‌ها را به هم متصل می‌کند تشکیل شده است. در اینجا دو کلید خارجی وجود دارد: یکی در جدول [personne] و دیگری در جدول [activite]. این ستون‌های کلید خارجی توسط ویژگی‌های joinColumns و inverseJoinColumns تعریف شده‌اند.
      • توضیح @JoinColumn بر روی ویژگی joinColumns، کلید خارجی را در جدولِ @Entity که رابطهٔ اصلی @ManyToMany را در خود دارد، تعریف می‌کند؛ در این مورد، جدول [personne]. این ستون کلید خارجی با نام PERSONNE_ID نام‌گذاری خواهد شد.
      • آنگاشت @JoinColumn بر روی ویژگی inverseJoinColumns، کلید خارجی را در جدول @Entity که رابطه معکوس @ManyToMany را در خود جای داده است، تعریف می‌کند؛ در این مورد، جدول [activite]. این ستون کلید خارجی با نام ACTIVITE_ID نام‌گذاری خواهد شد.

@Entity Adresse به شرح زیر است:


@Entity
@Table(name = "jpa07_hb_adresse")
public class Adresse implements Serializable {

    // میدان‌ها
    @Id
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false)
    private String adr1;
    @Column(length = 30)
    private String adr2;
    @Column(length = 30)
    private String adr3;
    @Column(length = 5, nullable = false)
    private String codePostal;
    @Column(length = 20, nullable = false)
    private String ville;
    @Column(length = 3)
    private String cedex;
    @Column(length = 20, nullable = false)
    private String pays;
    @OneToOne(mappedBy = "adresse")
    private Personne personne;

  • خطوط ۲۸–۲۹: رابطه @OneToOne که معکوس رابطه @OneToOne است، به @Entity Personne اشاره دارد (خطوط ۳۷–۳۸ از Personne).

@Entity Activite به شرح زیر است


@Entity
@Table(name = "jpa08_hb_activite")
public class Activite implements Serializable {

    // fields
    @Id()
    @Column(nullable = false)
    @GeneratedValue(strategy = GenerationType.AUTO)
    // SQL Server TopLink: @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    @Version
    private int version;

    @Column(length = 30, nullable = false, unique = true)
    private String nom;

    // رابطه معکوس: فعالیت -> شخص
    @ManyToMany(mappedBy = "activites")
    private Set<Personne> personnes = new HashSet<Personne>();
...
  • سطور ۲۰–۲۱: رابطهٔ چند به چند که @Entity Activite را به @Entity Personne متصل می‌کند. این رابطه قبلاً در @Entity Personne تعریف شده است. بنابراین ما صرفاً در اینجا بیان می‌کنیم که این رابطه معکوس رابطه @ManyToMany موجود در فیلد «activites» (mappedBy = "activites") از @اِنتیتی Personne.
  • به یاد داشته باشید که یک رابطه معکوس همیشه اختیاری است. در اینجا از آن برای بازیابی افرادی که در فعالیت جاری شرکت دارند استفاده می‌کنیم. از Set<Person> 'people' برای بازیابی آن‌ها استفاده خواهد شد. حالت بارگذاری برای وابستگی‌های Personne از @Entity Activite مشخص نشده است. ما در مثال قبلی نیز آن را مشخص نکردیم. به طور پیش‌فرض، این حالت fetch=FetchType.LAZY است.

اکنون توصیف انتیته‌های پایگاه داده را به پایان رسانده‌ایم. این کار ساده‌تر از موردی بود که در آن جدول الحاق [personne_activite] به عنوان یک جدول صریح تعریف شده باشد. این راه‌حل ساده‌تر ممکن است در طول زمان معایبی داشته باشد: این روش اجازه نمی‌دهد که ستون‌هایی به جدول الحاق اضافه شوند. با این حال، این کار ممکن است برای برآورده کردن نیازهای جدید ضروری شود، برای مثال، افزودن ستونی به جدول [personne_activite] که تاریخ ثبت‌نام فرد در فعالیت را نشان دهد.

2.6.3. پروژه Eclipse / Hibernate

پیاده‌سازی JPA که در اینجا استفاده شده، مربوط به Hibernate است. پروژه Eclipse برای تست‌ها به شرح زیر است:

[1] حاوی پروژه Eclipse است، در حالی که [2] حاوی کد جاوا است. این پروژه در [3] در داخل پوشه مثال‌ها [4] قرار دارد. ما آن را وارد خواهیم کرد.

2.6.4. ایجاد پایگاه داده DDL

طبق دستورالعمل‌های بخش 2.1.7، خروجی DDL که از SGBD و MySQL5 به دست آمده، به شرح زیر است:


alter table jpa08_hb_personne 
        drop 
        foreign key FKA44B1E555FE379D0;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A5CD852024;

    alter table jpa08_hb_personne_activite 
        drop 
        foreign key FK5A6A55A568C7A284;

    drop table if exists jpa08_hb_activite;

    drop table if exists jpa08_hb_adresse;

    drop table if exists jpa08_hb_personne;

    drop table if exists jpa08_hb_personne_activite;

    create table jpa08_hb_activite (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_adresse (
        id bigint not null auto_increment,
        version integer not null,
        adr1 varchar(30) not null,
        adr2 varchar(30),
        adr3 varchar(30),
        codePostal varchar(5) not null,
        ville varchar(20) not null,
        cedex varchar(3),
        pays varchar(20) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne (
        id bigint not null auto_increment,
        version integer not null,
        nom varchar(30) not null unique,
        prenom varchar(30) not null,
        datenaissance date not null,
        marie bit not null,
        nbenfants integer not null,
        adresse_id bigint not null unique,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa08_hb_personne_activite (
        PERSONNE_ID bigint not null,
        ACTIVITE_ID bigint not null,
        primary key (PERSONNE_ID, ACTIVITE_ID)
    ) ENGINE=InnoDB;

    alter table jpa08_hb_personne 
        add index FKA44B1E555FE379D0 (adresse_id), 
        add constraint FKA44B1E555FE379D0 
        foreign key (adresse_id) 
        references jpa08_hb_adresse (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A5CD852024 (ACTIVITE_ID), 
        add constraint FK5A6A55A5CD852024 
        foreign key (ACTIVITE_ID) 
        references jpa08_hb_activite (id);

    alter table jpa08_hb_personne_activite 
        add index FK5A6A55A568C7A284 (PERSONNE_ID), 
        add constraint FK5A6A55A568C7A284 
        foreign key (PERSONNE_ID) 
        references jpa08_hb_personne (id);

این DDL مشابه آنچه با استفاده از جدول پیوند صریح به دست آمده است و با طرح‌بندی که قبلاً ارائه شده مطابقت دارد:

2.6.5. InitDB

ما درباره کلاس [InitDB] که با نسخه قبلی خود یکسان است و نتایج مشابهی تولید می‌کند، کم صحبت خواهیم کرد. بیایید صرفاً روی کد زیر تمرکز کنیم که پیوست بین Personne و Activite را نمایش می‌دهد:


        //نمایش افراد/فعالیت‌ها
        System.out.println("[personnes/activites]");
        Iterator iterator = em.createQuery("select p.id,a.id from Personne p join p.activites a").getResultList().iterator();
        while (iterator.hasNext()) {
            Object[] row = (Object[]) iterator.next();
            System.out.format("[%d,%d]%n", (Long) row[0], (Long) row[1]);
}
  • خط ۳: دستور JPQL که عملیات الحاق را انجام می‌دهد. نتیجه select شناسه‌های موجودیت‌های Personne و Activite را که از طریق جدول الحاق به هم متصل شده‌اند، برمی‌گرداند. فهرست بازگردانده‌شده توسط select شامل سطرهایی است که هر سطر شامل دو شیء از نوع Long است. برای پیمایش این فهرست، خط ۳ یک شیء Iterator را از فهرست درخواست می‌کند.
  • خطوط ۴–۷: با استفاده از شیء قبلی از نوع Iterator، لیست پیمایش می‌شود.
    • خط ۵: هر عنصر در لیست یک آرایه است که شامل یک سطر حاصل از select می‌باشد.
    • خط ۶: عناصر سطر نتیجهٔ جاری از select بازیابی می‌شوند، با تبدیل‌های نوع مناسب اعمال شده.

نتیجه [InitDB] به شرح زیر است:

[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[activites]
Ac[1,1,act1]
Ac[2,1,act2]
Ac[3,1,act3]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
terminé...

2.6.6. اصلی

کلاس [Main] مجموعه‌ای از تست‌ها را اجرا می‌کند که برخی از آن‌ها را بررسی خواهیم کرد.

2.6.6.1. Test3

این آزمون به شرح زیر است:


// حذف فعالیت act1
    public static void test3() {
        // زمینه پایداری
        EntityManager em = getEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // حذف فعالیت act1 از p2
        p2.getActivites().remove(act1);
        // act1 از زمینه پایداری حذف می‌شود
        em.remove(act1);
        //پایان تراکنش‌ها
        tx.commit();
        //نمایش جدول‌های جدید
        dumpPersonne();
        dumpActivite();
        dumpAdresse();
        dumpPersonne_Activite();
    }
  • خط ۱۱: فعالیت act1 از زمینه پایداری حذف می‌شود
  • خط ۹: فعالیت act1 یکی از فعالیت‌های تنها شخص باقی‌مانده در زمینه، یعنی شخص p2 است. خط ۹ فعالیت act1 را از فعالیت‌های شخص p2 حذف می‌کند. ما این کار را برای حفظ سازگاری زمینه پایداری انجام می‌دهیم، زیرا آن را برای استفاده بعدی نگه می‌داریم.

نتایج به شرح زیر است:

main : ----------- test1
[personnes]
P[1,0,p1,Paul,31/01/2000,true,2,1]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[1,adr1,null,null,49000,Angers,null,France]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[1,1]
[1,2]
[2,1]
[2,3]
main : ----------- test2
[personnes]
P[2,0,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[1,0,act1]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,1]
[2,3]
main : ----------- test3
[personnes]
P[2,1,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[adresses]
A[2,adr2,Les Mimosas,15 av Foch,49002,Angers,03,France]
A[3,adr3,x,x,x,x,x,x]
A[4,adr4,y,y,y,y,y,y]
[personnes/activites]
[2,3]
  • فعالیت act1، که در خط 26 در test2 ظاهر می‌شود، از فعالیت‌های test3 (خطوط 40–41) حذف شده است.
  • شخص p2 فعالیت act1 را در test2 (خط ۳۳) داشت. در پایان test3، دیگر آن را ندارد (خط ۴۷)

2.6.6.2. Test6

این آزمون به شرح زیر است:


// تغییر فعالیت‌های یک شخص
    public static void test6() {
        // زمینه پایداری
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        //بازیابی شخص p2
        p2 = em.find(Personne.class, p2.getId());
        //بازیابی فعالیت act2
        act2 = em.find(Activite.class, act2.getId());
        // p2 اکنون فقط فعالیت act2 را انجام می‌دهد
        p2.getActivites().clear();
        p2.getActivites().add(act2);
        //پایان تراکنش
        tx.commit();
        // جدول‌های جدید نمایش داده می‌شوند
        dumpPersonne();
        dumpActivite();
        dumpPersonne_Activite();
    }
  • خط ۴: یک زمینه پایداری جدید و خالی استفاده می‌شود
  • خط ۹: شخص p2 از پایگاه داده به داخل زمینه پایداری بازیابی می‌شود
  • خط ۱۱: فعالیت act2 از پایگاه داده به زمینه پایداری فراخوانی می‌شود
  • خط ۱۳: فعالیت‌های شخص p2 (act3) از پایگاه داده به داخل زمینه (fetchType.LAZY) بازیابی می‌شوند. این فراخوانی [getActivites] است که این بارگذاری را آغاز می‌کند. فعالیت‌های p2 حذف می‌شوند. این یک حذف واقعی فعالیت‌ها (remove) نیست، بلکه تغییری در وضعیت شخص p2 است. آنها دیگر هیچ فعالیتی انجام نمی‌دهند.
  • خط ۱۴: فعالیت act2 به شخص p2 اضافه می‌شود. در نهایت، مجموعه فعالیت‌های جدید برای شخص p2، مجموعه {act2} است.
  • خط ۱۶: پایان تراکنش. فرآیند همگام‌سازی اشیاء در زمینه (p2, act2, act3) را بررسی کرده و تشخیص می‌دهد که وضعیت p2 تغییر کرده است. دستورات SQL که این تغییر را به پایگاه داده منتقل می‌کنند، اجرا خواهند شد.
  • خطوط ۱۸–۲۰: تمام جداول نمایش داده می‌شوند

نتایج به شرح زیر است:

main : ----------- test4
1 - Activités de la personne p2 (JPQL) :
act3
2 - Activités de la personne p2 (relation principale) :
act3
main : ----------- test5
1 - Personnes pratiquant l'activité act3 (JPQL) :
p2
2 - Personnes pratiquant l'activité act3 (relation inverse) :
p2
main : ----------- test6
[personnes]
P[2,2,p2,Sylvie,05/07/2001,false,0,2]
P[3,0,p3,Sylvie,05/07/2001,false,0,3]
[activites]
Ac[2,0,act2]
Ac[3,0,act3]
[personnes/activites]
[2,2]
  • در پایان تست ۴، شخص p2 در حال انجام فعالیت act3 (خط ۳) بود.
  • در پایان آزمون ۶ (خط ۱۹)، شخص p2 دیگر فعالیت act3 (خط ۳) را انجام نمی‌دهد و اکنون در حال انجام فعالیت act2 است.

ما اکنون از پیاده‌سازی JPA / Toplink استفاده می‌کنیم:

پروژه Eclipse با Toplink کپی‌ای از پروژه Eclipse با Hibernate است:

فایل <persistence.xml> [2] در یک جنبه تغییر کرده است، یعنی انتیت‌های اعلام‌شده:


        <!--  ارائه‌دهنده -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
        <!-- کلاس‌های پایدار -->
        <class>entites.Activite</class>
        <class>entites.Adresse</class>
        <class>entites.Personne</class>
...
  • خطوط ۴–۶: موجودیت‌های مدیریت‌شده

اجرای [InitDB] با SGBD و MySQL5 نتایج زیر را تولید می‌کند:

در [1]، خروجی کنسول؛ در [2]، جداول تولیدشده [jpa07_tl]؛ و در [3]، اسکریپت‌های تولیدشده SQL. محتوای آن‌ها به شرح زیر است:

create.sql


CREATE TABLE jpa08_tl_personne_activite (PERSONNE_ID BIGINT NOT NULL, ACTIVITE_ID BIGINT NOT NULL, PRIMARY KEY (PERSONNE_ID, ACTIVITE_ID))
CREATE TABLE jpa08_tl_activite (ID BIGINT NOT NULL, VERSION INTEGER NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_personne (ID BIGINT NOT NULL, DATENAISSANCE DATE NOT NULL, MARIE TINYINT(1) default 0 NOT NULL, NOM VARCHAR(30) UNIQUE NOT NULL, NBENFANTS INTEGER NOT NULL, VERSION INTEGER NOT NULL, PRENOM VARCHAR(30) NOT NULL, adresse_id BIGINT UNIQUE NOT NULL, PRIMARY KEY (ID))
CREATE TABLE jpa08_tl_adresse (ID BIGINT NOT NULL, ADR3 VARCHAR(30), CODEPOSTAL VARCHAR(5) NOT NULL, VERSION INTEGER NOT NULL, VILLE VARCHAR(20) NOT NULL, ADR2 VARCHAR(30), CEDEX VARCHAR(3), ADR1 VARCHAR(30) NOT NULL, PAYS VARCHAR(20) NOT NULL, PRIMARY KEY (ID))
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_ACTIVITE_ID FOREIGN KEY (ACTIVITE_ID) REFERENCES jpa08_tl_activite (ID)
ALTER TABLE jpa08_tl_personne_activite ADD CONSTRAINT FK_jpa08_tl_personne_activite_PERSONNE_ID FOREIGN KEY (PERSONNE_ID) REFERENCES jpa08_tl_personne (ID)
ALTER TABLE jpa08_tl_personne ADD CONSTRAINT FK_jpa08_tl_personne_adresse_id FOREIGN KEY (adresse_id) REFERENCES jpa08_tl_adresse (ID)
CREATE TABLE SEQUENCE (SEQ_NAME VARCHAR(50) NOT NULL, SEQ_COUNT DECIMAL(38), PRIMARY KEY (SEQ_NAME))
INSERT INTO SEQUENCE(SEQ_NAME, SEQ_COUNT) values ('SEQ_GEN', 1)

اجرای [InitDB] و [Main] بدون خطا انجام می‌شود.

2.6.8. پروژه Eclipse / Hibernate 2

ما در حال ایجاد یک پروژه Eclipse بر اساس پروژه قبلی با کپی کردن آن هستیم:

در [1]، پروژهٔ Eclipse؛ در [2]، کد Java. پروژه در [3] درون پوشهٔ مثال‌های [4] قرار دارد. آن را وارد خواهیم کرد.

ما رابطهٔ متصل‌کنندهٔ Personne به Activité را به شرح زیر اصلاح می‌کنیم:

شخص


    // رابطه Person (بسیار) -> Activity (بسیار) از طریق یک جدول الحاق personne_activite
    // personne_activite(PERSONNE_ID) یک کلید خارجی روی Person (id) است
    // personne_activite(ACTIVITE_ID) یک کلید خارجی روی Activity(id) است
    // دیگر هیچ آبشاری روی فعالیت‌ها اعمال نمی‌شود
    // @ManyToMany(cascade={CascadeType.PERSIST})
    @ManyToMany()
    @JoinTable(name = "jpa09_hb_personne_activite", joinColumns = @JoinColumn(name = "PERSONNE_ID"), inverseJoinColumns = @JoinColumn(name = "ACTIVITE_ID"))
private Set<Activite> activites = new HashSet<Activite>();
  • خط ۶: رابطه اصلی @ManyToMany دیگر دارای آبشاری ماندگاری Person -> Activity نیست (به نسخه قبلی، خط ۵ مراجعه کنید)

فعالیت


    // دیگر رابطه معکوس با Person ندارد
    // @ManyToMany(mappedBy = "activities")
// private Set<Person> people = new HashSet<Person>();
  • خطوط ۲–۳: رابطه معکوس @ManyToMany Activity -> Person حذف شده است

ما در تلاشیم نشان دهیم که ویژگی‌های حذف‌شده (آبشاری و رابطه معکوس) ضروری نیستند. اولین تغییر معرفی‌شده توسط این پیکربندی جدید در [InitDB] یافت می‌شود:


        // ارتباطات بین افراد و فعالیت‌ها
        p1.getActivites().add(act1);
        p1.getActivites().add(act2);
        p2.getActivites().add(act1);
        p2.getActivites().add(act3);
        // پایداری فعالیت‌ها
        em.persist(act1);
        em.persist(act2);
        em.persist(act3);
        // پایداری افراد
        em.persist(p1);
        em.persist(p2);
        em.persist(p3);
        //و آدرس a4 که به شخص متصل نیست
em.persist(adr4);
  • سطور ۷–۹: ما موظفیم فعالیت‌های act1 تا act3 را صراحتاً در زمینه پایداری قرار دهیم. هنگامی که Person -> آبشاری ماندگاری فعالیت وجود داشت، خطوط ۱۱–۱۳ هم افراد p1 تا p3 و هم فعالیت‌های آن افراد act1 تا act3 را حفظ کردند.

یک تغییر دوم را می‌توان در [Main] مشاهده کرد:


    //بازیابی افرادی که یک فعالیت معین را انجام می‌دهند
    public static void test5() {
        // زمینه پایداری
        EntityManager em = getNewEntityManager();
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        System.out.format("1 - Personnes pratiquant l'activité act3 (JPQL) :%n");
        //درخواست فعالیت‌های p2
        for (Object pa : em.createQuery("select p.nom from Personne p join p.activites a where a.nom='act3'").getResultList()) {
            System.out.println(pa);
        }
        //پایان تراکنش
        tx.commit();
}
  • خطوط ۹–۱۲: پرس‌وجوی JPQL افرادی را که فعالیت act3 را انجام می‌دهند، بازیابی می‌کند
  • در نسخه قبلی، همین نتیجه از طریق رابطه معکوس Activity -> Person نیز به دست آمده بود که اکنون حذف شده است:

        // ادامه از طریق رابطه معکوس act3
        System.out.format("2 - Personnes pratiquant l'activité act3 (relation inverse) :%n");
        act3 = em.find(Activite.class, act3.getId());
        for (Personne p : act3.getPersonnes()) {
            System.out.println(p.getNom());
}

ما در حال ایجاد یک پروژه Eclipse با کپی کردن پروژه قبلی Eclipse / Toplink هستیم:

در [1] پروژه اکلیپس قرار دارد و در [2] کد جاوا قرار دارد. پروژه در [3] در داخل پوشه مثال‌ها [4] قرار دارد. ما آن را وارد خواهیم کرد.

کد جاوا با نسخه Hibernate یکسان است.

2.7. مثال ۷: استفاده از پرس‌وجوهای نام‌گذاری‌شده

این مرور طولانی بر رویته‌های JPA، که از پاراگراف ۲ آغاز شد، را با یک مثال نهایی که کاربرد پرس‌وجوهای JPQL را که در یک فایل پیکربندی خارجی شده‌اند، نشان می‌دهد، به پایان می‌رسانیم. این مثال از منبع زیر گرفته شده است:

[ref2]: «شروع کار با JPA در Spring 2.0» اثر مارک فیشر در نشانی

[http://blog.springframework.com/markf/archives/2006/05/30/getting-started-with-jpa-in-spring-20/].

2.7.1. پایگاه دادهٔ نمونه

پایگاه داده به شرح زیر است:

  • در [1]: فهرستی از رستوران‌ها به همراه نام و آدرس‌هایشان
  • در [2]: جدول آدرس رستوران‌ها، محدود به شماره پلاک و نام خیابان. بین جدول‌های restaurant و adresse یک رابطه یک‌به‌یک وجود دارد: یک رستوران تنها یک آدرس دارد.
  • در [3]: جدولی از غذاها به همراه نام آن‌ها و یک پرچم true/false برای نشان دادن اینکه آیا غذا گیاهی است یا خیر
  • در [4]: جدول پیوندی برای رستوران‌ها و غذاها: یک رستوران چندین غذا را سرو می‌کند و یک غذا ممکن است توسط چندین رستوران سرو شود. بین جداول restaurant و plat یک رابطه چندبه‌چند وجود دارد.

2.7.2. ابجکت‌های @Entity که نمایندهٔ پایگاه داده هستند

جدول‌های بالا با anotationهای @Entity زیر نمایش داده می‌شوند:

  • @Entity Restaurant نمایانگر جدول [restaurant] خواهد بود
  • @Entity Adresse نمایانگر جدول [adresse] خواهد بود
  • @Entity Plat نمایانگر جدول [plat] خواهد بود

روابط بین این اشیاء به شرح زیر است:

  • یک رابطه یک‌به‌یک بین موجوده Restaurant و موجوده Adresse برقرار است: یک رستوران r دارای یک آدرس a است. سازه Restaurant که کلید خارجی را در خود دارد، سازه اصلی خواهد بود. سازه Adresse رابطه معکوس نخواهد داشت.
  • یک رابطه چند به چند، موجودات Restaurant و Plat را به هم متصل می‌کند: یک رستوران چندین غذا را سرو می‌کند و یک غذای واحد ممکن است توسط چندین رستوران سرو شود. این رابطه با یک حاشیه‌نویسی @ManyToMany در موجوده Restaurant نشان داده خواهد شد. موجوده Plat رابطه معکوس نخواهد داشت.

@Entity Restaurant به شرح زیر است:


package entites;

...
@Entity
@Table(name = "jpa10_hb_restaurant")
public class Restaurant implements java.io.Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column(unique = true, length = 30, nullable = false)
    private String nom;

    @OneToOne(cascade = CascadeType.ALL)
    private Adresse adresse;

    @ManyToMany(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
    @JoinTable(name = "jpa10_hb_restaurant_plat", inverseJoinColumns = @JoinColumn(name = "plat_id"))
    private Set<Plat> plats = new HashSet<Plat>();

    // سازنده‌ها
    public Restaurant() {

    }

    public Restaurant(String name, Adresse address, Set<Plat> entrees) {
...
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...

    //toString
    public String toString() {
        String signature = "R[" + getNom() + "," + getAdresse();
        for (Plat e : getPlats()) {
            signature += "," + e;
        }
        return signature + "]";
    }
}
  • خط 17: رابطه یک‌به‌یک بین @Entity Restaurant و @Entity Adresse. تمام عملیات پایداری بر روی یک رستوران به آدرس آن سرایت می‌کند.
  • خط ۲۰: رابطه متصل‌کننده @Entity Restaurant به @Entity Plat از مجموعه plats در خط ۲۲ از نوع چند به چند است (ManyToMany):
    • یک رستوران (یک) چندین غذا (چندین) دارد
    • یک غذا (یک) می‌تواند توسط چندین رستوران (بسیار) سرو شود
    • در نهایت، @Entity Restaurant و Plat توسط یک رابطه ManyToMany به هم متصل هستند. ما تصمیم می‌گیریم که @Entity Restaurant رابطه اصلی را داشته باشد و @Entity Plat رابطه معکوس نداشته باشد.
    • رابطه @ManyToMany نیازمند یک جدول الحاقی است. این با استفاده از تگ @JoinTable در خط ۴۷ تعریف شده است.
      • ویژگی `name` به جدول یک نام می‌دهد.
      • جدول الحاق شامل کلیدهای خارجی جدول‌هایی است که آن‌ها را به هم متصل می‌کند. در اینجا دو کلید خارجی وجود دارد: یکی در جدول [restaurant] و دیگری در جدول [plat]. این ستون‌های کلید خارجی توسط ویژگی‌های joinColumns و inverseJoinColumns تعریف شده‌اند.
      • ویژگی joinColumns کلید خارجی را در جدول @Entity که رابطه اصلی @ManyToMany را در خود دارد، تعریف می‌کند؛ در این مورد، جدول [restaurant]. ویژگی joinColumns در اینجا وجود ندارد. در این مورد، JPA دارای مقدار پیش‌فرض است: [table]_[clé_primaire_de_table]، در این نمونه [jpa10_hb_restaurant_id].
      • توضیح @JoinColumn بر روی ویژگی inverseJoinColumns، کلید خارجی را در جدول @Entity که رابطه معکوس @ManyToMany را نگه می‌دارد، تعریف می‌کند؛ در این مورد جدول [plat]. این ستون کلید خارجی plat_id نام‌گذاری خواهد شد.

@Entity Adresse به شرح زیر است:


package entites;

...
@Entity
@Table(name="jpa10_hb_adresse")
public class Adresse implements java.io.Serializable {
  
  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private long id;
  
  @Column(name = "NUMERO_RUE")
  private int numeroRue;
  
  @Column(name = "NOM_RUE", length=30, nullable=false)
  private String nomRue;
  
  //گیرنده‌ها و تنظیم‌کننده‌ها
 ...
 
  // سازنده‌ها
  public Adresse(int streetNumber, String streetName){
...
  }
  
  public Adresse(){
    
  }
  
  // toString
  public String toString(){
    return "A["+getNumeroRue()+","+getNomRue()+"]";
  }
}
  • @Entity `Adresse` یک انتیت است که هیچ رابطه مستقیمی با سایر انتیت‌ها ندارد. این انتیت تنها از طریق انتیتی به نام `Restaurant` قابل پایداری است.
  • یک آدرس با نام خیابان (خط 16) و شماره خانه (خط 13) تعریف می‌شود.

@Entity Plat به شرح زیر است


package entites;
...
@Entity
@Table(name="jpa10_hb_plat")
public class Plat implements java.io.Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

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

    private boolean vegetarien;

    // سازنده‌ها
    public Plat() {

    }

    public Plat(String name, boolean vegetarian) {
...
    }

    // گیرنده‌ها و تنظیم‌کننده‌ها
...

    // toString
    public String toString() {
        return "E[" + getNom() + "," + isVegetarien() + "]";
    }

}
  • @Entity Plat یک انتیت است که هیچ رابطه مستقیمی با سایر انتیت‌ها ندارد. این انتیت فقط می‌تواند از طریق انتیت Restaurant پابرکار شود.
  • یک غذا با یک نام (خط ۱۲) و اینکه گیاهی است یا خیر (خط ۱۴) تعریف می‌شود.

2.7.3. پروژه اکلیپس / هایبرنت

پیاده‌سازی JPA که در اینجا استفاده شده، متعلق به Hibernate است. پروژه تست Eclipse به شرح زیر است:

در [1] پروژه اکلیپس، در [2] کد جاوا و در JPA پیکربندی قرار دارد. به وجود فایلی به نام [orm.xml] که قبلاً با آن مواجه نشده‌ایم، توجه کنید. این پروژه در [3] و در داخل پوشهٔ مثال‌ها [4] قرار دارد. آن را وارد خواهیم کرد.

2.7.4. تولید DDL از پایگاه داده

طبق دستورالعمل‌های بخش 2.1.7، فایل DDL که برای SGBD و MySQL5 به دست آمده، به شرح زیر است:


alter table jpa10_hb_restaurant 
        drop 
        foreign key FK3E8E4F5D5FE379D0;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D11F0F78A4;

    alter table jpa10_hb_restaurant_plat 
        drop 
        foreign key FK1D2D06D1AFAC3E44;

    drop table if exists jpa10_hb_adresse;

    drop table if exists jpa10_hb_plat;

    drop table if exists jpa10_hb_restaurant;

    drop table if exists jpa10_hb_restaurant_plat;

    create table jpa10_hb_adresse (
        id bigint not null auto_increment,
        NUMERO_RUE integer,
        NOM_RUE varchar(30) not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_plat (
        id bigint not null auto_increment,
        nom varchar(50) not null unique,
        vegetarien bit not null,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant (
        id bigint not null auto_increment,
        nom varchar(30) not null unique,
        adresse_id bigint,
        primary key (id)
    ) ENGINE=InnoDB;

    create table jpa10_hb_restaurant_plat (
        jpa10_hb_restaurant_id bigint not null,
        plat_id bigint not null,
        primary key (jpa10_hb_restaurant_id, plat_id)
    ) ENGINE=InnoDB;

    alter table jpa10_hb_restaurant 
        add index FK3E8E4F5D5FE379D0 (adresse_id), 
        add constraint FK3E8E4F5D5FE379D0 
        foreign key (adresse_id) 
        references jpa10_hb_adresse (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D11F0F78A4 (plat_id), 
        add constraint FK1D2D06D11F0F78A4 
        foreign key (plat_id) 
        references jpa10_hb_plat (id);

    alter table jpa10_hb_restaurant_plat 
        add index FK1D2D06D1AFAC3E44 (jpa10_hb_restaurant_id), 
        add constraint FK1D2D06D1AFAC3E44 
        foreign key (jpa10_hb_restaurant_id) 
        references jpa10_hb_restaurant (id);
  • رده‌های ۲۱–۲۶: جدول [adresse]
  • رده‌های ۲۸–۳۳: جدول [plat]
  • رده‌های ۳۵–۴۰: جدول [restaurant]
  • رده‌های ۴۲–۴۶: جدول الحاق [restaurant_plat]. توجه کنید به کلید مرکب (رده‌ی ۴۵)
  • خطوط ۴۸–۵۲: کلید خارجی از جدول [restaurant] به جدول [adresse]
  • خطوط 54–58: کلید خارجی از جدول [restaurant_plat] به جدول [plat]
  • خطوط ۶۰–۶۴: کلید خارجی از جدول [restaurant_plat] به جدول [restaurant]

این DDL با طرح‌بندی که قبلاً ارائه شده مطابقت دارد:

در نمای SQL Explorer، پایگاه داده به شرح زیر ساختار یافته است:

  • در [1]: ۴ جدول در پایگاه داده
  • در [2]: آدرس‌ها
  • در [3]: غذاها
  • در [4]: رستوران‌ها. [adresse_id] به آدرس‌های موجود در [2] ارجاع می‌دهد.
  • در [5]: جدول الحاق [restaurant,plat]. [jpa10_hb_restaurant_id] به رستوران‌های موجود در [4] ارجاع می‌دهد و [plat_id] به غذاهای موجود در [3] ارجاع می‌دهد. بنابراین، [1,1] به این معنی است که رستوران «Burger Barn» غذای «CheeseBurger» را سرو می‌کند.

برای به‌دست‌آوردن داده‌های فوق، برنامه [QueryDB] از پروژه Eclipse اجرا شد.

2.7.5. پرس‌وجوی JPQL با استفاده از کنسول Hibernate

ما در حال ایجاد یک کنسول Hibernate مرتبط با پروژه قبلی Eclipse هستیم. ما رویه‌ای را که پیش‌تر دو بار تشریح شده است، به‌ویژه در بخش 2.1.12، دنبال خواهیم کرد.

  • در [1] و [2]: پیکربندی کنسول Hibernate
  • در [3]: یک پرس‌وجو JPQL و در [4] نتیجه.
  • در [5]: دستور معادل SQL

اکنون مجموعه‌ای از پرس‌وجوها JPQL را ارائه می‌دهیم. از خواننده دعوت می‌شود آن‌ها را اجرا کند و ترتیب SQL تولیدشده توسط Hibernate برای اجرای آن‌ها را کشف کند.

تمام رستوران‌ها و غذاهایشان را بازیابی کنید:

رستوران‌هایی را که حداقل یک غذای گیاهی سرو می‌کنند بازیابی کنید:

نام رستوران‌هایی را که فقط غذاهای گیاهی سرو می‌کنند، دریافت کنید:

رستوران‌هایی را که برگر سرو می‌کنند، پیدا کنید:

2.7.6. QueryDB

ما اکنون در حال بررسی برنامه [QueryDB] از پروژه Eclipse هستیم که:

  • پایگاه داده را پر می‌کند
  • و تعدادی پرس‌وجو، JPQL، را بر روی آن صادر می‌کند. این پرس‌وجوها در فایل [META-INF/orm.xml] در پروژه Eclipse ثبت شده‌اند:

فایل [orm.xml] را می‌توان به جای حاشیه‌نویسی‌های جاوا برای پیکربندی لایه JPA استفاده کرد. این امر انعطاف‌پذیری را در پیکربندی لایه JPA فراهم می‌کند. این فایل را می‌توان بدون کامپایل مجدد کد جاوا تغییر داد. هر دو روش می‌توانند همزمان استفاده شوند: anotationهای جاوا و فایل [orm.xml]. پیکربندی JPA ابتدا با استفاده از anotationهای جاوا و سپس با استفاده از فایل [orm.xml] تنظیم می‌شود. بنابراین، اگر می‌خواهید یک پیکربندی ایجاد شده با استفاده از یک آنوتیشن جاوا را بدون کامپایل مجدد تغییر دهید، کافی است آن پیکربندی را در [orm.xml] قرار دهید. این پیکربندی اولویت خواهد داشت.

در مثال ما، فایل [orm.xml] برای ذخیره متون پرس‌وجو از JPQL استفاده می‌شود. محتویات آن به شرح زیر است:


<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0">
    <description>Restaurants</description>
    <named-query name="supprimer le contenu de la table restaurant">
        <query>delete from Restaurant</query>
    </named-query>
    <named-query name="supprimer le contenu de la table plat">
        <query>delete from Plat</query>
    </named-query>
    <named-query name="obtenir tous les restaurants">
        <query>select r from Restaurant r order by r.nom asc</query>
    </named-query>
    <named-query name="obtenir toutes les adresses">
        <query>select a from Adresse a order by a.nomRue asc</query>
    </named-query>
    <named-query name="obtenir tous les plats">
        <query>select p from Plat p order by p.nom asc</query>
    </named-query>
    <named-query name="obtenir tous les restaurants avec leurs plats">
        <query>select r.nom,p.nom from Restaurant r join r.plats p</query>
    </named-query>
    <named-query name="obtenir les restaurants ayant au moins un plat vegetarien">
        <query>select distinct r from Restaurant r join r.plats p where p.vegetarien=true</query>
    </named-query>
    <named-query name="obtenir les restaurants avec uniquement des plats vegetariens">
        <query>
            select distinct r1.nom from Restaurant r1 where not exists (select p1 from Restaurant r2 join r2.plats p1 where r2.id=r1.id and
            p1.vegetarien=false)
        </query>
    </named-query>
    <named-query name="obtenir les restaurants d'une certaine rue">
        <query>select r from Restaurant r where r.adresse.nomRue=:nomRue</query>
    </named-query>
    <named-query name="obtenir les restaurants qui servent des burgers">
        <query>select r.nom,r.adresse.numeroRue, r.adresse.nomRue, p.nom from Restaurant r join r.plats p where p.nom like '%burger'</query>
    </named-query>
    <named-query name="obtenir les plats du restaurant untel">
        <query>select p.nom from Restaurant r join r.plats p where r.nom=:nomRestaurant</query>
    </named-query>
</entity-mappings>
  • ریشهٔ فایل [orm.xml] عبارت است از <entity-mappings> (خط ۲).
  • خطوط ۵–۷: پرس‌وجوهای نام‌گذاری‌شده JPQL در میان تگ‌های <named-query name= "... ">text</namedquery> قرار گرفته‌اند.
    • ویژگی name تگ، نام پرس‌وجو است.
    • محتوای تگ متن پرس‌وجو است.

QueryDB پرس‌وجوهای قبلی را اجرا خواهد کرد. کد آن به شرح زیر است:


package tests;

...
public class QueryDB {

    // زمینه پایداری
    private static EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa");

    private static EntityManager em = emf.createEntityManager();

    public static void main(String[] args) {
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        // حذف ورودی‌های جدول [restaurant]
        em.createNamedQuery("supprimer le contenu de la table restaurant").executeUpdate();
        // حذف ورودی‌های جدول [plat]
        em.createNamedQuery("supprimer le contenu de la table plat").executeUpdate();
        //ایجاد اشیاء Address
        Adresse adr1 = new Adresse(10, "Main Street");
        Adresse adr2 = new Adresse(20, "Main Street");
        Adresse adr3 = new Adresse(123, "Dover Street");
        // ایجاد اشیاء 'Entree'
        Plat ent1 = new Plat("Hamburger", false);
        Plat ent2 = new Plat("Cheeseburger", false);
        Plat ent3 = new Plat("Tofu Stir Fry", true);
        Plat ent4 = new Plat("Vegetable Soup", true);
        // ایجاد اشیاء رستوران
        Restaurant restaurant1 = new Restaurant();
        restaurant1.setNom("Burger Barn");
        restaurant1.setAdresse(adr1);
        restaurant1.getPlats().add(ent1);
        restaurant1.getPlats().add(ent2);
        Restaurant restaurant2 = new Restaurant();
        restaurant2.setNom("Veggie Village");
        restaurant2.setAdresse(adr2);
        restaurant2.getPlats().add(ent3);
        restaurant2.getPlats().add(ent4);
        Restaurant restaurant3 = new Restaurant();
        restaurant3.setNom("Dover Diner");
        restaurant3.setAdresse(adr3);
        restaurant3.getPlats().add(ent1);
        restaurant3.getPlats().add(ent2);
        restaurant3.getPlats().add(ent4);
        // پایداری اشیاء رستوران (و سایر اشیاء به‌صورت آبشاری)
        em.persist(restaurant1);
        em.persist(restaurant2);
        em.persist(restaurant3);
        //پایان تراکنش
        tx.commit();
        // خروجی پایگاه داده
        dumpDataBase();
        // پایان EntityManager
        em.close();
        //پایان EntityManagerFactory
        emf.close();
    }

    // نمایش محتویات پایگاه داده
    @SuppressWarnings("unchecked")
    private static void dumpDataBase() {
        // test2
        log("données de la base");
        // شروع تراکنش
        EntityTransaction tx = em.getTransaction();
        tx.begin();
        //فهرست رستوران‌ها
        log("[restaurants]");
        for (Object restaurant : em.createNamedQuery("obtenir tous les restaurants").getResultList()) {
            System.out.println(restaurant);
        }
        // نمایش آدرس‌ها
        log("[adresses]");
        for (Object adresse : em.createNamedQuery("obtenir toutes les adresses").getResultList()) {
            System.out.println(adresse);
        }
        // نمایش غذاها
        log("[plats]");
        for (Object plat : em.createNamedQuery("obtenir tous les plats").getResultList()) {
            System.out.println(plat);
        }
        // نمایش‌های پیوند رستوران <--> غذا
        log("[restaurants/plats]");
        Iterator record = em.createNamedQuery("obtenir tous les restaurants avec leurs plats").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%s]%n", currentRecord[0], currentRecord[1]);
        }
        log("[Liste des restaurants avec au moins un plat végétarien]");
        for (Object r : em.createNamedQuery("obtenir les restaurants ayant au moins un plat vegetarien").getResultList()) {
            System.out.println(r);
        }
        // پرس‌وجو
        log("[Liste des restaurants avec seulement des plats végétariens]");
        for (Object r : em.createNamedQuery("obtenir les restaurants avec uniquement des plats vegetariens").getResultList()) {
            System.out.println(r);
        }
        // پرس‌وجو
        log("[Liste des restaurants dans Dover Street]");
        for (Object r : em.createNamedQuery("obtenir les restaurants d'une certaine rue").setParameter("nomRue", "Dover Street").getResultList()) {
            System.out.println(r);
        }
        // پرس‌وجو
        log("[Liste des restaurants ayant un plat de type burger]");
        record = em.createNamedQuery("obtenir les restaurants qui servent des burgers").getResultList().iterator();
        while (record.hasNext()) {
            Object[] currentRecord = (Object[]) record.next();
            System.out.format("[%s,%d,%s,%s]%n", currentRecord[0], currentRecord[1], currentRecord[2], currentRecord[3]);
        }
        // پرس‌وجو
        log("[Plats de Veggie Village]");
        for (Object r : em.createNamedQuery("obtenir les plats du restaurant untel").setParameter("nomRestaurant", "Veggie Village").getResultList()) {
            System.out.println(r);
        }
        //پایان تراکنش
        tx.commit();
    }

    // لاگ‌ها
    private static void log(String message) {
        System.out.println(" -----------" + message);
    }

}

نتیجه اجرای [QueryDB] به شرح زیر است:

-----------données de la base
 -----------[restaurants]
R[Burger Barn,A[10,Main Street],E[Cheeseburger,false],E[Hamburger,false]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
 -----------[adresses]
A[123,Dover Street]
A[10,Main Street]
A[20,Main Street]
 -----------[plats]
E[Cheeseburger,false]
E[Hamburger,false]
E[Tofu Stir Fry,true]
E[Vegetable Soup,true]
 -----------[restaurants/plats]
[Burger Barn,Cheeseburger]
[Burger Barn,Hamburger]
[Dover Diner,Cheeseburger]
[Dover Diner,Hamburger]
[Dover Diner,Vegetable Soup]
[Veggie Village,Tofu Stir Fry]
[Veggie Village,Vegetable Soup]
 -----------[Liste des restaurants avec au moins un plat végétarien]
R[Veggie Village,A[20,Main Street],E[Tofu Stir Fry,true],E[Vegetable Soup,true]]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants avec seulement des plats végétariens]
Veggie Village
 -----------[Liste des restaurants dans Dover Street]
R[Dover Diner,A[123,Dover Street],E[Cheeseburger,false],E[Hamburger,false],E[Vegetable Soup,true]]
 -----------[Liste des restaurants ayant un plat de type burger]
[Burger Barn,10,Main Street,Cheeseburger]
[Burger Barn,10,Main Street,Hamburger]
[Dover Diner,123,Dover Street,Cheeseburger]
[Dover Diner,123,Dover Street,Hamburger]
 -----------[Plats de Veggie Village]
Tofu Stir Fry
Vegetable Soup

ما این وظیفه را به خواننده واگذار می‌کنیم تا ارتباط بین کد و نتایج را برقرار کند. برای این کار، پیشنهاد می‌کنیم پرس‌وجوی JPQL را در کنسول Hibernate اجرا کرده و کد متناظر SQL را بررسی کنید.

خوانندگان علاقه‌مند می‌توانند پروژه قبلی را که با استفاده از Toplink پیاده‌سازی شده است، در مثال‌های موجود برای دانلود با این آموزش بیابند:

پروژه اکلیپس با Toplink، کپی‌ای از پروژه اکلیپس با Hibernate است:

فایل <persistence.xml> [2] موجودیت‌های مدیریت‌شده را اعلام می‌کند:


        <!--  ارائه‌دهنده -->
        <provider>oracle.toplink.essentials.PersistenceProvider</provider>
            <!-- کلاس‌های پایدار -->
        <class>entites.Restaurant</class>
        <class>entites.Adresse</class>
        <class>entites.Plat</class>

...
  • خطوط ۴–۶: موجودیت‌های مدیریت‌شده

پرس‌وجوهای JPQL که در [orm.xml] ذخیره شده‌اند، به‌درستی توسط Toplink اجرا می‌شوند. برای اطمینان از این موضوع، در پروژه قبلی مراقب بودیم از پرس‌وجوهای HQL (زبان پرس‌وجوی Hibernate) استفاده نکنیم، که در واقع یک ابرمجموعه از JPQL است و بخشی از نحو آن توسط JPQL پذیرفته نمی‌شود.

2.8. Conclusion

این پایان مطالعه ما در مورد موجودیت‌های JPA است. این یک فرآیند طولانی بود، اما با این حال برخی از جنبه‌های مهم (برای توسعه‌دهنده پیشرفته) پوشش داده نشده‌اند. بار دیگر، توصیه می‌شود کتاب مرجعی مانند کتابی که برای این آموزش استفاده شده است، مطالعه کنید:

[ref1]: Java Persistence with Hibernate، نوشته کریستین بائر و گاوین کینگ، منتشر شده توسط منینگ.