Tag: Java

  • Accessibility in Android and Usage

    1. Accessibility là gì

    • Điện thoại di động đã và đang trở thành một vật bất ly thân với mỗi người chúng ta. Tuy nhiên trong nhiều trường hợp, mọi người sẽ cảm thấy khó khăn trong việc sử dụng điện thoại di động. Điều này bao gồm một người bị mù bẩm sinh hoặc mất kỹ năng vận động trong một tai nạn. Điều này cũng bao gồm cả những người không thể sử dụng tay vì họ đang bế một đứa trẻ. Bạn có thể gặp khó khăn khi sử dụng điện thoại khi đeo găng tay khi trời lạnh. Có thể bạn gặp khó khăn trong việc phân biệt các mục trên màn hình khi trời sáng.
    • Trong những trường hợp này, thứ họ cần chính là những hỗ trợ từ những chiếc điện thoại thông minh. Accessibility từ đó được sinh ra để hỗ trợ chúng ta.
    • Dịch vụ trợ năng (Accessibility) là một tính năng của Android Framework được thiết kế để cung cấp phản hồi điều hướng thay thế cho người dùng thay mặt cho các ứng dụng được cài đặt trên thiết bị Android. Dịch vụ trợ năng có thể thay mặt ứng dụng giao tiếp với người dùng, chẳng hạn như bằng cách chuyển đổi văn bản thành giọng nói hoặc cung cấp phản hồi xúc giác khi người dùng di chuột trên một khu vực quan trọng của màn hình. Phòng học mã này chỉ cho bạn cách tạo một dịch vụ trợ năng rất đơn giản.

    2. Ứng dụng của Accessibility trong Android

    • Switch Access: cho phép các người dùng android bị hạn chế vận động tương tác với điện thoại qua một hoặc nhiều nút.
    • Voice Access (beta): cho phép các người dùng Android bị hạn chế vận động điều khiển thiết bị bằng cử chỉ giọng nói.
    • Talkback: một trình đọc màn hình thường được người khiếm thị hoặc người mù sử dụng.

    3. Hướng dẫn cài đặt Accessibility Service

    Cách cài đặt Accessibility Service trong project Android

    • Accessibility yêu cầu các điện thoại chạy chúng phải có phiên bản từ Android 7 trở lên
    • Cùng xem chúng ta cần những gì trong file AndroidManifest của service này
    <manifest xmlns:android="http://schemas.android.com/apk/res/android">
        <application>
            <service
                android:name=".HelperService"
                android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE" android:exported="true">
                <intent-filter>
                    <action android:name="android.accessibilityservice.AccessibilityService"/>
                </intent-filter>
                <meta-data
                    android:name="android.accessibilityservice"
                    android:resource="@xml/helper_service"/>
            </service>
        </application>
    </manifest>
    • Để có thể chạy service này ta cần thêm quyền android:permission=”android.permission.BIND_ACCESSIBILITY_SERVICE”
    • Sau đó ta thêm file metadata vào service: @xml/helper_service
    <accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
        android:accessibilityFeedbackType="feedbackGeneric"
        android:accessibilityFlags="flagDefault"
        android:canPerformGestures="true"
        android:canRetrieveWindowContent="true" />
    • Để thực hiện thao tác vuốt, android:canPerformGesture được đặt thành true
    • Để truy cập nội dung cửa sổ, android:canRetrieveWindowContent được đặt thành true.

    Sau đó, để triển khai các chức năng của AccessibilityService, chúng ta phải tạo một Service kế thừa AccessibilityService

    public class HelperService extends AccessibilityService {
        @Override
        public void onAccessibilityEvent(AccessibilityEvent event) {
        }
        @Override
        public void onInterrupt() {
        }
    }
    • Chúng ta sẽ tạo một view để hiển thị các nút bấm chức năng hỗ trợ trong service này
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button
            android:gravity="start"
            android:id="@+id/power"
            android:text="@string/power"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/volume_up"
            android:text="@string/volume_up"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/volume_down"
            android:text="@string/volume_down"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/swipe_down"
            android:text="@string/swipe_down"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
        <Button
            android:id="@+id/swipe_right"
            android:text="@string/swipe_right"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>
    </LinearLayout>
    • Kết quả chúng ta sẽ có được một view như sau:
    • Trong hàm onServiceConnected() của HelperService, chúng ta có thể khởi tạo một giao diện sử dụng quyền WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY – thứ sẽ giúp chúng ta vẽ lên trên màn hình.
        @Override
        protected void onServiceConnected() {
            WindowManager wm = (WindowManager) getSystemService(WINDOW_SERVICE);
            mLayout = new FrameLayout(this);
            WindowManager.LayoutParams lp = new WindowManager.LayoutParams();
            lp.type = WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY;
            lp.format = PixelFormat.TRANSLUCENT;
            lp.flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
            lp.width = WindowManager.LayoutParams.WRAP_CONTENT;
            lp.height = WindowManager.LayoutParams.WRAP_CONTENT;
            lp.gravity = Gravity.TOP;
            LayoutInflater inflater = LayoutInflater.from(this);
            inflater.inflate(R.layout.action_bar, mLayout);
        }

    Thiết lập các chức năng cho AccessibilityService

    • Từ các quá trình cài đặt trên chúng ta đã có giao diện cho service của mình, giờ là lúc chúng ta thêm các chức năng cho các nút bấm này
    • Ví dụ về chức năng tắt nguồn
        private void configurePowerButton() {
            Button powerButton = mLayout.findViewById(R.id.power);
            powerButton.setOnClickListener(view -> performGlobalAction(GLOBAL_ACTION_POWER_DIALOG));
        }
    • Ví dụ về chức năng tăng âm lượng:
        private void configureVolumeButtonUp() {
            Button volumeUpButton = mLayout.findViewById(R.id.volume_up);
            volumeUpButton.setOnClickListener(view -> {
                AudioManager audioManager = (AudioManager) getSystemService(AUDIO_SERVICE);
                audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
                        AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI);
            });
        }
    • Ví dụ về chức năng vuốt màn hình:
        private void configureSwipeButtonDown() {
            Button swipeButton = (Button) mLayout.findViewById(R.id.swipe_down);
            swipeButton.setOnClickListener(view -> {
                Path swipePath = new Path();
                swipePath.moveTo(100, 1000);
                swipePath.lineTo(100, 100);
                GestureDescription.Builder gestureBuilder = new GestureDescription.Builder();
                gestureBuilder.addStroke(new GestureDescription.StrokeDescription(swipePath, 0, 500));
                dispatchGesture(gestureBuilder.build(), null, null);
            });
        }

    4. Chạy thử trên thiết bị thật

    • Để chạy service này trên thiết bị thật, chúng ta phải chỉnh sửa phần configurations của mục run, chọn Launch OptionsNothing
    • Khi service đã được chạy thành công từ Android Studio, chúng ta phải vào phần Settings -> Additional settings -> Accessibility, chọn đến phần Downloaded Apps chúng ta sẽ thấy tên service của chúng ta (HelperService), ấn chọn vào service và bật service lên. Giao diện các chức năng hỗ trợ của chúng ta sẽ hiển thị lên.
    • Demo trên thiết bị thật

    5. Những hiểm nguy từ Accessibility Service

    • Accessibility Service được sinh ra với một mục đích rất tốt, đó là hộ trỡ những người khuyết tật hoặc những người hạn chế, khó trong các vận động. Tuy nhiên với các khả năng có thể đọc được các dữ liệu trên màn hình, điều khiển điện thoại,… nếu như chúng được sử dụng vào mục đích xấu thì sẽ rất dễ gây nguy hiểm, rủi ro bảo mật thông tin đến cho chủ sở hữu điện thoại.

    Để phòng tránh những rủi ro tiềm ẩn này thì chúng ta nên có những biện pháp phòng tránh như là:

    • Chỉ cài đặt các ứng dụng trên các chợ chính chủ của các hệ điều hành. Accessibility Service được đánh giá là một quyền nguy hiểm. Vậy nên việc kiểm duyệt các ứng dụng này diễn ra rất nghiêm ngặt.
    • Không tải/ cài app từ các nguồn không chính thống, trên các đường link lạ
    • Với các ứng dụng có yêu cầu quyền này, hãy đọc kỹ các điều khoản dịch vụ.

    Author: LamNT59
    References: https://codelabs.developers.google.com/codelabs/developing-android-a11y-service#0
    Github Demo: https://github.com/lamdev99/HelperService

  • Observable/ Observer trong RxJava (Rx in Android Part 2)

    Observable/ Observer trong RxJava (Rx in Android Part 2)

    Chào các bạn. Mình xin tiếp tục với chuỗi bài tìm hiểu Rx trong lập trình Android, cụ thể ở đây là RxJava. Hôm nay mình xin giới thiệu chi tiết hơn về 2 thành phần quan trọng, gần như là cốt lõi trong RxJava đó là Observable và Observer.

    1. Observable

    Observable trong RxJava là một thành phần quan trọng cho việc xử lý luồng dữ liệu trong phát triển ứng dụng Android. Observable đại diện cho một luồng dữ liệu có thể phát ra các sự kiện hoặc giá trị dữ liệu theo thời gian.

    Trong RxJava, bạn có thể tạo một Observable từ các nguồn dữ liệu khác nhau như danh sách, tập hợp, sự kiện giao diện người dùng, kết quả truy vấn cơ sở dữ liệu, gọi API mạng, và nhiều nguồn dữ liệu khác.

    Chúng ta sẽ có 5 loại Observable sau:

    • Observable
    • Single
    • Maybe
    • Flowable
    • Completable

    Để tạo Observable trong RxJava, bạn có thể sử dụng các phương thức như:

    • Observable.create(): Tạo một Observable từ mã logic tùy chỉnh. Bạn có thể sử dụng các phương thức của Observer để phát ra các sự kiện hoặc giá trị dữ liệu.

    • Observable.just(): Tạo một Observable từ một hoặc nhiều giá trị cụ thể. Observable này sẽ phát ra các giá trị này và hoàn thành sau đó.

    • Observable.interval(): Tạo một Observable phát ra các số nguyên liên tục sau một khoảng thời gian nhất định.

    • Observable.fromIterable(): Tạo một Observable từ một danh sách, một tập hợp hoặc một iterable.

    • Observable.fromCallable(): Tạo một Observable từ một Callable, nơi bạn có thể thực hiện các tác vụ bất đồng bộ và trả về một giá trị.

    Khi bạn đã tạo Observable, bạn có thể sử dụng các toán tử để biến đổi, lọc và xử lý dữ liệu trong Observable theo nhu cầu của bạn. Sau đó, bạn có thể đăng ký (Subscribe) một Observer với Observable để nhận và xử lý các sự kiện và giá trị được phát ra từ Observable.

    Các Observable trong RxJava cho phép bạn xử lý dữ liệu một cách linh hoạt, thực hiện các tác vụ bất đồng bộ và tương tác với các thành phần Android khác trong việc phát triển ứng dụng Android.

    2. Observer

    Observer trong RxJava là một thành phần quan trọng để nhận và xử lý các sự kiện hoặc giá trị từ một Observable trong phát triển ứng dụng Android. Observer đăng ký (Subscribe) với một Observable để nhận thông báo về các sự kiện và giá trị được phát ra từ Observable đó.

    Chúng ta sẽ có 5 loại Observer sau:

    • Observer
    • SingleObserver
    • MaybeObserver
    • CompletableObserver

    Trong RxJava, bạn có thể tạo một Observer bằng cách triển khai đối tượng Observer<T>. Đối tượng này định nghĩa các phương thức mà bạn cần triển khai để xử lý các sự kiện và giá trị từ Observable.

    Các phương thức chính trong giao diện Observer bao gồm:

    • onNext(T value): Phương thức này được gọi khi một giá trị mới được phát ra từ Observable. Bạn có thể định nghĩa các hành động xử lý khi nhận được giá trị này.

    • onError(Throwable throwable): Phương thức này được gọi khi có một lỗi xảy ra trong quá trình phát ra giá trị từ Observable. Bạn có thể xử lý và báo cáo lỗi trong phương thức này.

    • onComplete(): Phương thức này được gọi khi Observable hoàn thành việc phát ra các giá trị. Bạn có thể thực hiện các hành động dọn dẹp hoặc xử lý cuối cùng trong phương thức này.

    Khi bạn đã triển khai giao diện Observer, bạn có thể đăng ký Observer với một Observable bằng cách sử dụng phương thức subscribe() trên Observable. Khi đăng ký thành công, Observer sẽ nhận các sự kiện và giá trị từ Observable và thực hiện các hành động xử lý tương ứng.

    Ví dụ:

    val observable: Observable<String> = Observable.just("Android", "RxJava", "RxAndroid");
    
    val observer: Observer<String> = object : Observer<String> {
        override fun onSubscribe(d: Disposable) {}
    
        override fun onError(e: Throwable) {
            // Handle when an error occurs during value generation
        }
    
        override fun onComplete() {
            // Handle when Observable finishes emitting value
        }
    
        override fun onNext(t: String) {
            // Handle value which receive from Observable
        }
    };
    
    observable.subscribe(observer)
    
    

    Trên đây là cách sử dụng Observer trong RxJava trong phát triển ứng dụng Android. Observer giúp bạn nhận và xử lý các sự kiện và giá trị từ Observable một cách linh hoạt và dễ dàng.

    Sau đây mình sẽ nói về sự kết hợp và lấy ví dụ cho từng loại Observable và Observer với nhau.

    3. Các loại và triển khai của Observable/ Observer

    Như chúng ta đã đề cập ở trên có 5 loại Observable và 4 loại Observer. Bảng dưới đây sẽ mô tả sự tương ứng giữa Observable và Observer cũng như số emissions của từng loại

    Observable Observer Nums of emissions
    Observable Observer Multiple or None
    Single SingleObserver One
    Maybe SingleObserver One or None
    Flowable Observer Multiple or None
    Completable CompletableObserver None

    3.1. Observable & Observer

    Observable là một loại được sử dụng khá phổ biến. Nó có thể phát ra một hoặc nhiều items. Mình sẽ triển khai 1 ví dụ minh hoạ sau:

    Đầu tiên, chúng ta sẽ tạo một Observable:

    val observableList = arrayListOf("RxJava", "RxAndroid", "Coroutine")
    
    val observable: Observable<String> = Observable.create { emitter ->
        // emit each item
        for (item in observableList) {
            Log.i("PhongPN3", "emitter: $item - ${Thread.currentThread().name}")
            emitter.onNext(item)
        }
    
        // all items are emitted
        emitter.onComplete()
    }
    
    

    Chúng ta sử dụng hàm onNext() để phát ra mỗi item. Khi nào hoàn thành quá trình emission, chúng ta sẽ dùng hàm onComplete(). Bước tiếp theo chúng ta định nghĩa Observer để handle các item được phát ra.

    val observer: Observer<String> = object : Observer<String> {
        override fun onSubscribe(d: Disposable) {
            Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
        }
    
        override fun onNext(t: String) {
            Log.i("PhongPN3", "onNext: $t - ${Thread.currentThread().name}")
        }
    
        override fun onError(e: Throwable) {
            Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
        }
    
        override fun onComplete() {
            Log.i("PhongPN3", "onComplete - ${Thread.currentThread().name}")
        }
    }
    

    Cuối cùng là subscribe việc lắng nghe dữ liệu từ 1 Observable.

    observable.subscribe(observer)
    

    Kết quả sẽ là:

    onSubscribe - main
    emitter: RxJava - main
    onNext: RxJava - main
    emitter: RxAndroid - main
    onNext: RxAndroid - main
    emitter: Coroutine - main
    onNext: Coroutine - main
    onComplete - main
    
    3.2. Single & SingleObserver

    Single luôn luôn emit một item duy nhất hoặc ném ra một ngoại lệ nào đó.

    val s = "RxJava"
    val singleObservable: Single<String> = Single.create { emitter ->
        emitter.onSuccess(s)
    }
    

    SingleObserver cũng sẽ khác với Observer bình thường, cụ thể nó sẽ không có hàm onNext() và onComple(), thay đó sẽ làm hàm onSuccess().

    val singleObserver: SingleObserver<String> = object : SingleObserver<String> {
        override fun onSubscribe(d: Disposable) {
            Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
        }
    
        override fun onError(e: Throwable) {
            Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
        }
    
        override fun onSuccess(t: String) {
            Log.i("PhongPN3", "onSuccess: $t - ${Thread.currentThread().name}")
        }
    }
    

    Cuối cùng là subscribe việc lắng nghe dữ liệu từ 1 Observable.

    singleObservable.subscribe(singleObserver)
    

    Kết quả sẽ là:

    onSubscribe - main
    onSuccess: RxJava - main
    
    3.3. Maybe & MaybeObserver

    Maybe là loại Observable mà có thể phát 1 item hoặc ko phát item nào cả (có 1 hoặc ko có gì). Với Maybe chúng ta sẽ sử dụng cho trường hợp giá trị muốn nhận là tùy biến có thể có hoặc ko. Ví dụ chúng ta query note by Id trong database nó có thể có hoặc cũng có thể không.

    val s = "RxJava"
    val maybeObservable = Maybe.create { emitter: MaybeEmitter<String> ->
        emitter.onSuccess(s)
    }
    

    Nếu muốn phát ra item, chúng ta sẽ sử dụng onSuccess, còn nếu ko muốn phát ra item thì chúng ta sẽ sử dụng onComplete. Đây chính là điểm khác nhau với Single observable.

    val maybeObserver: MaybeObserver<String> = object : MaybeObserver<String> {
        override fun onSubscribe(d: Disposable) {
            Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
        }
    
        override fun onError(e: Throwable) {
            Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
        }
    
        override fun onSuccess(t: String) {
            Log.i("PhongPN3", "onSuccess: $t - ${Thread.currentThread().name}")
        }
    
        override fun onComplete() {
            Log.i("PhongPN3", "onComplete - ${Thread.currentThread().name}")
        }
    }
    

    Cuối cùng là subscribe việc lắng nghe dữ liệu từ 1 Observable.

     maybeObservable.subscribe(maybeObserver)
    

    Kết quả sẽ là:

    onSubscribe - main
    onSuccess: RxJava - main
    
    3.4. Completable & CompletableObserver

    Completable là loại Observable sẽ ko phát bất kỳ item nào mà nó chỉ thực thi một nhiệm vụ nào đó và thông báo nhiệm vụ hoàn thành hoặc chưa hoàn thành.

    Khởi tạo Observable:

    val completableObservable = Completable.create { emitter: CompletableEmitter ->
        // do something
        emitter.onComplete()
    }
    

    Định nghĩa Observer:

    val completeObserver: CompletableObserver = object : CompletableObserver {
        override fun onSubscribe(d: Disposable) {
            Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
        }
    
        override fun onError(e: Throwable) {
            Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
        }
    
        override fun onComplete() {
            Log.i("PhongPN3", "onComplete - ${Thread.currentThread().name}")
        }
    }
    

    Cuối cùng là subscribe việc lắng nghe dữ liệu từ Observable.

    completableObservable.subscribe(completeObserver)
    

    Kết quả sẽ là:

    onSubscribe - main
    onComplete - main
    
    3.5. Flowable & SingleObsever

    Được sử dụng khi một Observable tạo ra số lượng lớn các sự kiện / dữ liệu mà Observer có thể xử lý. Flowable có thể được sử dụng khi nguồn tạo ra rất nhiều sự kiện (theo nhiều tài liệu là khoảng 10k+ sự kiện) và Onserver không thể tiêu thụ tất cả. Flowable sử dụng phương pháp Backpressure để xử lý dữ liệu tránh lỗi MissingBackpressureException và OutOfMemoryError.

    Ở ví dụ này, chúng ta sẽ tính tổng từ 1 đến 10, và kết quả sẽ được thông báo cho một SingleObserver.

    val flowable = Flowable.range(1, 10)
    
    val singleObserver: SingleObserver<Int> = object : SingleObserver<Int> {
        override fun onSubscribe(d: Disposable) {
            Log.i("PhongPN3", "onSubscribe - ${Thread.currentThread().name}")
        }
    
        override fun onError(e: Throwable) {
            Log.i("PhongPN3", "onError: ${e.message} - ${Thread.currentThread().name}")
        }
    
        override fun onSuccess(t: Int) {
            Log.i("PhongPN3", "onSuccess: $t - ${Thread.currentThread().name}")
        }
    }
    
    
    flowable.reduce(0) { sum: Int, item: Int ->
        sum + item
    }.subscribe(singleObserver)
    
    
    

    Hàm reduce có tác dụng xử lý từng item mà flowable phát ra và trả về một giá trị là tổng của tất cả items.

    Kết quả sẽ là:

    onSubscribe - main
    onSuccess: 55 - main
    

    Lưu ý : Ở các ví dụ source code tham khảo, mình hay để lại Log để các bạn có thể tiện thử chạy và ra output giống kết quả mà mình trình bày.

    Tổng kết

    Trên đây là các loại và cách triển khai của các loại Observable và Observer tương ứng. Mình hy vọng bài viết phần nào giúp mọi người hiểu và nắm được cách sử dụng cơ bản nhất về 2 thành phần này RxJava.

    Bài viết sắp tới mình sẽ tiếp tục với các Operator trong RxJava. Hẹn mọi người ở bài viết sắp tới.

  • Ánh xạ quan hệ 1-1 sử dụng chia sẻ khoá chính trong Hibernate

    Ánh xạ quan hệ 1-1 sử dụng chia sẻ khoá chính trong Hibernate

    Các chương trình máy tính thể hiện các nhu cầu thực tế của con người, chúng ánh xạ các đối tượng trong thế giới thực thành các thực thể. Khi thực hiện quá trình ánh xạ đó, chúng ta thực hiện ánh xạ cả mối quan hệ giữa chúng. Trong bài viết này chúng ta đặt mối quan tâm tới các đối tượng có mối quan hệ 1-1 với nhau. Chúng ta sẽ cùng tìm hiểu cách chúng được thể hiện trong chương trình máy tính như thế nào.

    Các đối tượng trong thế giới thực được phản ánh trong chương trình máy tính như thế nào?

    Trước tiên chúng ta thấy các đối tượng sẽ được ánh xạ tương ứng thành các class trong các ngôn ngữ lập trình. Khi chúng được lưu trữ vào database, chúng sẽ được ánh xạ thành các bản ghi của một bảng. Vậy thì mối quan hệ giữa chúng được định nghĩa như thế nào? Đối với các bảng trong database, các khoá trong bảng sẽ thể hiện mối quan hệ giữa các bảng. Đối với quan hệ 1-1 chúng ta có thể định nghĩa theo 2 cách:

    • Sử dụng khoá ngoại duy nhất (một cột được đánh dấu là khoá ngoại và nó cũng là duy nhất trong bảng đó).
    • Hai bảng cùng chia sẻ khoá chính.

    Các đối tượng được phản ánh thành các bảng trong database

    Chúng ta cùng xem xét các thực thể được phản ánh thành các bảng trong database thông qua một vài ví dụ các bảng được thiết kế trong database như thế nào. Đối với cách sử dụng khoá ngoại duy nhất, các bảng có thể được định nghĩa như sau:

    Trong trường hợp bạn sử dụng cách chia sẻ khoá chính giữa hai bảng, các bảng trong database có thể được định nghĩa như sau:

    Các đối tượng được ánh xạ thành các class như thế nào?

    Đối với cách sử dụng khoá ngoại duy nhất, chúng ta có thể tham khảo cách định nghĩa mối quan hệ của chúng trong Spring Boot qua các bài viết sau:

    Trường hợp bạn sử dụng cách chia sẻ khoá chính giữa hai bảng chúng ta tìm hiểu qua từng bước dưới đây.

    Định nghĩa các bảng trong database

    Với ví dụ ở trên các bạn có thể sử dụng đoạn mã sau để tạo ra các bảng:

    CREATE TABLE IF NOT EXISTS `user` (
      `id` BIGINT NOT NULL AUTO_INCREMENT
      , `username` VARCHAR(255) UNIQUE
      , `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
      , `created_by` BIGINT DEFAULT NULL
      , `updated_at` DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
      , `updated_by` BIGINT DEFAULT NULL
      , `deleted_at` DATETIME DEFAULT NULL
      , `deleted_by` BIGINT DEFAULT NULL
      , PRIMARY KEY (`id`)
    );
    
    CREATE TABLE IF NOT EXISTS `user_info` (
      `user_id` BIGINT NOT NULL
      , `first_name` VARCHAR(255)
      , `last_name` VARCHAR(255)
      , PRIMARY KEY (`user_id`)
      , FOREIGN KEY (`user_id`) REFERENCES `user` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
    );
    

    Chúng ta có thể tham khảo cách migrate các bảng này bằng cách sử dụng Flyway trong bài viết Hướng dẫn migrate cơ sở dữ liệu sử dụng Flyway trong ứng dụng Spring Boot .

    Định nghĩa các entity để ánh xạ các bảng với các class

    Tiếp theo chúng ta cần định nghĩa các entity thành các class tương ứng. Từ đó, chúng ta có thể thực hiện các thao tác CRUD hoặc các thao tác truy vấn trên các bảng tương ứng.

    /**
     * <code>user_info</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity(name = "user_info")
    public class UserInfo {
    
      /** <code>user_id</code>. */
      @Id
      private Long userId;
    
      /** <code>first_name</code>. */
      private String firstName;
    
      /** <code>last_name</code>. */
      private String lastName;
    
      @MapsId
      @ToString.Exclude
      @PrimaryKeyJoinColumn
      @Fetch(FetchMode.JOIN)
      @OneToOne(cascade = CascadeType.PERSIST, optional = false, fetch = FetchType.EAGER)
      private User user;
    }
    
    /**
     * <code>user</code>.
     *
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @ToString
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity(name = "user")
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class User {
    
      /** <code>id</code>. */
      @Id
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>username</code>. */
      private String username;
    
      /** <code>created_at</code>. */
      private LocalDateTime createdAt;
    
      /** <code>created_by</code>. */
      private Long createdBy;
    
      /** <code>updated_at</code>. */
      private LocalDateTime updatedAt;
    
      /** <code>updated_by</code>. */
      private Long updatedBy;
    
      /** <code>deleted_at</code>. */
      private LocalDateTime deletedAt;
    
      /** <code>deleted_by</code>. */
      private Long deletedBy;
    
      /** <code>user_info.user_id</code> */
      @ToString.Exclude
      @OneToOne(mappedBy = "user", fetch = FetchType.EAGER)
      private UserInfo userInfo;
    }
    

    Chúng ta sử @Id để đánh dấu thuộc tính được ánh xạ tương ứng với trường khoá chính của bảng. Trong ví dụ này, chúng ta sử dụng trường tự tăng để sinh ra khoá chính cho bảng, do đó chúng ta sử dụng @GeneratedValue(strategy = GenerationType.IDENTITY) để thông báo với Hibernate rằng trường này sẽ được tự sinh trong database.

    Tiếp theo là phần quan trọng nhất, chúng ta sử dụng @MapsId để đánh dấu thuốc tính định nghĩa mối quan hệ 1-1 cùng với @OneToOne để xác định thực thể trong bảng có quan hệ 1-1 tương ứng. @MapsId sẽ thông báo cho Hibernate biết rằng chúng ta đang sử dụng khoá chính làm trường để thực hiện phép JOIN.

    Tiếp đến để thực hiện ánh xạ quan hệ 1-1 hai chiều, chúng ta sử dụng @OneToOne(mappedBy = "user", fetch = FetchType.EAGER) để đánh dấu thuộc tính ánh xạ sang thực thể nguồn đã được định nghĩa ở trên. Thuộc tính mappedBy chính là tên thuộc tính được khai báo với @MapsId ở trên.

    Xác nhận việc định nghĩa quan hệ 1-1

    Tiếp theo chúng ta cùng viết một đoạn chương trình nhỏ để kiểm tra lại các bước đã thực hiện ở trên.

    /**
     * Main.
     *
     * @author Hieu Nguyen
     */
    @Component
    @RequiredArgsConstructor
    public class Main implements CommandLineRunner {
    
      private final UserRepository userRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var uuid = UUID.randomUUID();
        var userInfo = UserInfo.builder().firstName("Hieu-" + uuid).lastName("Nguyen-" + uuid).build();
        var user = User.builder().username("hieunv-" + UUID.randomUUID()).userInfo(userInfo).build();
        userInfo.setUser(user);
        userRepository.save(user);
      }
    }
    

    Chạy thử đoạn chương trình này chúng ta sẽ nhận được output như sau:

    2023-02-15 18:56:38.262 DEBUG 25263 --- [           main] org.hibernate.SQL                        : 
        insert 
        into
            user
            (created_at, created_by, deleted_at, deleted_by, passport_id, updated_at, updated_by, username) 
        values
            (?, ?, ?, ?, ?, ?, ?, ?)
    Hibernate: 
        insert 
        into
            user
            (created_at, created_by, deleted_at, deleted_by, passport_id, updated_at, updated_by, username) 
        values
            (?, ?, ?, ?, ?, ?, ?, ?)
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [TIMESTAMP] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [TIMESTAMP] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [4] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [5] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [6] as [TIMESTAMP] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [7] as [BIGINT] - [null]
    2023-02-15 18:56:38.264 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [8] as [VARCHAR] - [hieunv-48779f8f-4efe-4d15-b658-92ebd3f7d9a3]
    2023-02-15 18:56:38.278 DEBUG 25263 --- [           main] org.hibernate.SQL                        : 
        insert 
        into
            user_info
            (first_name, last_name, user_id) 
        values
            (?, ?, ?)
    Hibernate: 
        insert 
        into
            user_info
            (first_name, last_name, user_id) 
        values
            (?, ?, ?)
    2023-02-15 18:56:38.278 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [Hieu-3c8726d4-0d3f-49f6-ba05-819bbb428863]
    2023-02-15 18:56:38.278 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Nguyen-3c8726d4-0d3f-49f6-ba05-819bbb428863]
    2023-02-15 18:56:38.278 TRACE 25263 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [BIGINT] - [9]
    

    Chúng ta thấy rằng có 2 bản ghi đã được insert vào 2 bảng chúng ta đã định nghĩa ở trên.

    Tổng kết

    Trong bài viết này chúng ta đã cùng đi từ các khái niệm cơ bản liên quan đến quan hệ 1-1 cũng như cách triển khai quan hệ này với MySQL database. Sau đó chúng ta cũng viết một ứng dụng đơn giản bằng Spring Boot để minh hoạ cơ chế hoạt động của quan hệ này.

  • Ánh xạ one-to-one bidirectional trong Hibernate sử dụng khoá ngoại

    Ánh xạ one-to-one bidirectional trong Hibernate sử dụng khoá ngoại

    Chúng ta đã cùng tìm hiểu về ánh xạ one-to-one unidirectional. Chúng ta cũng đã đề cập tới one-to-one bidirectional. Với one-tone-unidirectional chúng ta đã có thể truy cập thực thể đích từ thực thể nguồn nhưng không thể truy cập ngược lại. Trong bài viết này chúng ta sẽ cùng tìm hiểu những hạn chế của quan hệ unidirectional và tại sao chúng ta cần quan hệ bidirectional.

    Những hạn chế của one-to-one unidirectional

    Trong bài viết trước chúng ta đã định nghĩa hai thực thể CustomerAccount có quan hệ thông qua khoá ngoại ACCOUNT_ID. Nếu bằng cách nào đó, chúng ta chỉ xoá thực thể Account và để nguyên thực thể Customer, khi đó khoá ngoại trong bảng Customer sẽ tham chiếu tới một đối tượng không tồn tại, vấn đề này còn được gọi là dangling foreign key. Tuỳ chọn xoá thực thể Customer khi thực thị Account bị xoá phụ thuộc vào thiết kế cơ dữ liệu, đôi khi chúng ta muốn giữ lại thực thể Customer dưới dạng thông tin lưu trữ để theo dõi lịch sử. Chúng ta có thể làm điều này mà không cần thay đổi cơ sở dữ liệu bằng cách thay đổi thực thể Account

    /**
     * <code>account</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Entity(name = "account")
    @ToString(onlyExplicitlyIncluded = true)
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class Account {
    
      /** <code>id</code>. */
      @Id
      @ToString.Include
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>username</code>. */
      @ToString.Include private String username;
    
      @OneToOne(mappedBy = "account", cascade = CascadeType.ALL)
      private Customer customer;
    }
    

    Ở thực thể Account chúng ta không có cột nào có thể sử dụng để tham chiếu tới thực thể Customer. Do đó chúng ta cần đến sự hỗ trợ từ Hibernate.

    Cài đặt one-to-one bidirectional

    Chúng ta chỉ cần thêm đoạn mã sau vào thực thể Account:

      ...
      @OneToOne(mappedBy = "account", cascade = CascadeType.ALL)
      private Customer customer;
      ...
    

    Định nghĩa mappedBy = "account" sẽ thông báo cho Hibernate biết rằng nó cần tìm thuộc tính account trong thực thể Customer và liên kết thực thể cụ thể đó với đối tượng Account. Bây giờ chúng ta cùng thêm một thực thể vào database nhưng lúc này chúng ta sẽ save thực thể Account và thực thể Customer cũng sẽ được thêm vào database vì chúng ta đã sử dụng cascade = CascadeType.ALL.

    @Component
    public class Main implements CommandLineRunner {
      @Autowired private AccountRepository accountRepository;
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var customer = Customer.builder().firstName("Hieu").lastName("Nguyen").build();
        var account = Account.builder().username("hieunv").customer(customer).build();
        customer.setAccount(account);
        accountRepository.save(account);
      }
    }
    

    Sau khi chạy đoạn mã trên, chúng ta thấy rằng có 2 bản ghi đã được insert vào database. Chúng ta cũng sẽ thấy ouput như sau:

    2023-02-07 13:05:42.746 DEBUG 90289 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-07 13:05:42.746 DEBUG 90289 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-07 13:05:42.764 DEBUG 90289 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-07 13:05:42.767 DEBUG 90289 --- [           main] org.hibernate.SQL                        : insert into account (username) values (?)
    Hibernate: insert into account (username) values (?)
    2023-02-07 13:05:42.769 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [hieunv]
    2023-02-07 13:05:42.778 DEBUG 90289 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 2
    2023-02-07 13:05:42.778 DEBUG 90289 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-07 13:05:42.779 DEBUG 90289 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-07 13:05:42.779 DEBUG 90289 --- [           main] org.hibernate.SQL                        : insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    Hibernate: insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    2023-02-07 13:05:42.779 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [2]
    2023-02-07 13:05:42.779 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Hieu]
    2023-02-07 13:05:42.779 TRACE 90289 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [Nguyen]
    2023-02-07 13:05:42.792 DEBUG 90289 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 2
    2023-02-07 13:05:42.792 DEBUG 90289 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-07 13:05:42.792 DEBUG 90289 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-07 13:05:42.793 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-07 13:05:42.793 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-07 13:05:42.794 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 0 deletions to 2 objects
    2023-02-07 13:05:42.794 DEBUG 90289 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-07 13:05:42.795 DEBUG 90289 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-07 13:05:42.795 DEBUG 90289 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=2, username=hieunv, customer=app.demo.entity.Customer#2}
    2023-02-07 13:05:42.795 DEBUG 90289 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=2, account=app.demo.entity.Account#2}
    

    Nhìn vào ouput trên chúng ta thấy rằng có 2 bản ghi đã được insert vào database với id=2.

    Truy xuất thông tin Customer từ thực thể Account

    Bây giờ chúng ta cùng tìm hiểu xem bằng cách nào Hibernate có thể lấy thông tin Customer thông qua thực thể Account. Chúng ta cùng cài đặt thử đoạn mã sau:

    @Component
    @Slf4j
    public class Main implements CommandLineRunner {
      @Autowired private AccountRepository accountRepository;
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        log.info("{}", accountRepository.findById(2L).get().getCustomer());
      }
    }
    

    Sau khi chạy chương trình các bạn sẽ thấy output:

    2023-02-07 13:17:51.141 DEBUG 90956 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-07 13:17:51.141 DEBUG 90956 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-07 13:17:51.157 DEBUG 90956 --- [           main] org.hibernate.SQL                        : select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    Hibernate: select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    2023-02-07 13:17:51.159 TRACE 90956 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [2]
    2023-02-07 13:17:51.170 DEBUG 90956 --- [           main] l.p.e.p.i.EntityReferenceInitializerImpl : On call to EntityIdentifierReaderImpl#resolve, EntityKey was already known; should only happen on root returns with an optional identifier specified
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Account#2]
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `username` : value = hieunv
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`username`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `customer` : value = 2
    2023-02-07 13:17:51.173 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`customer`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Account#2]
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Customer#2]
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `account` : value = 2
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`account`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `firstName` : value = Hieu
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`firstName`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `lastName` : value = Nguyen
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`lastName`)  - enhanced for lazy-loading? - false
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Customer#2]
    2023-02-07 13:17:51.174 DEBUG 90956 --- [           main] .l.e.p.AbstractLoadPlanBasedEntityLoader : Done entity load : app.demo.entity.Account#2
    2023-02-07 13:17:51.175  INFO 90956 --- [           main] app.demo.Main                            : Customer(id=2, firstName=Hieu, lastName=Nguyen)
    2023-02-07 13:17:51.175 DEBUG 90956 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-07 13:17:51.175 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-07 13:17:51.178 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 0 deletions to 2 objects
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=2, username=hieunv, customer=app.demo.entity.Customer#2}
    2023-02-07 13:17:51.179 DEBUG 90956 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=2, account=app.demo.entity.Account#2}
    
    

    Các bạn chú ý tới dòng sau:

    2023-02-07 13:17:51.157 DEBUG 90956 --- [           main] org.hibernate.SQL                        : select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    Hibernate: select account0_.id as id1_0_0_, account0_.username as username2_0_0_, customer1_.id as id1_1_1_, customer1_.account_id as account_4_1_1_, customer1_.first_name as first_na2_1_1_, customer1_.last_name as last_nam3_1_1_ from account account0_ left outer join customer customer1_ on account0_.id=customer1_.account_id where account0_.id=?
    

    Các bạn có thể thấy rằng thực thể Customer có thể đường truy xuất thông qua câu lệnh LEFT OUTER JOIN. Chúng ta cũng có thể thực hiện các thao tác updatedelete theo cùng cách như đã thực hiện với one-to-one unidirectional.

    Tổng kết

    Trong bài viết này chúng ta đã chỉ ra những vấn đề đối với quan hệ one-to-one unidirectional và cách triển khai quan hệ one-to-one bidirectional trong Hibernate để giải quyết các vấn đề với one-to-one unidirectional.

  • Ánh xạ one-to-one unidirectional trong Hibernate sử dụng khoá ngoại

    Ánh xạ one-to-one unidirectional trong Hibernate sử dụng khoá ngoại

    Hibernate là một framework cung cấp một số lớp trừu tượng, nghĩa là lập trình viên không phải lo lắng về việc triển khai, Hibernate tự thực hiện các triển khai bên trong nó như thiết lập một kết nối cơ sở dữ liệu, viết các truy vấn để thực hiện các thao tác CRUD, … Nó là một java framework được sử dụng để phát triển persistence logic. Persistence logic có nghĩa là lưu trữ và xử lí dữ liệu để sử dụng lâu dài. Chính xác hơn *Hibernate * là một framework ORM (Object Relational Mapping) mã nguồn mở để phát triển các đối tượng độc lập với các phần mềm cơ sở dữ liệu và tạo ra persistence logic độc lập với Java, J2EE.

    Ánh xạ one-to-one là gì?

    Anh xạ one-to-one thể hiện rằng một thực thể duy nhất có mối liên kết với một thể hiện duy nhất của một thực thể khác. Một thể hiện của thực thể nguồn có thể được ánh xạ tới nhiều nhất một thể hiện của thực thể đích. Một số ví dụ minh hoạ ánh xạ one-to-one:

    • Mỗi người chỉ có duy nhất một hộ chiếu, một hộ chiếu chỉ được liên kết với duy nhất một người.
    • Mỗi con báo có một mẫu đốm độc nhất, một mẫu đốm chỉ được liên kết với duy nhất một con báo.
    • Mỗi chúng ta có một định danh duy nhất ở trường đại học, mỗi định danh dược liên kết với một người duy nhất.

    Trong các hệ quản trị cơ sở dữ liệu, ánh xạ one-to-one thường có hai kiểu:

    • one-to-one unidirectional
    • one-to-one bidirectional

    one-to-one unidirectional

    Ở kiểu ánh xạ này một thực thể có một thuộc tính hoặc một cột tham chiếu tới một thuộc tính hoặc một cột ở thực thể đích. Chúng ta cùng xem ví dụ sau:

    Bảng customer tham chiếu tới bảng account thông qua khoá ngoại ACCOUNT_ID. Thực thể đích (account) không có cách nào tham chiếu tới bảng customer nhưng bảng customer có thể truy cập tới bảng account thông qua khoá ngoại. Quan hệ trên được sinh ra bởi kịch bản SQL sau:

    CREATE TABLE IF NOT EXISTS `ACCOUNT` (
      `ID` BIGINT NOT NULL AUTO_INCREMENT
      , `USERNAME` VARCHAR(255) UNIQUE
      , PRIMARY KEY (`ID`)
    );
    
    CREATE TABLE IF NOT EXISTS `CUSTOMER` (
      `ID` BIGINT NOT NULL AUTO_INCREMENT
      , `FIRST_NAME` VARCHAR(255) NULL DEFAULT NULL
      , `LAST_NAME` VARCHAR(255) NULL DEFAULT NULL
      , `ACCOUNT_ID` BIGINT NOT NULL UNIQUE
      , PRIMARY KEY (`ID`)
      , FOREIGN KEY (`ACCOUNT_ID`)
        REFERENCES `demo`.`ACCOUNT`(`ID`) ON DELETE NO ACTION ON UPDATE NO ACTION
    );
    

    Khi tạo bảng customer chúng ta tham chiếu khoá chính trong bảng account (account_id). Chúng ta cố tình đặt ON DELETE NO ACTIONON UPDATE NO ACTION vì chúng ta sẽ đặt các giá trị này bên trong Hibernate. Bây giờ chúng ta sẽ thực hiện migration kịch bản này bằng Flyway. Tham khảo Hướng dẫn migrate cơ sở dữ liệu sử dụng Flyway trong ứng dụng Spring Boot.

    Trước khi bắt đầu định nghĩa các thực thể, chúng ta cần thêm các thư viện cần thiết. Tham khảo Hướng dẫn sử dụng Spring Boot với Hibernate để thực hiện các thao tác cần thiết.

    Định nghĩa Hibernate entity

    Bây giờ chúng ta đã có thể định nghĩa các thực thể Hibernate.

    /**
     * <code>account</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @AllArgsConstructor
    @NoArgsConstructor
    @Entity(name = "account")
    @ToString(onlyExplicitlyIncluded = true)
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class Account {
    
      /** <code>id</code>. */
      @Id
      @ToString.Include
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>username</code>. */
      @ToString.Include private String username;
    }
    
    /**
     * <code>customer</code>.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    @Entity(name = "customer")
    @ToString(onlyExplicitlyIncluded = true)
    @EqualsAndHashCode(onlyExplicitlyIncluded = true)
    public class Customer {
    
      /** <code>id</code>. */
      @Id
      @ToString.Include
      @EqualsAndHashCode.Include
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
    
      /** <code>first_name</code> */
      @ToString.Include private String firstName;
    
      /** <code>last_name</code>. */
      @ToString.Include private String lastName;
    
      /** <code>account_id</code>. */
      @OneToOne(optional = false, cascade = CascadeType.ALL)
      @JoinColumn(name = "ACCOUNT_ID", unique = true, nullable = false, updatable = false)
      private Account account;
    }
    

    Chúng ta sử dụng @Entity annotation để định nghĩa Hibernate entity. Tên bảng tương ứng với entity được định nghĩa thông quan thuộc tính name của @Entity annotation hoặc có thể sử dụng @Table(name = "account") để định nghĩa tên bảng. Chúng ta cùng xem xét một số annotation khác:

    • @Id annotation định nghĩa trường tưng ứng là khoá chính của entity.
    • @GeneratedValue annotation định nghĩa chiến lược sinh giá trị cho khoá chính, chúng ta sử dụng strategy = GenerationType.IDENTITY để xác định khoá chính sẽ được sinh tự động trong cơ sở dữ liệu (cột tương ứng trong cơ sở dữ liệu được đánh dấu là AUTO_INCREMENT).
    • @Column annotation định nghĩa tên cột tương ứng trong cơ sở dữ liệu.

    Triển khai ánh xạ one-to-one

    Phần chính mà chúng ta cần chú ý tới:

      @OneToOne(optional = false, cascade = CascadeType.ALL)
      @JoinColumn(name = "ACCOUNT_ID", unique = true, nullable = false, updatable = false)
      private Account account;
    

    Đối tượng Account được thêm vào bên trong class Customer và được đánh dấu với @OneToOne annotation để xác định đây ánh xạ one-to-one. Annotation cũng chưa thuộc tính cascade xác định chiến lược cascading. Cascading là một tính năng của Hibernate được sử dụng để quản lí trạng thái của thực thể đích mỗi khi trạng thái của thực thể cha thay đổi. Hibernate có các kiểu cascading sau:

    • CascadeType.ALL – lan truyền tất cả các thao tác từ thực thể cha sang thực thể đích.
    • CascadeType.PERSIST – lan truyền thao tác persist từ thực thể cha sang thực thể đích.
    • CascadeType.MERGE – lan truyền thao tác merge từ thực thể cha sang thực thể đích.
    • CascadeType.REMOVE – lan truyền thao tác remove từ thực thể cha sang thực thể đích.
    • CascadeType.REFRESH – lan truyền thao tác refresh từ thực thể cha sang thực thể đích.
    • CascadeType.DETACH – lan truyền thao tác detach từ thực thể cha sang thực thể đích.

    Ví dụ nếu cascade = CascadeType.REMOVE thì nếu thực thể cha bị xoá khởi cơ sở dữ liệu thì thực thể đích cũng bị xoá khỏi cơ sở dữ liệu. Trong trường hợp của chúng ta nếu thực thể Customer bị xoá khởi cơ sở dữ liệu thì thực thể liên quan Account cũng bị xoá khỏi cơ sở dữ liệu.

    @JoinColumn được xử dụng để xác định tên cột được sử dụng để tìm kiến thực thể đích. Thực thể Account sẽ được tìm kiến thông qua cột ACCOUNT_ID, nó chính xác là khoá ngoại của bảng customer mà chúng ta định nghĩa ở trên.

    Sử dụng Hibernate entity để lưu dữ liệu vào cơ sở dữ liệu

    Chúng ta cùng tạo một đoạn mã đơn giản để kiểm tra lại toàn bộ định nghĩa đã tạo ở trên:

    @Component
    public class Main implements CommandLineRunner {
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var account = Account.builder().username("hieunv").build();
        var customer = Customer.builder().firstName("Hieu").lastName("Nguyen").account(account).build();
        customerRepository.save(customer);
      }
    }
    

    Sau khi thực thi chương trình chúng ta sẽ thấy output sau:

    2023-02-06 20:28:55.358 DEBUG 78531 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-06 20:28:55.358 DEBUG 78531 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-06 20:28:55.378 DEBUG 78531 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-06 20:28:55.381 DEBUG 78531 --- [           main] org.hibernate.SQL                        : insert into account (username) values (?)
    Hibernate: insert into account (username) values (?)
    2023-02-06 20:28:55.383 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [VARCHAR] - [hieunv]
    2023-02-06 20:28:55.394 DEBUG 78531 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 1
    2023-02-06 20:28:55.394 DEBUG 78531 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-06 20:28:55.395 DEBUG 78531 --- [           main] org.hibernate.engine.spi.ActionQueue     : Executing identity-insert immediately
    2023-02-06 20:28:55.395 DEBUG 78531 --- [           main] org.hibernate.SQL                        : insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    Hibernate: insert into customer (account_id, first_name, last_name) values (?, ?, ?)
    2023-02-06 20:28:55.396 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2023-02-06 20:28:55.396 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [2] as [VARCHAR] - [Hieu]
    2023-02-06 20:28:55.396 TRACE 78531 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [3] as [VARCHAR] - [Nguyen]
    2023-02-06 20:28:55.400 DEBUG 78531 --- [           main] o.h.id.IdentifierGeneratorHelper         : Natively generated identity: 1
    2023-02-06 20:28:55.400 DEBUG 78531 --- [           main] o.h.r.j.i.ResourceRegistryStandardImpl   : HHH000387: ResultSet's statement was not registered
    2023-02-06 20:28:55.400 DEBUG 78531 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-06 20:28:55.401 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-06 20:28:55.401 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-06 20:28:55.403 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 0 deletions to 2 objects
    2023-02-06 20:28:55.403 DEBUG 78531 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-06 20:28:55.403 DEBUG 78531 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-06 20:28:55.404 DEBUG 78531 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=1, username=hieunv}
    2023-02-06 20:28:55.404 DEBUG 78531 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=1, account=app.demo.entity.Account#1}
    

    Kiểm tra trong cơ sở dữ liệu chúng ta sẽ thấy các bản ghi sau đã được insert vào trong database.

    Xoá dữ liệu cascading

    Tiếp theo chúng ta cùng xem một đoạn mã để kiểm chứng cơ chế hoạt động cascading. Chúng ta sẽ thử xoá thực thể Customer để xem thực thể Account tương ứng sẽ được xử lí như thế nào. Chúng ta cùng xem đoạn mã sau:

    @Component
    public class Main implements CommandLineRunner {
      @Autowired private CustomerRepository customerRepository;
    
      @Override
      @Transactional
      public void run(String... args) throws Exception {
        var customer = customerRepository.findById(1L).orElseThrow(() -> new EntityNotFoundException());
        customerRepository.delete(customer);
      }
    }
    

    Sau khi chạy chương trình chúng ta sẽ thấy output như sau:

    2023-02-07 09:45:07.300 DEBUG 83225 --- [           main] o.h.e.t.internal.TransactionImpl         : On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
    2023-02-07 09:45:07.300 DEBUG 83225 --- [           main] o.h.e.t.internal.TransactionImpl         : begin
    2023-02-07 09:45:07.317 DEBUG 83225 --- [           main] org.hibernate.SQL                        : select customer0_.id as id1_1_0_, customer0_.account_id as account_4_1_0_, customer0_.first_name as first_na2_1_0_, customer0_.last_name as last_nam3_1_0_, account1_.id as id1_0_1_, account1_.username as username2_0_1_ from customer customer0_ inner join account account1_ on customer0_.account_id=account1_.id where customer0_.id=?
    Hibernate: select customer0_.id as id1_1_0_, customer0_.account_id as account_4_1_0_, customer0_.first_name as first_na2_1_0_, customer0_.last_name as last_nam3_1_0_, account1_.id as id1_0_1_, account1_.username as username2_0_1_ from customer customer0_ inner join account account1_ on customer0_.account_id=account1_.id where customer0_.id=?
    2023-02-07 09:45:07.319 TRACE 83225 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2023-02-07 09:45:07.324 DEBUG 83225 --- [           main] l.p.e.p.i.EntityReferenceInitializerImpl : On call to EntityIdentifierReaderImpl#resolve, EntityKey was already known; should only happen on root returns with an optional identifier specified
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Customer#1]
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `account` : value = 1
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`account`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.328 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `firstName` : value = Hieu
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`firstName`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `lastName` : value = Nguyen
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`lastName`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Customer#1]
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Resolving attributes for [app.demo.entity.Account#1]
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Processing attribute `username` : value = hieunv
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Attribute (`username`)  - enhanced for lazy-loading? - false
    2023-02-07 09:45:07.329 DEBUG 83225 --- [           main] o.h.engine.internal.TwoPhaseLoad         : Done materializing entity [app.demo.entity.Account#1]
    2023-02-07 09:45:07.330 DEBUG 83225 --- [           main] .l.e.p.AbstractLoadPlanBasedEntityLoader : Done entity load : app.demo.entity.Customer#1
    2023-02-07 09:45:07.334 DEBUG 83225 --- [           main] o.h.e.t.internal.TransactionImpl         : committing
    2023-02-07 09:45:07.334 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Processing flush-time cascades
    2023-02-07 09:45:07.334 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Dirty checking collections
    2023-02-07 09:45:07.335 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 insertions, 0 updates, 2 deletions to 2 objects
    2023-02-07 09:45:07.335 DEBUG 83225 --- [           main] o.h.e.i.AbstractFlushingEventListener    : Flushed: 0 (re)creations, 0 updates, 0 removals to 0 collections
    2023-02-07 09:45:07.335 DEBUG 83225 --- [           main] o.hibernate.internal.util.EntityPrinter  : Listing entities:
    2023-02-07 09:45:07.336 DEBUG 83225 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Customer{firstName=Hieu, lastName=Nguyen, id=1, account=app.demo.entity.Account#1}
    2023-02-07 09:45:07.336 DEBUG 83225 --- [           main] o.hibernate.internal.util.EntityPrinter  : app.demo.entity.Account{id=1, username=hieunv}
    2023-02-07 09:45:07.341 DEBUG 83225 --- [           main] org.hibernate.SQL                        : delete from customer where id=?
    Hibernate: delete from customer where id=?
    2023-02-07 09:45:07.341 TRACE 83225 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    2023-02-07 09:45:07.346 DEBUG 83225 --- [           main] org.hibernate.SQL                        : delete from account where id=?
    Hibernate: delete from account where id=?
    2023-02-07 09:45:07.346 TRACE 83225 --- [           main] o.h.type.descriptor.sql.BasicBinder      : binding parameter [1] as [BIGINT] - [1]
    

    Chúng ta thấy rằng khi thực thể Customer bị xoá thì thực thể Account tương ứng được liên kết thông quan anh xạ one-to-one cũng bị xoá.

    Tổng kết

    Chúng ta đã tiến hành cài đặt ánh xạ one-to-one unidirectional. Thực thể Customer có thể truy cập vào thực thể Account nhưng chúng ta không thể thực hiện ngược lại. Trong thực tế thì chúng ta cần truy cập được thực thể Customer từ thực thể Account. Do đó chúng ta cần đến anh xạ one-to-one bidirectional. Chúng ta cùng xem xét trong bài viết tiếp theo nhé.

  • Hướng dẫn sử dụng Spring Boot với Hibernate

    Hướng dẫn sử dụng Spring Boot với Hibernate

    Trong bài viết này chúng ta sẽ tìm về cách sử dụng Spring Boot cùng với Hibernate. Chúng ta sẽ tạo một ứng dụng Spring Boot đơn giản để minh hoạ cách tích hợp Spring Boot với Hibernate.

    Khởi tạo ứng dụng Spring Boot

    Chúng ta sẽ sử dụng Spring Initializr để khởi tạo ứng dụng Spring Boot. Chúng ta cần thêm một số thư viện và cấu hình để tích hợp Hibernate, thêm các thư viện Web, JPA, MySQL. Bây giờ chúng ta cùng kiểm tra lại cấu trúc dự án đã được tạo ra và xác định các tệp cấu hình mà chúng ta sẽ cần.

    Cấu trúc dự án sẽ giống như sau:

    ├── HELP.md
    ├── README.md
    ├── mvnw
    ├── mvnw.cmd
    ├── pom.xml
    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   └── app
    │   │   │       └── demo
    │   │   │           └── DemoApplication.java
    │   │   └── resources
    │   │       ├── application.properties
    │   │       ├── db
    │   │       │   └── migration
    │   │       ├── static
    │   │       └── templates
    │   └── test
    │       ├── java
    │       │   └── app
    │       │       └── demo
    │       │           └── DemoApplicationTests.java
    │       └── resources
    

    MySQL

    Các bạn tham khảo bài viết HƯỚNG DẪN MIGRATE CƠ SỞ DỮ LIỆU SỬ DỤNG FLYWAY TRONG ỨNG DỤNG SPRING BOOT để có thể tạo một cơ sở dữ liệu và cách tạo database schema cho ứng dụng của bạn.

    Thư viện Maven

    Khi chúng ta mở tệp pom.xml, chúng ta sẽ thấy các thư viện maven spring-boot-starter-webspring-boot-starter-test.

        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-test</artifactId>
          <scope>test</scope>
        </dependency>
    

    Đây là hai thư viện cần thiết khi chúng ta bắt đầu một dự án với Spring Boot.

    Chúng ta cũng sẽ nhìn thấy thư viên JPA trong phần các thư viện phụ thuộc.

        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    

    Thư viện này bao gồm các thư viện phụ thuộc JPA API, JPA implementation, JDBC và các thư viện cần thiết khác. Mặc định triển khai JPA implementation được sử dụng là Hibernate.

    Tiếp theo chúng ta cũng sẽ thấy thư viện để tích hợp với MySQL.

        <dependency>
          <groupId>mysql</groupId>
          <artifactId>mysql-connector-java</artifactId>
          <scope>runtime</scope>
        </dependency>
    

    Cấu hình datasource

    Mặc định Spring Boot sử dụng cấu hình datasource được cấu hình trong application.properties hoặc nếu bạn đang sử dụng application.yml datasource được cấu hình như sau:

      datasource:
        url: jdbc:mysql://localhost:3306/demo?createDatabaseIfNotExist=true
        schemas: "demo"
        username: root
        password: demo@123
        driverClassName: com.mysql.cj.jdbc.Driver
    

    Tạo và sử dụng Entity

    Chúng ta tạo package entity và định nghĩa JPA entity:

    /**
     * Product.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Entity
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Product {
    
      @Id
      @GeneratedValue(generator = "Product")
      @TableGenerator(name = "Product", table = "hibernate_sequence")
      private Long id;
    
      private String name;
    
      public static Product of(ProductCreateRequest request) {
        return Product.builder().name(request.getName()).build();
      }
    }
    

    Sau đó chúng ta tạo ProductRepository trong package repository:

    /**
     * ProductRepository.
     *
     * @author Hieu Nguyen
     */
    @Repository
    public interface ProductRepository extends JpaRepository<Product, Long> {
    }
    

    Ở đây chúng ta sử dụng JpaRepository đã được cài đặt trong Spring Data JPA. Các bạn có thể tham khảo các phương thức của JpaRepository trong tài liệu này. Trong ví dụ dưới đây chúng ta sẽ thử với phương thức findById.

    Chúng ta cũng tạo ProductRepositoryTest để test ProductRepository:

    @SpringBootTest
    class ProductRepositoryTest {
    
      @Autowired private ProductRepository productRepository;
    
      @Test
      void test() {
        Product product =
            productRepository.findById(1L).orElseThrow(EntityNotFoundException::new);
        assertThat(product.getId(), is(1L));
      }
    }
    

    Sau khi chạy thử test này chúng ta nhận được output log như sau:

    Hibernate: select product0_.id as id1_1_0_, product0_.name as name2_1_0_ from product product0_ where product0_.id=?
    

    Mặc định Hibernate sẽ query sử dụng tên bảng là tên entity ở dạng chữ viết thường.

    Xác định tên bảng

    Trong ví dụ của chúng ta đang sử dụng MySQL với --lower_case_table_names=1 nên sẽ không có vấn đề xảy ra. Trong trường hợp bạn sử dụng database cần phân biệt tên bảng viết thường và viết hoa hoặc trong trường hợp tên bảng và tên entity không giống nhau. Khi đó chúng ta cần xác định tên bảng tương ứng với entity như sau:

    /**
     * Product.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Entity(name = "PRODUCT")
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Product {
      ...
    }
    

    Tổng kết

    Trong bài viết này chúng ta đã cũng tìm hiểu cách tìm hợp Hibernate vào ứng dụng Spring Boot. Chúng ta cũng đã test thử mã nguồn với database MySQL.

  • Hướng dẫn migrate cơ sở dữ liệu sử dụng Flyway trong ứng dụng Spring Boot

    Hướng dẫn migrate cơ sở dữ liệu sử dụng Flyway trong ứng dụng Spring Boot

    Với hầu hết các dự án việc quản lý các phiên bản database schema là vô cùng quan trọng. Phương pháp thực hiện migrate database tốt sẽ giúp tất cả thành viên dự án dễ dàng đồng bộ môi trường phát triển cũng như là triển khai database schema lên các môi trường khác nhau. Trong bài viết này chúng ta sẽ cùng tìm hiểu phương pháp migrate database schema sử dụng Flyway trong ứng dụng Spring Boot.

    Khởi tạo ứng dụng Spring Boot

    Chúng ta sẽ sử dụng Spring Initializr để khởi tạo ứng dụng Spring Boot. Cấu trúc dự án sẽ giống như sau:

    ├── HELP.md
    ├── README.md
    ├── mvnw
    ├── mvnw.cmd
    ├── pom.xml
    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   └── app
    │   │   │       └── demo
    │   │   │           └── DemoApplication.java
    │   │   └── resources
    │   │       ├── application.properties
    │   │       ├── db
    │   │       │   └── migration
    │   │       │       └── V20221124103000__Initial.sql
    │   │       ├── static
    │   │       └── templates
    │   └── test
    │       ├── java
    │       │   └── app
    │       │       └── demo
    │       │           └── DemoApplicationTests.java
    │       └── resources
    

    MySQL

    Chúng ta sẽ sử dụng docker để container chạy MySQL.

    docker run --name demo -e MYSQL_ROOT_PASSWORD=demo@123 -p 3306:3306 -d mysql --lower_case_table_names=1
    

    Chúng ta có thể sử dung MySQL Workbench để kiểm tra database MySQL đã chạy hay chưa.

    Thư viện Maven

    Để sử dụng Flyway chúng ta thêm thư viện phụ thuộc sau:

        <dependency>
          <groupId>org.flywaydb</groupId>
          <artifactId>flyway-mysql</artifactId>
        </dependency>
    

    Trong bài viết này chúng ta sẽ dùng Maven plugin để thực hiện migrate database, chúng ta cần cấu hình plugin này trong pom.xml như sau:

          <plugin>
            <groupId>org.flywaydb</groupId>
            <artifactId>flyway-maven-plugin</artifactId>
            <configuration>
              <url>jdbc:mysql://localhost:3306/demo?createDatabaseIfNotExist=true</url>
              <user>root</user>
              <password>demo@123</password>
            </configuration>
          </plugin>
    

    Định nghĩa phiên bản migration

    Mặc định Flyway sẽ đọc các tệp SQL trong thư mục resources/db/migration. Tên tệp được định nghĩa theo tài liệu Versioned Migrations.

    Mỗi phiên bản migration có phiên bản, thông tin mô tả phiên bản và checksum. Các phiên bản phải là duy nhất. Thông tin phiên bản là thông tin thuần khiết gợi nhớ cái gì được thực hiện trong phiên bản đó. Checksum được sử dụng để phát hiện các thay đổi ngẫu nhiên. Các phiên bản migration được áp dụng theo một thứ tự nhất định. Các định dạng phiên bản có thể sử dụng như sau:

    • 1
    • 001
    • 5.2
    • 1.2.3.4.5.6.7.8.9
    • 205.68
    • 20130115113556
    • 2013.1.15.11.35.56
    • 2013.01.15.11.35.56

    Trong nội dung bài viết này chúng ta định nghĩa một phiên bản V20221124103000__Initial.sql với nội dung như sau:

    CREATE TABLE IF NOT EXISTS hibernate_sequence (
      sequence_name VARCHAR(128) NOT NULL,
      next_val INT NOT NULL
    );
    
    DROP TABLE IF EXISTS PRODUCT;
    
    CREATE TABLE PRODUCT (
      ID BIGINT PRIMARY KEY AUTO_INCREMENT
      , NAME VARCHAR(255)
      , CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP
      , UPDATED_AT DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
      , DELETED_AT DATETIME DEFAULT NULL
      , CONSTRAINT uk_product_name UNIQUE(name)
    );
    
    DROP TABLE IF EXISTS `ORDER`;
    
    CREATE TABLE `ORDER` (
      ID BIGINT PRIMARY KEY AUTO_INCREMENT
      , NAME VARCHAR(255)
      , CREATED_AT DATETIME DEFAULT CURRENT_TIMESTAMP
      , UPDATED_AT DATETIME DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP
      , DELETED_AT DATETIME DEFAULT NULL
      , CONSTRAINT uk_order_name UNIQUE(NAME)
    );
    

    Thực thi migration

    Để tiến hành migrate database schema chúng ta thực thi lệnh ./mvnw flyway:migrate.

    ➜  demo git:(main) ✗ ./mvnw flyway:migrate
    [INFO] Scanning for projects...
    [INFO] 
    [INFO] ------------------------------< app:demo >------------------------------
    [INFO] Building demo 0.0.1-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [WARNING] The artifact mysql:mysql-connector-java:jar:8.0.31 has been relocated to com.mysql:mysql-connector-j:jar:8.0.31: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
    [INFO] 
    [INFO] --- flyway-maven-plugin:8.5.13:migrate (default-cli) @ demo ---
    [INFO] Flyway Community Edition 8.5.13 by Redgate
    [INFO] See what's new here: https://flywaydb.org/documentation/learnmore/releaseNotes#8.5.13
    [INFO] 
    [INFO] Database: jdbc:mysql://localhost:3306/demo (MySQL 8.0)
    [INFO] Successfully validated 1 migration (execution time 00:00.013s)
    [INFO] Creating Schema History table `demo`.`flyway_schema_history` ...
    [INFO] Current version of schema `demo`: << Empty Schema >>
    [INFO] Migrating schema `demo` to version "20221124103000 - Initial"
    [WARNING] DB: Unknown table 'demo.product' (SQL State: 42S02 - Error Code: 1051)
    [WARNING] DB: Unknown table 'demo.order' (SQL State: 42S02 - Error Code: 1051)
    [INFO] Successfully applied 1 migration to schema `demo`, now at version v20221124103000 (execution time 00:00.100s)
    [INFO] Flyway Community Edition 8.5.13 by Redgate
    [INFO] See what's new here: https://flywaydb.org/documentation/learnmore/releaseNotes#8.5.13
    [INFO] 
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  1.044 s
    [INFO] Finished at: 2023-01-30T14:41:40+07:00
    [INFO] ------------------------------------------------------------------------
    

    Trong trường hợp thực thi migrate có lỗi. Chúng ta có thể thực hiện lệnh ./mvnw flyway:repair trước khi thực thi lại lệnh migrate.

    ➜  demo git:(main) ✗ ./mvnw flyway:repair
    [INFO] Scanning for projects...
    [INFO] 
    [INFO] ------------------------------< app:demo >------------------------------
    [INFO] Building demo 0.0.1-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [WARNING] The artifact mysql:mysql-connector-java:jar:8.0.31 has been relocated to com.mysql:mysql-connector-j:jar:8.0.31: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
    [INFO] 
    [INFO] --- flyway-maven-plugin:8.5.13:repair (default-cli) @ demo ---
    [INFO] Flyway Community Edition 8.5.13 by Redgate
    [INFO] See what's new here: https://flywaydb.org/documentation/learnmore/releaseNotes#8.5.13
    [INFO] 
    [INFO] Database: jdbc:mysql://localhost:3306/demo (MySQL 8.0)
    [INFO] Repair of failed migration in Schema History table `demo`.`flyway_schema_history` not necessary. No failed migration detected.
    [INFO] Successfully repaired schema history table `demo`.`flyway_schema_history` (execution time 00:00.030s).
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  0.908 s
    [INFO] Finished at: 2023-01-30T14:44:31+07:00
    [INFO] ------------------------------------------------------------------------
    

    Để tiến thành clean database schema chúng ta thực thi lệnh ./mvnw flyway:clean.

    ➜  demo git:(main) ✗ ./mvnw flyway:clean
    [INFO] Scanning for projects...
    [INFO] 
    [INFO] ------------------------------< app:demo >------------------------------
    [INFO] Building demo 0.0.1-SNAPSHOT
    [INFO] --------------------------------[ jar ]---------------------------------
    [WARNING] The artifact mysql:mysql-connector-java:jar:8.0.31 has been relocated to com.mysql:mysql-connector-j:jar:8.0.31: MySQL Connector/J artifacts moved to reverse-DNS compliant Maven 2+ coordinates.
    [INFO] 
    [INFO] --- flyway-maven-plugin:8.5.13:clean (default-cli) @ demo ---
    [INFO] Flyway Community Edition 8.5.13 by Redgate
    [INFO] See what's new here: https://flywaydb.org/documentation/learnmore/releaseNotes#8.5.13
    [INFO] 
    [INFO] Database: jdbc:mysql://localhost:3306/demo (MySQL 8.0)
    [INFO] Successfully dropped pre-schema database level objects (execution time 00:00.002s)
    [INFO] Successfully cleaned schema `demo` (execution time 00:00.009s)
    [INFO] Successfully cleaned schema `demo` (execution time 00:00.008s)
    [INFO] Successfully dropped post-schema database level objects (execution time 00:00.002s)
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  0.861 s
    [INFO] Finished at: 2023-01-30T14:39:23+07:00
    [INFO] ------------------------------------------------------------------------
    

    Tổng kết

    Trong bài viết này chúng ta đã tìm hiểu cách tạo một instance MySQL sử dụng docker và tạo và quản lý các phiên bản database schema sử dụng Flyway.

  • Mô đun hoá theo tầng hay mô đun hoá theo tính năng?

    Mô đun hoá theo tầng hay mô đun hoá theo tính năng?

    Mô đun hoá là quá trình tách một hệ thống phần mềm thành nhiều mô đun. Ngoài việc giảm độ phức tạp, nó làm tăng tính dễ hiểu, khả năng bảo trì và khả năng sử dụng lại của hệ thống. Trong bài viết này sẽ đề cập đến hai phương pháp mô đun hoá (theo tầng và theo tính năng). Chúng ta nên chọn phương pháp nào và tại sao?

    Trước khi đến với nội dung chính chúng ta cùng xem một số nội dung liên quan:

    1. KIẾN TRÚC PHÂN TẦNG (LAYERED ARCHITECTURE) (PHẦN 1)
    2. KIẾN TRÚC PHÂN TẦNG (LAYERED ARCHITECTURE) (PHẦN 2)
    3. ÁP DỤNG KIẾN TRÚC PHÂN TẦNG TRONG ỨNG DỤNG SPRING BOOT

    Mô đun hoá theo tầng

    Khi áp dụng kiến trúc phân tầng vào các dự án kiểu này, các class được đặt trong các package dựa theo tầng trong kiến trúc phân tầng mà chúng thuộc về. Phương pháp này làm giảm tính gắn kết (low cohesion) giữa các class bên trong các package bởi vì trong cùng một package có chứa các class không liên quan chặt chẽ với nhau. Dưới dây là một ví dụ áp dụng phương pháp mô đun hoá theo tầng.

    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   └── app
    │   │   │       └── demo
    │   │   │           ├── DemoApplication.java
    │   │   │           ├── controller
    │   │   │           │   ├── OrderController.java
    │   │   │           │   └── ProductController.java
    │   │   │           ├── entity
    │   │   │           │   ├── Order.java
    │   │   │           │   └── Product.java
    │   │   │           ├── repository
    │   │   │           │   ├── OrderRepository.java
    │   │   │           │   └── ProductRepository.java
    │   │   │           ├── request
    │   │   │           │   ├── OrderCreateRequest.java
    │   │   │           │   └── ProductCreateRequest.java
    │   │   │           ├── response
    │   │   │           │   ├── OrderCreateResponse.java
    │   │   │           │   └── ProductCreateResponse.java
    │   │   │           └── service
    │   │   │               ├── OrderService.java
    │   │   │               ├── OrderServiceImpl.java
    │   │   │               ├── ProductService.java
    │   │   │               └── ProductServiceImpl.java
    

    Ngoài ra khi kiểm tra cấu trúc của các dự án như trên chúng ta thấy rằng giữa các package có liên kết chặt chẽ với nhau (high coupling). Bởi vì các class ở tầng Repository được sử dụng trong các class ở tầng Service và các class ở tầng Service được sử dụng trong các class ở tầng Controller. Hơn nữa, mỗi khi có yêu cầu thay đổi chúng ta cần phải thay đổi ở nhiều package khác nhau.

    Để có thể giúp một việc nào đó, chúng ta cần phải biết mọi thứ.

    CohesionCoupling nghĩa là gì?

    • Cohesion: Cohesion đề cập đến mức độ quan hệ logic giữa các class trong cùng package với nhau. High-cohesion giữa các class đảm bảo tính độc lập của package. Low-cohesion không chỉ giảm tính độc lập mà còn giảm đáng kể khả năng sử dụng lại và tính dễ hiểu.
    • Coupling: Coupling đề cập đến mức độ phụ thuộc lẫn nhau giữa các package/class. Low-coupling làm tăng đáng kể khả năng bảo trì. Bởi vì những thay đổi được thực hiện bên trong class do yêu cầu thay đổi không ảnh hưởng đến các class khác, không có tác dụng phụ và việc bảo trì dễ dàng hơn.

    High-cohesion bên trong các packagelow-coupling giữa các package là thiết yếu đối với một hệ thống được thiết kế tốt. Một thiết kế tốt làm tăng đáng kể tính bền vững của hệ thống. Vậy thì làm thế để đạt được điều đó?

    Mô đun hoá theo tính năng

    Dưới đây là một ví dụ áp dụng phương pháp mô đun hoá theo tính năng.

    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   └── app
    │   │   │       └── demo
    │   │   │           ├── DemoApplication.java
    │   │   │           ├── domain
    │   │   │           │   ├── order
    │   │   │           │   │   └── create
    │   │   │           │   │       ├── OrderCreateController.java
    │   │   │           │   │       ├── OrderCreateRequest.java
    │   │   │           │   │       ├── OrderCreateResponse.java
    │   │   │           │   │       ├── OrderCreateService.java
    │   │   │           │   │       └── OrderCreateServiceImpl.java
    │   │   │           │   └── product
    │   │   │           │       └── create
    │   │   │           │           ├── ProductCreateController.java
    │   │   │           │           ├── ProductCreateRequest.java
    │   │   │           │           ├── ProductCreateResponse.java
    │   │   │           │           ├── ProductCreateService.java
    │   │   │           │           └── ProductCreateServiceImpl.java
    │   │   │           ├── entity
    │   │   │           │   ├── Order.java
    │   │   │           │   └── Product.java
    │   │   │           └── repository
    │   │   │               ├── OrderRepository.java
    │   │   │               └── ProductRepository.java
    

    Trong cấu trúc dự án kiểu này, các package chứa tất cả các class được yêu cầu bởi một tính năng. Tính độc lập của package dượcd đảm bảo bằng cách đặt các class có liên quan chặt chẽ trong cùng một package.

    Việc sử dụng một class bởi một class trong gói khác được loại bỏ ở cấu trúc này. Ngoài ra, các class trong cùng một package có liên quan chặt chẽ với nhau. Vì vậy high-cohesion trong cùng một packagelow-coupling giữa các package được đảm bảo bởi cấu trúc này.

    Hơn nữa, cấu trúc này làm tăng tính mô đun hoá. Giả sử rằng chúng ta có thêm 10 domain (ngoài ProductOrder). Với phương pháp mô đun hoá theo tầng, các class sẽ được đặt trong các package controller, service, repository. Vì vậy toàn bộ ứng dụng sẽ bao gồm 3 package (ngoại trừ các class tiện ích), các package sẽ có số lượng lớn class. Tuy nhiên, trong phương pháp mô đun hoá theo tính năng, cùng ứng dụng đó sẽ bảo gồm 12 package tương ứng với 12 domain, tính mô đun hoá đã được tăng lên.

    Trong ví dụ trên chúng ta thấy có 2 ngoại lệ, repositoryentity package không được cấu trúc theo tính năng như bình thường. Với các entity và các repository được sử dụng ở nhiều service khác nhau, do chúng không là bắt buộc ở một tính năng cụ thể nào nên chúng ta cấu trúc chúng theo phương pháp mô đun theo tầng như bình thường. Với những entityrepository chỉ được sử dụng ở một tính năng cụ thể nào đó, chúng ta vẫn cấu trúc chúng theo phương pháp mô đun hoá theo tính năng như bình thường.

    Nếu một tính năng có thể được xoá bởi chỉ một hành động, ứng dụng đó có tính mô đun hoá cao nhất.

    Lợi ích của việc mô đun hoá theo tính năng

    • Mô đun hoá theo tính năng tạo ra các packagehigh-cohesion, low-coupling và tính mô đun hoá cao.
    • Mô đun hoá theo tính năng cho phép các class được khai báo với thuộc tính truy cập là private thay vì public, đo đó tăng tính đóng gói. Mặt khác mô đun hoá theo tầng buộc chúng ta phải đặt gần như toàn bộ các classpublic.
    • Mô đun hoá theo tính năng giúp giảm việc phải điều hướng giữa các package bởi vì các class cần thiết cho một tính năng được đặt trong cùng một package.
    • Mô đun hoá theo tính năng giống như kiến trúc microservice. Mỗi package được giới hạn bởi các class liên quan với một tính năng cụ thể. Mặt khác, mô đun hoá theo tầng giống như kiến trúc nguyên khối. Khi một ứng dụng tăng kích thước, số class trong mỗi package sẽ tăng lên không giới hạn.

    Tổng kết

    Martin Fowler gợi ý bắt đầu một dự án mới với kiến trúc microservice có thể không phải là một ý kiến hay. Nếu ứng dụng của chúng ta đạt mức tăng trưởng lớn và giới hạn của nó là chắc chắn, thì bạn nên chuyển sang kiến trúc microservice.

    Hãy tưởng tượng tình huống trên, chúng ta đã quyết định tách các microservice từ ứng dụng nguyên khối. Giả sử rằng microservice sử dụng phương pháp mô đu hoá theo tính năng, vậy cấu trúc nào sẽ dễ dàng chuyển sang kiến trúc microservice hơn?

    Câu trả lời cho câu hỏi này và các ưu điểm khác giúp chúng ta biết nên sử dụng phương pháp mô đun hoá nào.

  • Áp dụng kiến trúc phân tầng trong ứng dụng Spring Boot

    Áp dụng kiến trúc phân tầng trong ứng dụng Spring Boot

    Trong bài viết này chúng ta sẽ cùng tìm hiểu kiến trúc phân tầng được ứng dụng như thế nào trong ứng dụng Spring Boot.

    Chúng ta nên sử dụng bao nhiêu tầng?

    Trong kiến trúc phân tầng chúng ta không bị hạn chế về số tầng. Tuy nhiên, các dự án trong thực tế triển khai thường sử dụng 4 tầng. Các ứng dụng Spring Boot cũng có thể triển khai kiến trúc phân tầng với 4 tầng như sau:

    • Tầng Controller là triển khai của tầng Presentation.
    • Tầng Service là triển khai của tầng Business.
    • Tầng Repository là triển khai của tầng Persistence.
    • Tầng Database không được phản ánh trong mã nguồn của ứng dụng.

    Do một ứng dụng có hoặc không cần sử dụng tới database. Khi đó có thể tầng Repository cũng không tồn tại. Trong thực tế chúng ta thường thấy các Entity được định nghĩa trong mã nguồn. Về mặt lí thuyết thì các Entity thuộc về tầng Persistence. Tuy nhiên các Entity chính là phản ánh của các bảng trong database. Do đó chúng ta có thể xem các Entity như là thể hiện của tầng Database.

    Kiến trúc phân tầng trong ứng dụng Spring Boot

    Dưới đây là một ví dụ triển khai của kiến trúc phân tầng với ứng dụng Spring Boot:

    ├── src
    │   ├── main
    │   │   ├── java
    │   │   │   └── app
    │   │   │       └── demo
    │   │   │           ├── DemoApplication.java
    │   │   │           ├── controller
    │   │   │           │   └── ProductController.java
    │   │   │           ├── entity
    │   │   │           │   └── Product.java
    │   │   │           ├── repository
    │   │   │           │   └── ProductRepository.java
    │   │   │           ├── request
    │   │   │           │   └── ProductCreateRequest.java
    │   │   │           ├── response
    │   │   │           │   └── ProductCreateResponse.java
    │   │   │           └── service
    │   │   │               ├── ProductService.java
    │   │   │               └── ProductServiceImpl.java
    

    Mỗi một request từ client sẽ lần lượt đi qua các tầng Controller, Service, Repository và kết thúc ở tầng Database. Trong ví dụ trên, bảng Product trong database sẽ được ánh xạ tương ứng với Product entity. Các thao tác tương tác với bảng Product sẽ được triển khai ở ProductRepository. Logic nghiệp vụ liên quan tới Product sẽ được cài đặt trong ProductService. ProductController sẽ là nơi tiếp nhận yêu cầu từ phía client.

    Các yêu cầu từ phía client sẽ được tiếp nhận ở ProductController. Sau đó các thông tin nhận được sẽ được truyền tới ProductService. Tại đây các logic nghiệp vụ liên qua sẽ được xử lí trước khi được truyền tới ProductRepository. Cuối cùng dữ liệu sẽ được lưu trữ trong bảng Product của có sở dữ liệu.

    Tầng Controller

    /**
     * ProductController.
     *
     * @author Hieu Nguyen
     */
    @RestController
    @RequiredArgsConstructor
    @RequestMapping("/products")
    public class ProductController {
      private final ProductService productService;
    
      @PostMapping
      public ResponseEntity<ProductCreateResponse> create(@RequestBody ProductCreateRequest request) {
        return ResponseEntity.ok().body(ProductCreateResponse.of(productService.create(request)));
      }
    }
    

    ProductController nhận dữ liệu từ client trong request body thông qua ProductCreateRequest. Sau đó nó truyền dữ liệu ProductCreateRequest xuống cho ProductService. Kết quả trả lại từ ProductService được chuyển thành ProductCreateResponse để trả lại client trong response body. ProductCreateRequestProductCreateResponse được cài đặt như sau:

    /**
     * ProductCreateRequest.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class ProductCreateRequest {
      private String name;
    
    }
    
    /**
     * ProductCreateResponse.
     */
    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class ProductCreateResponse {
      private Integer id;
    
      private String name;
    
      public static ProductCreateResponse of(Product product) {
        return ProductCreateResponse.builder().id(product.getId()).name(product.getName()).build();
      }
    }
    

    Tầng Service

    /**
     * ProductRepository.
     *
     * @author Hieu Nguyen
     */
    @Service
    @RequiredArgsConstructor
    public class ProductServiceImpl implements ProductService {
      private final ProductRepository productRepository;
    
      @Override
      @Transactional
      public Product create(ProductCreateRequest request) {
        return productRepository.save(Product.of(request));
      }
    }
    

    Tại tầng Service, ProductService nhận dữ liệu thông qua ProductCreateRequest được truyền xuống từ tầng Controller. Nó chuyển dữ liệu ProductCreateRequest vào Product entity, sau đó truyền dữ liệu Product entity xuống ProductRepository và cuối cùng dữ liệu được insert vào database.

    Tầng Repository

    Trong ví dụ này chúng ta sử dụng Spring Data JPAHibernate để cài đặt tầng Repository.

    /**
     * ProductRepository.
     *
     * @author Hieu Nguyen
     */
    @Repository
    public interface ProductRepository extends JpaRepository<Product, Integer> {}
    

    Chúng ta ánh xạ bảng Product vào Product entity như sau:

    /**
     * Product.
     *
     * @author Hieu Nguyen
     */
    @Data
    @Entity
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public class Product {
    
      @Id
      @GeneratedValue(generator = "Product")
      @TableGenerator(name = "Product", table = "hibernate_sequence")
      private Integer id;
    
      private String name;
    
      public static Product of(ProductCreateRequest request) {
        return Product.builder().name(request.getName()).build();
      }
    }
    

    Sử dụng tầng đóng hay mở

    Tất cả các tầng nên là đóng, nghĩa là với bất kì một tính năng nào chúng ta cần triển khai đủ 4 tầng: Controller, Service, Repository, Database. Tuy nhiên, trong qua trình sử dùng kiến trúc phân tầng sẽ có nhiều bạn đặt câu hỏi liệu có thực sự cần đến tầng Service không? Trên thực tế có nhiếu tính năng chúng ta sẽ không cần cài đặt mã nguồn ở tầng Service. Khi đó tẩng Service chỉ làm nhiệm vụ chuyển tiếp dữ liệu từ tầng Controller xuống tầng Repository. Tuy nhiên để đảm bảo không có những sai phạm không đáng có như việc cài đặt các logic nghiệp vụ ở tầng Controller sau đó gọi trực tiếp tới tầng Repository thì chúng ta nên triển khai tất cả các tầng đóng. Khi đó thì dù có hay không có logic nghiệp vụ ở tầng Service chúng ta vẫn nên triển khai tầng này.

    Tổng kết

    Trong bài viết này chúng ta đã cùng tìm hiểu cách triển khai kiến trúc phân tầng trong một ứng dụng Spring Boot. Việc triển khai thực sự không khó, tuy nhiên để đảm bảo việc triển khai được thống nhất thì thực sự rất khó. Bài viết này hi vọng rằng có thể đem lại cái nhìn thống nhất giữa tất các các thành viên trong một dự án. Khi đó việc áp dụng kiến trúc phân tầng sẽ có hiệu quả hơn.

  • Kiến trúc phân tầng (Layered Architecture) (Phần 2)

    Kiến trúc phân tầng (Layered Architecture) (Phần 2)

    Trong phần 1 chúng ta đã tìm hiều về Kiến trúc phân tầng và các khái niệm quan trọng nhất của nó. Trong phần 2 này chúng ta sẽ xem xét cách thức hoạt động của kiến trúc phân tầng và những điểm cần lưu ý khi sử dụng kiến trúc này.

    Ví dụ

    Để minh họa cách thức hoạt động của kiến trúc phân lớp, chúng ta cùng xem xét một yêu cầu từ người dùng doanh nghiệp muốn truy xuất thông tin khách hàng của một cá nhân cụ thể.

    Các mũi tên màu đen thể hiện luồng yêu cầu xuống cơ sở dữ liệu để lấy thông tin khách hàng. Các mũi tên màu đỏ thể hiện luồng phản hồi ngược trở lại màn hình để hiển thị dữ liệu. Trong ví dụ này, thông tin khách hàng bao gồm cả dữ liệu khách hàng và dữ liệu đơn hàng (đơn hàng do khách hàng đặt). Customer Screen có nhiệm vụ tiếp nhận yêu cầu và hiển thị thông tin khách hàng. Nó hoàn toàn không biết nơi dữ liệu được lư trữ, làm thế nào để lấy nó hoặc có bao nhiêu bảng cơ sở dữ liệu phải được truy vấn để lấy dữ liệu. Khi Customer Screen nhận được yêu cấu lấy thông tin khác hàng của một cá nhân cụ thể, nó sẽ chuyển tiếp yêu cầu đó tới mô đun Customer Delegate. Mô đun này có nhiệm vụ biết các mô đun ở tầng nghiệp vụ có thể xử lí yêu cầu đó cũng như làm thế nào để lấy các mô đun đó và dữ liệu nào mà nó cần (hợp đồng). Customer Object ở tầng nghiệp vụ có nhiệm vụ tổng hợp toàn bộ thông tin cần thiết bởi yêu cầu nghiệp vụ(trong trường hợp này là thông tin khách hàng). Mô đun này gọi ra Customer DAO (đối tượng truy cập dữ liệu) ở tầng lưu trữ để lấy dữ liệu khách hàng cùng với Order DAO để lấy thông tin đơn hàng. Các mô đun này lần lượt thực thi các câu lệnh SQL để lấy dữ liệu tương ứng và trả lại cho Customer Object ở tầng nghiệp vụ. Khi Customer Object nhận được dữ liệu, nó sẽ tổng hợp dữ liệu và trả lại các thông tin đó cho Customer Delegate, sau đó Customer Delegate trả lại các dữ liệu đó cho Customer Screen để hiển thị cho người dùng. Từ khía cạnh công nghệ, có hàng tá cách để cài đặt các mô đun này. Ví dụ với nên tàng Java, Customer Screen có thể là một màn hình JSF(Java Server Faces) cùng với Customer Delete là thành phần bean được quản lí. Customer Object ở tầng nghiệp vụ có thể là một Spring Bean cục bộ hoặc EJB3 bean từ xa. Các đối tượng truy cập cơ sở dữ liệu được minh hoạ trong ví dụ trước có thể được triển khai dưới dạng POJO (Plain Old Java Objects) đơn giản, MyBatis XML Mapper, hoặc ngay cả cá đối tượng đóng gọi lời gọi JDBC thuần hoặc các truy vấn Hibernate. Trên nền tảng Microsoft, Customer Screen có thể là một mô đun ASP(Active Server Pages) sử dụng framework .NET để truy cập các mô đun C# ở tầng nghiệp vụ với các mô đun truy cập dữ liệu khác hàng và đơn hàng được triển khai dưới dàng ADO(ActiveX Data Objects).

    Những điểm cần cân nhắc khi sử dụng kiến trúc phân tầng

    Kiến trúc phân tầng là một mẫu kiến trúc về cơ bản là vững chắc, hấu hết các ứng dụng có thể bắt đầu bằng kiến trúc này, đặc biệt khi chúng ta không chắc chắn kiến trúc nào là phù hợp nhất cho ứng dụng của chúng ta. Tuy nhiên về mặt kiến trúc thì có một vài điều cần cân nhắc trước khi chọn mẫu kiến trúc này.

    Điểm đầu tiên cần chú ý là các anti-pattern(phản mẫu – các mẫu thiết kế cần tránh sử dụng). Một trong số đó là kiến trúc hố sụt. Kiến trúc này mô tả tình huống luồng yêu cầu đi qua nhiều tầng của kiến trúc mà đơn giản xử lí chuyển tiếp với rất ít hoặc không có logic được thực hiện bên trong mỗi tầng. Ví dụ, tầng trình diễn phản hồi một yêu cầu từ người dùng muốn lấy dữ liệu khách hàng. Tầng trình diễn chuyển yêu cầu tới tầng nghiệp vụ, nó đơn giản chuyển tiếp yêu cầu tới tầng lưu trữ, sau đó tạo một lời gọi SQL đơn giản tới tầng cơ sở dữ liệu để lấy dữ liệu khách hàng. Dữ liệu sau đó được truyền theo đường ngược lại mà không có xử lí thêm hay logic tổng hợp, tính toán hoặc biến đổi dữ liệu.

    Mỗi kiến trúc phân tầng sẽ có ít nhất một số tình huống rơi vào phản mẫu kiến trúc hố sụt. Tuy nhiên điều quan trọng là phân tích tỉ lệ phần trăm yêu cầu thuộc loại này. Quy tắc 80-20 thường là một phương pháp hay, nên tuân theo để xác định liệu có hay không chúng ta đang rơi vào phản mẫu kiến trúc hố sụt. Thông thường có khoảng 20% yêu cầu được xử lí đơn giản và 80% yêu cầu có một số logic nghiệp vụ liên quan đến yêu cầu đó. Tuy nhiên nếu chúng ta thấy rằng tỉ lệ này bị đảo ngược và phần lớn các yêu cầu của chúng ta là quá trình xử lí đơn giản, chúng ta có thể cân nhắc một số tầng trong kiến trúc là mở.

    Một điểm khác cần cân nhắc với mẫu kiến trúc phân lớp là nó có xu hướng thích ứng với các ứng dụng nguyên khối, ngay cả khi chúng ta tách tầng trình diễn và tầng nghiệp vụ thành các đơn có thể triển khai riêng biệt. Mặc dù điều này có thể không phải là vấn đề đáng lo ngại đối với một số ứng dụng nhưng nó đặt ra một số vấn đề tiềm ẩn về triển khai, độ bền và độ tin cậy chung, hiệu suất và khả năng mở rộng.

    Phân tích kiến trúc phân tầng

    Dưới đây là bảng đánh giá và phân tích về các đặc điểm kiến trúc phổ biến của kiến trúc phân tầng. Các đánh giá cho từng đặc điểm dựa trên xu hướng tự nhiên của các đặc điểm đó, điển hình như là khả năng triển khai cũng như là mức độ phổ biến của mẫu kiến trúc này.

    Đặc điểm Đánh giá
    Tính linh hoạt tổng thể
    Dễ triển khai
    Khả năng kiểm thử
    Hiệu năng
    Khả năng mở rộng
    Dễ phát triển

    Tính linh hoạt tổng thể

    Tính linh hoạt tổng thể là khả năng đáp ứng nhanh chóng với một môi trường thay đổi liên tục. Trong khi thay đổi được cô lập thông qua tầng cô lập, nó vẫn cồng kềnh và tốn thời gian để thực hiện các thay đổi trong kiến trúc này bởi vì bản chất nguyên khối của hầu hết các triển khai cũng như sự liên kết chặt chẽ của các thành phần thường được tìm thấy với mẫu kiến trúc này.

    Dễ triển khai

    Tùy thuộc vào cách chúng ta triển khai mẫu này, khả năng triển khai có thể trở thành một vấn đề, đặc biệt đối với các ứng dụng lớn. Một thay đổi nhỏ đối với một thành phần có thể yêu cầu triển khai lại toàn bộ ứng dụng (hoặc một phần lớn của ứng dụng), dẫn đến việc triển khai cần được lập kế hoạch, được lên lịch và thực hiện ngoài giờ hoặc vào cuối tuần. Như vậy, mô hình này không dễ thích ứng với việc triển khai liên tục, tiếp tục giảm xếp hạng tổng thể cho triển khai.

    Khả năng kiểm thử

    Trong kiến thúc này, các thành phần trong thuộc vào một tầng cụ thể, các tầng khác có thể được mô phỏng hoặc khai thác, giúp cho kiến trúc này tương đối dễ kiểm thử. Một nhà phát triển có thể giả lập một thành phần trình bày hoặc màn hình để cô lập thử nghiệm trong một thành phần nghiệp vụ, cũng như mô phỏng tầng nghiệp vụ để kiểm tra chức năng màn hình nhất định.

    Hiệu năng

    Mặc dù đúng là một số kiến trúc phân tầng có thể hoạt động tốt, nhưng kiến trúc này không phù hợp với các ứng dụng hiệu năng cao do tính không hiệu quả của việc phải đi qua nhiều tầng của kiến trúc để đáp ứng yêu cầu nghiệp vụ.

    Khả năng mở rộng

    Do xu hướng triển khai nguyên khối và liên kết chặt chẽ của kiến trúc này, các ứng dụng được xây dựng bằng cách sử dụng mẫu kiến trúc này thường khó mở rộng quy mô. Chúng ta có thể mở rộng quy mô kiến trúc phân tầng bằng cách tách các tầng thành các triển khai vật lý riêng biệt hoặc sao chép toàn bộ ứng dụng thành nhiều nút, nhưng nhìn chung mức độ chi tiết quá rộng, khiến việc mở rộng quy mô trở nên tốn kém.

    Dễ phát triển

    Tính dễ phát triển nhận được điểm tương đối cao, chủ yếu là do mô hình này quá nổi tiếng và không quá phức tạp để thực hiện. Bởi vì hầu hết các công ty phát triển ứng dụng bằng cách tách các bộ kỹ năng theo tầng (trình diễn, nghiệp vụ, cơ sở dữ liệu), kiến trúc này trở thành lựa chọn tự nhiên cho hầu hết việc phát triển ứng dụng kinh doanh. Mối liên hệ giữa cơ cấu tổ chức và truyền thông của công ty với cách thức phát triển phần mềm được vạch ra là cái được gọi là định luật Conway. Bạn có thể Google "Conway’s law" để có thêm thông tin về mối tương quan hấp dẫn này.

    Tài liệu tham khảo

    • Software Architecture Patterns