Skip to content

JPA Integration

Konstantin Triger edited this page Sep 26, 2019 · 13 revisions

FluentJPA:

  • 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.

Concept

  • 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 and Department 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.

Special Cases

@ManyToMany

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.

@ElementCollection

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()));
});

Inheritance / Table split

There are several option to model inheritance with JPA

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:

  1. PartialTable<> - represents and additional table in any case of table split (Joined Subclass, Secondary Table).
  2. typeOf() - used to create a discriminator column/value filter.

Examples:

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(e).JOIN(empEx).ON(condition);
});

Enums

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().