Cara Membangun Aplikasi Multitenant: Tutorial Hibernate
Diterbitkan: 2022-03-11Ketika kita berbicara tentang aplikasi cloud di mana setiap klien memiliki data terpisah, kita perlu memikirkan cara menyimpan dan memanipulasi data ini. Bahkan dengan semua solusi NoSQL yang hebat di luar sana, terkadang kita masih perlu menggunakan database relasional lama yang bagus. Solusi pertama yang mungkin terlintas dalam pikiran untuk memisahkan data adalah dengan menambahkan pengenal di setiap tabel, sehingga dapat ditangani secara individual. Itu berhasil, tetapi bagaimana jika klien meminta database mereka? Akan sangat merepotkan untuk mengambil semua catatan yang tersembunyi di antara yang lain.
Tim Hibernate datang dengan solusi untuk masalah ini beberapa waktu lalu. Mereka menyediakan beberapa titik ekstensi yang memungkinkan seseorang untuk mengontrol dari mana data harus diambil. Solusi ini memiliki opsi untuk mengontrol data melalui kolom pengenal, beberapa database, dan beberapa skema. Artikel ini akan membahas solusi beberapa skema.
Jadi, ayo mulai bekerja!
Mulai
Jika Anda adalah pengembang Java yang lebih berpengalaman dan tahu cara mengonfigurasi semuanya, atau jika Anda sudah memiliki proyek Java EE Anda sendiri, Anda dapat melewati bagian ini.
Pertama, kita harus membuat proyek Java baru. Saya menggunakan Eclipse dan Gradle, tetapi Anda dapat menggunakan IDE dan alat bangunan pilihan Anda, seperti IntelliJ dan Maven.
Jika Anda ingin menggunakan alat yang sama dengan saya, Anda dapat mengikuti langkah-langkah ini untuk membuat proyek Anda:
- Instal plugin Gradle di Eclipse
- Klik File -> Baru -> Lainnya…
- Temukan Gradle (STS) dan klik Berikutnya
- Informasikan nama dan pilih Java Quickstart untuk proyek sampel
- Klik Selesai
Besar! Ini harus menjadi struktur file awal:
javaee-mt |- src/main/java |- src/main/resources |- src/test/java |- src/test/resources |- JRE System Library |- Gradle Dependencies |- build |- src |- build.gradle
Anda dapat menghapus semua file yang ada di dalam folder sumber, karena itu hanya file sampel.
Untuk menjalankan proyek, saya menggunakan Wildfly, dan saya akan menunjukkan cara mengkonfigurasinya (sekali lagi Anda dapat menggunakan alat favorit Anda di sini):
- Unduh Wildfly: http://wildfly.org/downloads/ (Saya menggunakan versi 10)
- Buka zip file
- Instal plugin JBoss Tools di Eclipse
- Pada tab Server, klik kanan area kosong dan pilih New -> Server
- Pilih Wildfly 10.x (9.x juga berfungsi jika 10 tidak tersedia, tergantung pada versi Eclipse Anda)
- Klik Next, pilih Create New Runtime (halaman berikutnya) dan klik Next lagi
- Pilih folder tempat Anda membuka ritsleting Wildfly sebagai Direktori Rumah
- Klik Selesai
Sekarang, mari kita konfigurasikan Wildfly untuk mengetahui databasenya:
- Buka folder bin di dalam folder Wildfly Anda
- Jalankan add-user.bat atau add-user.sh (tergantung OS Anda)
- Ikuti langkah-langkah untuk membuat pengguna Anda sebagai Manajer
- Di Eclipse, buka tab Server lagi, klik kanan pada server yang Anda buat dan pilih Mulai
- Di browser Anda, akses http://localhost:9900, yang merupakan Antarmuka Manajemen
- Masukkan kredensial pengguna yang baru saja Anda buat
- Terapkan toples driver database Anda:
- Buka tab Deployment dan klik Add
- Klik Berikutnya, pilih file jar driver Anda
- Klik Berikutnya dan Selesai
- Buka tab Konfigurasi
- Pilih Subsistem -> Sumber Data -> Non-XA
- Klik Tambah, pilih database Anda dan klik Berikutnya
- Beri nama untuk sumber data Anda dan klik Berikutnya
- Pilih tab Deteksi Driver dan pilih driver yang baru saja Anda gunakan
- Masukkan informasi database Anda dan klik Berikutnya
- Klik Uji Koneksi jika Anda ingin memastikan informasi langkah sebelumnya benar
- Klik Selesai
- Kembali ke Eclipse dan hentikan server yang sedang berjalan
- Klik kanan padanya, pilih Tambah dan Hapus
- Tambahkan proyek Anda di sebelah kanan
- Klik Selesai
Baiklah, kita telah mengkonfigurasi Eclipse dan Wildfly bersama-sama!
Ini semua konfigurasi yang diperlukan di luar proyek. Mari kita beralih ke konfigurasi proyek.
Proyek Bootstrap
Sekarang setelah Eclipse dan Wildfly dikonfigurasi dan proyek kami dibuat, kami perlu mengonfigurasi proyek kami.
Hal pertama yang akan kita lakukan adalah mengedit build.gradle. Ini adalah bagaimana seharusnya terlihat:
apply plugin: 'java' apply plugin: 'war' apply plugin: 'eclipse' apply plugin: 'eclipse-wtp' sourceCompatibility = '1.8' compileJava.options.encoding = 'UTF-8' compileJava.options.encoding = 'UTF-8' compileTestJava.options.encoding = 'UTF-8' repositories { jcenter() } eclipse { wtp { } } dependencies { providedCompile 'org.hibernate:hibernate-entitymanager:5.0.7.Final' providedCompile 'org.jboss.resteasy:resteasy-jaxrs:3.0.14.Final' providedCompile 'javax:javaee-api:7.0' }
Semua dependensi dideklarasikan sebagai "providedCompile", karena perintah ini tidak menambahkan dependensi dalam file perang terakhir. Wildfly sudah memiliki dependensi ini, dan itu akan menyebabkan konflik dengan dependensi aplikasi.
Pada titik ini, Anda dapat mengklik kanan proyek Anda, pilih Gradle (STS) -> Refresh All untuk mengimpor dependensi yang baru saja kita deklarasikan.
Saatnya membuat dan mengkonfigurasi file “persistence.xml”, file yang berisi informasi yang dibutuhkan Hibernate:
- Di folder sumber src/main/resource, buat folder bernama META-INF
- Di dalam folder ini, buat file bernama ketekunan.xml
Isi file harus seperti berikut ini, ubah jta-data-source agar sesuai dengan sumber data yang Anda buat di Wildfly dan package com.toptal.andrehil.mt.hibernate
dengan yang akan Anda buat selanjutnya bagian (kecuali jika Anda memilih nama paket yang sama):
<?xml version="1.0" encoding="UTF-8" ?> <persistence xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0" xmlns="http://java.sun.com/xml/ns/persistence"> <persistence-unit name="pu"> <jta-data-source>java:/JavaEEMTDS</jta-data-source> <properties> <property name="hibernate.multiTenancy" value="SCHEMA"/> <property name="hibernate.tenant_identifier_resolver" value="com.toptal.andrehil.mt.hibernate.SchemaResolver"/> <property name="hibernate.multi_tenant_connection_provider" value="com.toptal.andrehil.mt.hibernate.MultiTenantProvider"/> </properties> </persistence-unit> </persistence>
Kelas Hibernasi
Konfigurasi yang ditambahkan ke persistensi.xml mengarah ke dua kelas kustom MultiTenantProvider dan SchemaResolver. Kelas pertama bertanggung jawab untuk menyediakan koneksi yang dikonfigurasi dengan skema yang tepat. Kelas kedua bertanggung jawab untuk menyelesaikan nama skema yang akan digunakan.

Berikut adalah implementasi dari dua kelas:
public class MultiTenantProvider implements MultiTenantConnectionProvider, ServiceRegistryAwareService { private static final long serialVersionUID = 1L; private DataSource dataSource; @Override public boolean supportsAggressiveRelease() { return false; } @Override public void injectServices(ServiceRegistryImplementor serviceRegistry) { try { final Context init = new InitialContext(); dataSource = (DataSource) init.lookup("java:/JavaEEMTDS"); // Change to your datasource name } catch (final NamingException e) { throw new RuntimeException(e); } } @SuppressWarnings("rawtypes") @Override public boolean isUnwrappableAs(Class clazz) { return false; } @Override public <T> T unwrap(Class<T> clazz) { return null; } @Override public Connection getAnyConnection() throws SQLException { final Connection connection = dataSource.getConnection(); return connection; } @Override public Connection getConnection(String tenantIdentifier) throws SQLException { final Connection connection = getAnyConnection(); try { connection.createStatement().execute("SET SCHEMA '" + tenantIdentifier + "'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [" + tenantIdentifier + "]", e); } return connection; } @Override public void releaseAnyConnection(Connection connection) throws SQLException { try { connection.createStatement().execute("SET SCHEMA 'public'"); } catch (final SQLException e) { throw new HibernateException("Error trying to alter schema [public]", e); } connection.close(); } @Override public void releaseConnection(String tenantIdentifier, Connection connection) throws SQLException { releaseAnyConnection(connection); } }
Sintaks yang digunakan dalam pernyataan di atas berfungsi dengan PostgreSQL dan beberapa database lain, ini harus diubah jika database Anda memiliki sintaks yang berbeda untuk mengubah skema saat ini.
public class SchemaResolver implements CurrentTenantIdentifierResolver { private String tenantIdentifier = "public"; @Override public String resolveCurrentTenantIdentifier() { return tenantIdentifier; } @Override public boolean validateExistingCurrentSessions() { return false; } public void setTenantIdentifier(String tenantIdentifier) { this.tenantIdentifier = tenantIdentifier; } }
Pada titik ini, sudah dimungkinkan untuk menguji aplikasi. Untuk saat ini, penyelesai kami menunjuk langsung ke skema publik yang dikodekan secara keras, tetapi sudah dipanggil. Untuk melakukan ini, hentikan server Anda jika sedang berjalan dan mulai lagi. Anda dapat mencoba menjalankannya dalam mode debug dan menempatkan breakpoint di titik mana pun dari kelas di atas untuk memeriksa apakah itu berfungsi.
Penggunaan Praktis Resolver
Jadi, bagaimana resolver benar-benar berisi nama skema yang benar?
Salah satu cara untuk mencapai ini adalah dengan menyimpan pengidentifikasi di header semua permintaan dan kemudian membuat filter untuk memasukkan nama skema.
Mari kita terapkan kelas filter untuk mencontohkan penggunaan. Penyelesai dapat diakses melalui SessionFactory Hibernate, jadi kami akan memanfaatkannya untuk mendapatkannya dan menyuntikkan nama skema yang tepat.
@Provider public class AuthRequestFilter implements ContainerRequestFilter { @PersistenceUnit(unitName = "pu") private EntityManagerFactory entityManagerFactory; @Override public void filter(ContainerRequestContext containerRequestContext) throws IOException { final SessionFactoryImplementor sessionFactory = ((EntityManagerFactoryImpl) entityManagerFactory).getSessionFactory(); final SchemaResolver schemaResolver = (SchemaResolver) sessionFactory.getCurrentTenantIdentifierResolver(); final String username = containerRequestContext.getHeaderString("username"); schemaResolver.setTenantIdentifier(username); } }
Sekarang, ketika kelas mana pun mendapatkan EntityManager untuk mengakses database, kelas itu sudah dikonfigurasi dengan skema yang tepat.
Demi kesederhanaan, implementasi yang ditampilkan di sini adalah mendapatkan pengenal langsung dari string di header, tetapi ada baiknya menggunakan token otentikasi dan menyimpan pengenal di token. Jika Anda tertarik untuk mengetahui lebih banyak tentang subjek ini, saya sarankan untuk melihat JSON Web Tokens (JWT). JWT adalah perpustakaan yang bagus dan sederhana untuk manipulasi token.
Cara Menggunakan Semua Ini
Dengan semua yang dikonfigurasi, tidak ada lagi yang perlu dilakukan di entitas dan/atau kelas Anda yang berinteraksi dengan EntityManager
. Apa pun yang Anda jalankan dari EntityManager akan diarahkan ke skema yang diselesaikan oleh filter yang dibuat.
Sekarang, yang perlu Anda lakukan adalah mencegat permintaan di sisi klien dan menyuntikkan pengenal/token di header untuk dikirim ke sisi server.
Tautan di akhir artikel menunjuk ke proyek yang digunakan untuk menulis artikel ini. Ini menggunakan Flyway untuk membuat 2 skema dan berisi kelas entitas yang disebut Mobil dan kelas layanan istirahat yang disebut CarService
yang dapat digunakan untuk menguji proyek. Anda dapat mengikuti semua langkah di bawah ini, tetapi alih-alih membuat proyek Anda sendiri, Anda dapat mengkloningnya dan menggunakan yang ini. Kemudian, saat menjalankan Anda dapat menggunakan klien HTTP sederhana (seperti ekstensi Postman untuk Chrome) dan membuat permintaan GET ke http://localhost:8080/javaee-mt/rest/cars dengan header key:value:
- nama pengguna:jo; atau
- nama pengguna: fred.
Dengan melakukan ini, permintaan akan mengembalikan nilai yang berbeda, yang berada dalam skema yang berbeda, satu disebut joe dan yang lainnya disebut "fred".
Kata-kata Terakhir
Ini bukan satu-satunya solusi untuk membuat aplikasi multitenancy di dunia Java, tetapi ini adalah cara sederhana untuk mencapainya.
Satu hal yang perlu diingat adalah bahwa Hibernate tidak menghasilkan DDL saat menggunakan konfigurasi multitenancy. Saran saya adalah untuk melihat Flyway atau Liquibase, yang merupakan perpustakaan yang bagus untuk mengontrol pembuatan database. Ini adalah hal yang baik untuk dilakukan bahkan jika Anda tidak akan menggunakan multitenancy, karena tim Hibernate menyarankan untuk tidak menggunakan pembuatan database otomatis mereka dalam produksi.
Kode sumber yang digunakan untuk membuat artikel ini dan konfigurasi lingkungan dapat ditemukan di github.com/andrehil/JavaEEMT