-
Notifications
You must be signed in to change notification settings - Fork 10
JPA Integration
- Shares the same model (entities) JPA uses, so the user has just a single domain model.
- Shares the same mappings, so the user does not not need to sync or cope with incompatibilities.
- Shares the same context (EntityManager), so the user can mix Hibernate and FluentJPA queries freely since they run in the same context/transaction. Also Hibernate will correctly flush the context (if needed), invoke Listeners etc. Its full life cycle is kept.
- Integrates with JPA Repositories, so the same programming model can be kept.
- Provides an alternative to Criteria API, so the user can create dynamic queries with JPA entities with fluent API.
-
JPA assumes that once the model is mapped, the user fully abstracts the database and works on Java classes level only. FluentJPA fully follows this concept. It reads all the JPA mapping annotations and generates correct Table and Column names, join associations, etc. For example, given 2 entities
Employee
andDepartment
and a query like this:FluentQuery query = FluentJPA.SQL((Employee e, Department d) -> { SELECT(e.getLastName(), d.getName()); // correct column names are generated FROM(e).JOIN(d).ON(e.getDepartment() == d); // correct JOIN condition is generated });
FluentJPA is JPA join-aware, so an expression like
e.getDepartment() == d
is perfectly legal and works as expected. -
When the
FluentQuery
instance is created, the next step is creating a JPA Query and executing it:query.createQuery(entityManager, <X>.class).getResultList(); // or getSingleResult() // or executeUpdate()
Since FluentJPA works via JPA pipeline (under the hood it calls entityManager.createNativeQuery), it's fully integrated with it (configuration, synchronization, transaction, etc). Also, returned entities enter the persistence context (as read-only). See Returning Results for more details.
Note, most of common SQL syntax is probably familiar, but in case of any doubt, use your database vendor documentation. FluentJPA declares SQL API according to the standard, and if your vendor does not implement some functionality, using it will fail in runtime.
FluentJPA does not perform any logical validation of the statements you write. Much like Java compiler transforms Java to byte code as is, FluentJPA translates this byte code to SQL.
Under the hood JPA models @ManyToMany
with a hidden Join Table, which holds foreign keys to both sides of the relationship. Hiding the Join Table simplifies the Java side experience, but inability to work with Join Table in SQL prevents different scenarios on SQL side. FluentJPA philosophy is to provide a full access to SQL, therefore FluentJPA supplies a "virtual" JoinTable
entity that "feels and behaves" exactly as if it was actually declared as an entity by the user. For example:
// In Customer class:
@ManyToMany
@JoinTable(name="CUST_PHONES")
private Set<PhoneNumber> phones;
// In PhoneNumber class:
@ManyToMany(mappedBy="phones")
private Set<Customer> customers;
Then to count phone numbers per customer we can write:
FluentJPA.SQL((Customer customer,
PhoneNumber phoneNumber,
JoinTable<Customer, PhoneNumber> custPhones) -> {
// special interface ^^^^^^^ that behaves like a real entity
SELECT(COUNT(custPhones.getInverseJoined().getId()), ...);
// we can access the JoinTable columns ^^^^^^^ in a type safe way
FROM(customer).JOIN(custPhones)
.ON(custPhones.join(customer, Customer::getPhones))
// method that generates the association ^^
.JOIN(phoneNumber)
.ON(custPhones.join(phoneNumber, PhoneNumber::getCustomers));
...
GROUP(BY(customer.getId()));
});
But for this task we actually don't need PhoneNumber
entity at all! We can simplify:
FluentJPA.SQL((Customer customer,
JoinTable<Customer, PhoneNumber> custPhones) -> {
SELECT(COUNT(custPhones.getInverseJoined().getId()), ...);
FROM(customer, custPhones);
WHERE(custPhones.join(customer, Customer::getPhones));
// can be in a where condition ^^^^^^
GROUP(BY(customer.getId()));
});
Another interesting implication is that we can INSERT directly into the Join Table:
FluentQuery query = FluentJPA.SQL((Student student,
JoinTable<Student, Course> coursesToStudents) -> {
// must "explain" the FluentJPA how to map, but then discard any produced SQL
discardSQL(coursesToStudents.join(student, Student::getLikedCourses));
// use JoinTable as any normal Entity
INSERT().INTO(viewOf(coursesToStudents, jt -> jt.getJoined().getId(),
jt -> jt.getInverseJoined().getId()));
VALUES(row(1, 2));
});
Or filter the Join Table:
int courseId = ...; //external parameter
FluentQuery query = FluentJPA.SQL((Student student,
JoinTable<Student, Course> coursesToStudents) -> {
// must "explain" the FluentJPA how to map, but then discard any produced SQL
discardSQL(coursesToStudents.join(student, Student::getLikedCourses)));
// use JoinTable as any normal Entity
SELECT(coursesToStudents.getJoined().getId()); // SELECTs Student ids
FROM(coursesToStudents);
WHERE(coursesToStudents.getInverseJoined().getId() == courseId);
});
Note, at least one call to
join()
must be in a query. This is how FluentJPA dynamically "learns" the association.
Under the hood JPA models @ElementCollection
with a hidden Element Table, which holds foreign keys to owning entity and an embedded element. Hiding the Element Table simplifies the Java side experience, but inability to work with Element Table in SQL prevents different scenarios on SQL side. FluentJPA philosophy is to provide a full access to SQL, therefore FluentJPA supplies a "virtual" ElementCollection
entity that "feels and behaves" exactly as if it was actually declared as an entity by the user. For example:
// in Person class
@ElementCollection
protected Set<String> nickNames = new HashSet();
To count nickname per person we can:
FluentQuery query = FluentJPA.SQL((Person person,
ElementCollection<Person, String> userNicks) -> {
// special interface ^^^^^^^^^^^^ that behaves like a real entity
SELECT(COUNT(userNicks.getOwner().getId()));
// ElementCollection columns ^^^^^^^ can be accessed in a type safe way
// getOwner() is a foreign key to the Person entity
FROM(person).JOIN(userNicks).ON(userNicks.join(person, User::getNickNames));
// method that generates the association ^^^^
...
GROUP(BY(userNicks.getOwner().getId()));
});
But for this task we actually don't need Person
entity at all! We can simplify:
FluentJPA.SQL((Person person,
ElementCollection<Person, String> userNicks) -> {
// must "explain" the FluentJPA how to map, but then discard any produced SQL
discardSQL(userNicks.join(person, User::getNickNames));
SELECT(COUNT(userNicks.getOwner().getId()));
FROM(userNicks);
GROUP(BY(userNicks.getOwner().getId()));
});
There are several option to model inheritance with JPA
-
Single Table Inheritance. In the database all the records are saved to the same table and differentiated by
@DiscriminatorValue
. -
Joined Subclass Inheritance. In the database the records are split by type to 2 tables and differentiated by
@DiscriminatorValue
. - Table per Class Inheritance. In the database there is a table per class.
In addition there is an option to map an Entity to multiple tables. This can be used for simple and derived entities (usually in "Table per Class Inheritance" cases).
FluentJPA provides 2 constructs to handle all the cases:
-
PartialTable<>
- represents and additional table in any case of table split (Joined Subclass, Secondary Table). -
typeOf()
- used to create a discriminator column/value filter.
Assuming Joined Subclass Inheritance:
FluentQuery query = FluentJPA.SQL((FullTimeEmployee e,
PartialTable<Employee> empEx) -> {
boolean condition = empEx.joined(e);
SELECT(empEx, e.getName(), e.getSalary());
FROM(e).JOIN(empEx).ON(condition);
});
// SELECT only FullTimeEmployee from Employee table:
FluentQuery query = FluentJPA.SQL((Employee e) -> {
SELECT(e);
FROM(e);
// use discriminator to filter
WHERE(typeOf(e, FullTimeEmployee.class));
});
Assuming there is a multiple tables mapping:
FluentQuery query = FluentJPA.SQL((Employee e,
PartialTable<Employee> empEx) -> {
boolean condition = empEx.secondary(e);
SELECT(e, e.getYearsOfService(), e.getManager().getId());
FROM(u).JOIN(empEx).ON(condition);
});
Enums are fully supported in FluentJPA, both ordinal and string types. But what if you need access to the underlying typed value? Just call name()
or ordinal()
. And of course you can always CAST()
.
Getting Started
- Introduction
- Setup
- Data Types
- Entities & Tuples
- Sub Queries
- JPA Integration
- Java Language Support
- Directives
- Library
- Returning Results
- JPA Repositories
Examples
Basic SQL DML Statements
Advanced SQL DML Statements
- Common Table Expressions (WITH Clause)
- Window Functions (OVER Clause)
- Aggregate Expressions
- MERGE
- Temporal Tables
Advanced Topics