Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DRAFT IN PROGRESS - DO NOT START ON THIS YET - Documentation, Jakarta Data 1.0 #7468

Open
1 of 2 tasks
njr-11 opened this issue Aug 14, 2024 · 0 comments
Open
1 of 2 tasks
Assignees

Comments

@njr-11
Copy link

njr-11 commented Aug 14, 2024

--- STILL IN PROGRESS BY DEV TEAM. DO NOT START ON DOCUMENTATION FOR THIS YET ---

Feature epic details

Operating systems

Does the documentation apply to all operating systems?

  • Yes
  • No; specify operating systems: ______

Jakarta Data

Overview of Jakarta Data

Jakarta Data is a programming model for relational and non-relational data access where you define simple Java objects to represent data and you define interfaces for operations on data. You inject these interfaces into your application and use them to access data. The implementation is supplied for you by a Jakarta Data Provider.

Concepts in Jakarta Data

Jakarta Data simplifies data access by allowing you to

  • represent data with simple Java objects (entities), and
  • define interfaces (repositories) with methods that perform operations on it.

Jakarta Data Providers

Jakarta Data Providers supply the implementation of repositories, which can be injected into applications via CDI. Jakarta Data Providers can be third-party products or can be provided by the application server. Open Liberty includes a built-in Jakarta Data Provider (link) for relational database access that is backed by the built-in Jakarta Persistence Provider, EclipseLink. Some third-party Jakarta Data providers include Hibernate (for relational data) and JNoSQL (for various types of non-relational data).

Entity Model

Jakarta Data reuses the entity models of Jakarta Persistence and Jakarta NoSQL.

  • Jakarta Persistence entities for relational data.
  • Jakarta NoSQL entities for non-relational (NoSQL) data.

Jakarta Data Providers might define additional entity models, possibly by defining their own entity annotations, or by defining conventions for supporting unannotated entities, or by some other vendor-specific model.

Jakarta Persistence Entity Example

@Entity
public class Car {
    @Id
    public String vin; 

    public String make;

    public String model;

    public int modelYear;

    public int odometer;

    public float price;
}

Static Metamodel

An entity can optionally have a static metamodel, typically generated from the entity class by development tooling, which can be used by the repository interface as well as the application in defining and invoking data access operations in a more type-safe manner.

By convention, static metamodel interface classes typically begin with the underscore character, which is followed by the entity class name.

Static Metamodel Example

@StaticMetamodel(Car.class)
public interface _Car {
    String MAKE = "make";
    String MODEL = "model";
    String MODELYEAR = "modelYear";
    String ODOMETER = "odometer";
    String PRICE = "price";
    String VIN = "vin";

    TextAttribute<Car> make = new TextAttributeRecord<>(MAKE);
    TextAttribute<Car> model = new TextAttributeRecord<>(MODEL);
    SortableAttribute<Car> modelYear = new SortableAttributeRecord<>(MODELYEAR);
    SortableAttribute<Car> odometer = new SortableAttributeRecord<>(ODOMETER);
    SortableAttribute<Car> price = new SortableAttributeRecord<>(PRICE);
    TextAttribute<Car> vin = new TextAttributeRecord<>(VIN);
}

Repositories

Repository interfaces must be annotated with the Repository annotation and can optionally inherit from any of the following repository supertypes that are defined by Jakarta Data:

The repository supertypes offer a variety of pre-defined methods for commonly-used data access operations. The first type parameter to the repository supertypes specifies a primary Entity class that can be assumed for repository methods that do not otherwise specify an Entity class.

Repository Interface Example

@Repository
public interface Cars extends BasicRepository<Car, String> {
    @Insert
    Car add(Car car);

    @OrderBy(_Car.ODOMETER)
    @OrderBy(_Car.VIN)
    List<Car> findByModelYearGreaterThanEqualAndPriceBetween(int minYear,
                                                             float minPrice,
                                                             float maxPrice);

    @Find
    Page<Car> search(@By(_Car.MAKE) String manufacturer,
                     @By(_Car.MODEL) String model,
                     @By(_Car.MODELYEAR) int year,
                     PageRequest pageRequest,
                     Order<Car> sortBy);

    @Query("UPDATE Car SET price = ?2 WHERE vin = ?1")
    boolean setPrice(String vehicleIdNum, float newPrice);

    @Query("SELECT price FROM Car WHERE vin = :vehicleId AND price > 0")
    Optional<Float> getPrice(@Param("vehicleId") String vin);

    @Delete
    boolean remove(@By(_Car.VIN) String vehicleIdNum);
}

Repository Interface Usage Example

@Path("/cars")
@ApplicationScoped
public class ExampleResource {
    @Inject
    Cars cars;

    @GET
    @Path("/make/{make}/model/{model}/year/{year}/page/{pageNum}")
    @Produces(MediaType.TEXT_PLAIN)
    public String search(
            @PathParam("make") String make,
            @PathParam("model") String model,
            @PathParam("year") int year,
            @PathParam("pageNum") long pageNum) {

        PageRequest pageRequest = PageRequest.ofPage(pageNum).size(10);

        Order<Car> mostToLeastExpensive = Order.by(
                _Car.price.desc(),
                _Car.vin.asc());

        Page<Car> page = cars.search(make, model, year, 
                                     pageRequest, 
                                     mostToLeastExpensive);

        return page.stream()
                .map(c -> c.modelYear + " " + c.make + " " + c.model + " " +
                          c.odometer + " miles $" + c.price + " #" + c.vin)
                .collect(Collectors.toList())
                .toString();
    }
}

Linking a Repository to a Provider

When there is only one Jakarta Data Provider that handles the type of entity, it is unnecessary for the Repository to specify which Jakarta Data Provider to use.

However, when multiple Jakarta Data providers coexist in a system and handle the same entity type, the Repository must specify a provider name to disambiguate which Jakarta Data Provider is used.

The built-in Jakarta Data Provider uses the provider name, "Liberty". It is included in the data-1.0 feature (link) and can be made unavailable (so it does not conflict with a third-party Jakarta Data Provider) by configuring the Liberty server to instead use the dataContainer-1.0 feature (link).

Hibernate Provider Name Example

@Repository(provider = "Hibernate", dataStore = "MyPersistenceUnit")
public interface Employee implements CrudRepository<Employee, Long> {
}

Built-in Provider Name Example

@Repository(provider = "Liberty", dataStore = "java:app/env/jdbc/MyDataSourceRef")
public interface Products implements CrudRepository<Product, String> {
}

Built-in Jakarta Data Provider

Overview of Built-in Jakarta Data Provider

Open Liberty includes a built-in Jakarta Data Provider for relational database access that is backed by the built-in Jakarta Persistence Provider, EclipseLink. The built-in provider is available when you enable the data-1.0 feature (link) alongside the persistence-3.2 feature (link) or jdbc-4.3 or jdbc-4.2 feature (links).

Entity Model

The built-in provider accepts Jakarta Persistence entities or Java Records as entities (link).

Provider Name

When multiple Jakarta Data providers supporting the same types of entities coexist in the system, use the provider name of "Liberty" to explicitly request the built-in Jakarta Data Provider.

@Repository(provider = "Liberty")
public interface Products implements BasicRepository<Product, String> {
}

Choosing the Database

The built-in Jakarta Data Provider, which uses the built-in Jakarta Persistence Provider, EclipseLink, can connect to the same databases that EclipseLink supports. Individual capabilities within Jakarta Data and the entity model are limited by the extent to which EcilpseLink supports each database.

The Default Database

If you do not specify otherwise, you end up with the Jakarta EE Default DataSource, which is java:comp/DefaultDataSource, as the data store for your repository. The Jakarta EE Default DataSource is not configured by default in Liberty. To configure it, include configuration of a <dataSource> with id of DefaultDataSource in your server configuration. For example,

<dataSource id="DefaultDataSource">
  <jdbcDriver libraryRef="PostgresLib"/>
  <properties.postgresql databaseName="exampledb" serverName="localhost" portNumber="5432"/>
  <containerAuthData user="dbuser1" password="dbpwd1"/>
</dataSource>

<library id="PostgresLib">
  <fileset dir="${server.config.dir}/lib/postgres" includes="*.jar"/>
</library>

EclipseLink will attempt to create the necessary database tables automatically, which is convenient for development, but is not efficient for running in production because checking for table existence incurs a lot of overhead and database admin privileges might be required for table creation. Refer to the sections on Specifying a DatabaseStore [link] and Specifying a Persistence Unit Reference [link] for information about how to control the table creation settings.

Specifying a DataSource

To specify which DataSource a repository will use, set the dataStore attribute of the Repository annotation to one of:

  • JNDI name of a resource reference to a DataSource. For example, java:app/env/jdbc/MyDataSourceRef.
  • JNDI name of a DataSource. This could be from the name value of a DataSourceDefinition or from the jndiName value of a dataSource from server configuration.
  • The id of a dataSource from server configuration.

When using JNDI names or resource reference names that are scoped to a namespace like java:comp or java:module, you must ensure that the scope is accessible to the location of the repository interface within the application. For example, a DataSource in the java:comp scope of an EJB is never accessible to a repository interface because although a repository interface might exist in an EJB module, the repository interface is not part of any specific EJB.

A good practice when using scoped names is to use java:app to allow the repository interface to be defined in any module of the application.

EclipseLink will attempt to create the necessary database tables automatically, which is convenient for development, but is not efficient for running in production because checking for table existence incurs a lot of overhead and database admin privileges might be required for table creation. Refer to the sections on Specifying a DatabaseStore [link] and Specifying a Persistence Unit Reference [link] for information about how to control the table creation settings.

DataSource id Example

This example shows a repository interface that points to the id of a <dataSource> in server configuration.

Repository interface:

@Repository(dataStore = "ExampleDataSourceID")
public interface Products implements BasicRepository<Product, String> {
}

Server configuration: [TODO switch to Oracle config for better variety]

<dataSource id="ExampleDataSourceID">
  <jdbcDriver libraryRef="PostgresLib"/>
  <properties.postgresql databaseName="exampledb" serverName="localhost" portNumber="5432"/>
  <containerAuthData user="dbuser1" password="dbpwd1"/>
</dataSource>

<library id="PostgresLib">
  <fileset dir="${server.config.dir}/lib/postgres" includes="*.jar"/>
</library>

DataSource jndiName Example

This example shows a repository interface that points to the jndiName of a <dataSource> in server configuration.

Repository interface:

@Repository(dataStore = "jdbc/ExampleDataSource")
public interface Products implements BasicRepository<Product, String> {
}

Server configuration: [TODO switch to DB2 config for better variety]

<dataSource jndiName="jdbc/ExampleDataSource">
  <jdbcDriver libraryRef="PostgresLib"/>
  <properties.postgresql databaseName="exampledb" serverName="localhost" portNumber="5432"/>
  <containerAuthData user="dbuser1" password="dbpwd1"/>
</dataSource>

<library id="PostgresLib">
  <fileset dir="${server.config.dir}/lib/postgres" includes="*.jar"/>
</library>

DataSource Resource Reference Example

This example shows a repository interface that points to a resource reference to a <dataSource> in server configuration.

Repository interface:

@Repository(dataStore = "java:app/env/jdbc/ExampleDataSourceRef")
public interface Products implements BasicRepository<Product, String> {
}

Jakarta EE component (defines the resource reference):

@ApplicationScoped
@Path("/products")
public class ExampleResource {
    @Resource(name = "java:app/env/jdbc/ExampleDataSourceRef",
              lookup = "jdbc/ExampleDataSource")
    DataSource ds;

    @Inject
    Product products;

    ...
}

Server configuration: [TODO switch to MS SQL Server config for better variety]

<dataSource jndiName="jdbc/ExampleDataSource">
  <jdbcDriver libraryRef="PostgresLib"/>
  <properties.postgresql databaseName="exampledb" serverName="localhost" portNumber="5432"/>
  <containerAuthData user="dbuser1" password="dbpwd1"/>
</dataSource>

<library id="PostgresLib">
  <fileset dir="${server.config.dir}/lib/postgres" includes="*.jar"/>
</library>

DataSourceDefinition Example

This example shows a repository interface that points to the name of a DataSourceDefinition.

Repository interface:

@Repository(dataStore = "java:comp/jdbc/ExampleDataSource")
public interface Products implements BasicRepository<Product, String> {
}

Web component or EJB component (defines the DataSourceDefinition):

@ApplicationScoped
@DataSourceDefinition(name = "java:app/jdbc/ExampleDataSource",
                      className = "org.apache.derby.jdbc.EmbeddedXADataSource",
                      databaseName = "memory:exampledb",
                      user = "dbuser1",
                      password = "dbpwd1",
                      properties = "createDatabase=create")
@Path("/products")
public class ExampleResource {
    @Inject
    Product products;

    ...
}

Specifying a Persistence Unit Reference

Defining a Persistence Unit gives you more control over the Jakarta Persistence configuration and EcilpseLink specific settings. To specify which Persistence Unit a repository will use, set the dataStore attribute of the Repository annotation to a Persistence Unit reference. For example,

Repository interface

@Repository(dataStore = "java:app/env/persistence/ExamplePersistenceUnitRef")
public interface Products implements BasicRepository<Product, String> {
}

Jakarta EE component

The persistence unit reference can be defined in a Jakarta EE component. This example also defines a data source resource reference which is used by the persistence unit.

@ApplicationScoped
@Path("/products")
public class ExampleResource {
    @PersistenceUnit(name = "java:app/env/persistence/ExamplePersistenceUnitRef",
                     unitName = "ExamplePersistenceUnit")
    EntityManagerFactory emf;

    @Resource(name = "java:app/env/jdbc/ExampleDataSourceRef",
              lookup = "jdbc/ExampleDataSource")
    DataSource ds;

    @Inject
    Product products;

    ...
}

Persistence Unit

The persistence unit is defined according to the Jakarta Persistence specification. It selects which DataSource to use and determines which entity classes are accessible to repositories.

Here is an example entry in persistence.xml defining a persistence unit that is used by the prior examples,

<persistence-unit name="ExamplePersistenceUnit">
  <jta-data-source>java:app/env/jdbc/ExampleDataSourceRef</jta-data-source>
  <class>example.data.entity.Product</class>
  <properties>
    <property name="jakarta.persistence.schema-generation.database.action" value="create"/>
    <property name="eclipselink.logging.parameters" value="true"/>
  </properties>
</persistence-unit>

The jakarta.persistence.schema-generation.database.action property, which is from the Jakarta Persistence specification, allows you to request the automatic creation and/or removal of database tables. Values for this setting include: create, drop, drop-and-create, none.

DataSource in server configuration:

<dataSource jndiName="jdbc/ExampleDataSource">
  <jdbcDriver libraryRef="PostgresLib"/>
  <properties.postgresql databaseName="exampledb" serverName="localhost" portNumber="5432"/>
  <containerAuthData user="dbuser1" password="dbpwd1"/>
</dataSource>

<library id="PostgresLib">
  <fileset dir="${server.config.dir}/lib/postgres" includes="*.jar"/>
</library>

When using JNDI names or resource reference names that are scoped to a namespace like java:comp or java:module, you must ensure that the scope is accessible to the location of the repository interface within the application. For example, a Persistence Unit reference in the java:comp scope of an EJB is never accessible to a repository interface because although a repository interface might exist in an EJB module, the repository interface is not part of any specific EJB.

A good practice when using scoped names is to use java:app to allow the repository interface to be defined in any module of the application.

Specifying a DatabaseStore

A databaseStore is an abstraction on top of a dataSource which gives you the ability to configure additional aspects related to Jakarta Persistence, such as table creation and removal, without needing to provide persistence units. It also ties into to the ddlgen tool, which generates DDL files for a database admin to run manually under the required database privileges.

Point the dataStore attribute of a Repository annotation at the id of a databaseStore element from server configuration.

Repository interface:

@Repository(dataStore = "ExampleDatabaseStore")
public interface Products implements BasicRepository<Product, String> {
}

Server configuration:

<databaseStore id="ExampleDatabaseStore" dataSourceRef="ExampleDataSource"
               createTables="true" dropTables="false" tablePrefix="DATA">
  <authData user="dbuser1" password="dbpwd1"/>
</databaseStore>

<dataSource id="ExampleDataSource">
  <jdbcDriver libraryRef="PostgresLib"/>
  <properties.postgresql databaseName="exampledb" serverName="localhost" portNumber="5432"/>
</dataSource>

<library id="PostgresLib">
  <fileset dir="${server.config.dir}/lib/postgres" includes="*.jar"/>
</library>

DDL Generation

TODO


Java Records as Entities

The built-in Jakarta Data Provider allows Java records to be entities. These entities are not annotated and instead rely on a couple of basic conventions to be interpreted as entities. The built-in Jakarta Data Provider substitutes a Jakarta Persistence entity for the Java Record. Thus the entity model for Java Record entities is in effect a subset of the Jakarta Persistence entity model, with the difference that it is defined by a Java Record rather than entity annotations.

Record Components

The record components are the attributes or fields of the entity.

Basic Persistent Field Types [Is there any way to link to the section with that title?] that are specified by Jakarta Data can be used. These include types such as int, Integer, String, UUID, byte[], LocalDateTime, 'Instant', and enum types. Some additional types for collections and embeddables can also be used.

Determining the Id

Precedence for determining the Id attribute (which is required), from highest to lowest:

  • record component name is "id", ignoring case.
  • record component name ends with "_id", ignoring case.
  • record component name ends with "Id" or "ID".
  • record component type is UUID with any name.

Use the same basic types for Ids that you can use with Jakarta Persistence entities. You can also form composite Ids by using a type that is another Java record whose record components are basic types for Ids that you can use with Jakarta Persistence entities.

Determining the Version

Precedence for determining the row version attribute (optional), from highest to lowest:

  • record component name is "version", ignoring case.
  • record component name is "_version", ignoring case.

Nullable Attributes

Record components with primitive types are non-nullable. Record component with other types are nullable.

Collection Attributes

Record component types that are instances of java.util.Collection are considered element collections.

Relation Attributes

Record components with other types (including Java record) are considered embeddables. Like Java record entities, these embeddables are not annotated.

Record Entity Example

public record Rectangle(
                String id,
                Point position,
                int height,
                int width,
                long version) {
 
    public static record Point(
                    int x,
                    int y) {
    }
}

The static metamodel for a record entities is optional and follows the same rules and conventions as with other types of entities. The static metamodel is typically generated by tooling.

Static Metamodel Example

@StaticMetamodel(Rectangle.class)
public interface _Rectangle {
    String HEIGHT = "height";
    String ID = "id";
    String POSITION = "position";
    String POSITION_X = "position.x";
    String POSITION_Y = "position.y";
    String VERSION = "version";
    String WIDTH = "width";

    SortableAttribute<Rectangle> height = new SortableAttributeRecord<>(HEIGHT);
    TextAttribute<Rectangle> id = new TextAttributeRecord<>(ID);
    Attribute<Rectangle> position = new AttributeRecord<>(POSITION);
    SortableAttribute<Rectangle> position_x = new SortableAttributeRecord<>(POSITION_X);
    SortableAttribute<Rectangle> position_y = new SortableAttributeRecord<>(POSITION_Y);
    SortableAttribute<Rectangle> version = new SortableAttributeRecord<>(VERSION);
    SortableAttribute<Rectangle> width = new SortableAttributeRecord<>(WIDTH);
}

Identifying When a Record is an Entity

A Repository that defines queries and other operations on Java Record entities must indicate its use of the Java Record as an entity in one of the following ways:

  1. Inherit from a built-in repository superinterface and specify the Java Record class as its first type parameter.

    Repository with Superinterface Example
@Repository
public interface Rectangles extends CrudRepository<Rectangle, String> {
}
  1. Instead of inheriting from a built-in repository superinterface, define one or more life cycle methods (annotated with Insert, Delete, Save, or Update, all of which specify the same Java record type as the type of the parameter or as the element type of the collection or array that is the parameter.

    Repository with Life Cycle Methods Example
@Repository
public interface Rectangles {
    @Insert
    Rectangle[] create(Rectangle... r);

    @Update
    Rectangle modify(Rectangle r);

    @Delete
    void remove(List<Rectangle> r);
}

Example Table for a Record Entity

...

Limitations of Record Entities

Jakarta Persistence does not allow Java records to be entities. The built-in Jakarta Data provider makes this possible by substituting a generated entity class for the record.

... more details such as the reserved entity name

@njr-11 njr-11 self-assigned this Aug 14, 2024
@KyleAure KyleAure self-assigned this Oct 3, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants