Graphine is a Java annotation processor that provides an ORM (Object/Relational Mapping) implementation by generating native human-readable JDBC (Java Database Connectivity) code. It does not support lazy loading, caching, dirty checking and other advanced JPA (Java Persistence API) features.
Graphine focuses on some high-level concepts from DDD (Domain-Driven Design) such as Aggregate (cluster of Entities and Value Objects) and Repository.
- Simplified annotation of entities and repositories
- Compile-time error reporting
- Native human-readable code generation
- NO reflection
- NO runtime dependencies
- NO magic
Graphine requires Java 11 or later.
- Add dependencies:
<properties>
<graphine.version>0.5.0</graphine.version> <!-- Latest version -->
</properties>
<dependency>
<groupId>io.github.omarchenko4j</groupId>
<artifactId>graphine-annotation</artifactId>
<version>${graphine.version}</version>
<scope>provided</scope> <!-- Graphine is not a runtime dependency! -->
</dependency>
- Configure maven-compiler-plugin:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.github.omarchenko4j</groupId>
<artifactId>graphine-processor</artifactId>
<version>${graphine.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
2.1 Configure Graphine with Lombok:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<annotationProcessorPaths>
<!-- Order of declaration is important! -->
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>io.github.omarchenko4j</groupId>
<artifactId>graphine-processor</artifactId>
<version>${graphine.version}</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
The standard behavior is that one entity maps to one database table.
@Entity
annotation has two attributes to specify schema and table names in database.
If the table name is not specified then the entity class name will be used to determine the database table name.
The following transformation pipeline is used by default: SNAKE_CASE
| LOWER_CASE
For example:
Entity class name | Table name |
---|---|
Order | order |
OrderItem | order_item |
This behavior can be changed using compiler arguments.
<compilerArgs>
<arg>-Agraphine.table_naming_pipeline=SNAKE_CASE|UPPER_CASE</arg> <!-- Result: ORDER_ITEM -->
</compilerArgs>
Supported transformation options:
Transformation option | Description | Example |
---|---|---|
SNAKE_CASE | Transforms entity class name to snake case format | Order_Item |
LOWER_CASE | Transforms entity class name to lower case format | orderitem |
UPPER_CASE | Transforms entity class name to upper case format | ORDERITEM |
UNCAPITALIZE | Transforms entity class name to uncapitalize format | orderItem |
Use
|
separator for pipeline transformation.
Use the following compiler argument to specify the default database schema:
<compilerArgs>
<arg>-Agraphine.default_schema={schema_name}</arg>
</compilerArgs>
- Entity must be a Java class;
- Entity class must be annotated with
@Entity
annotation; - Entity class must be
public
; - Entity class must have public no-arg constructor;
- Entity class must have at least one field that is annotated with
@Id
annotation; - Entity class must have getters/setters for all persistent fields.
By default, all fields of an entity class are persistent.
Terminology note: The fields of an entity class are generally referred to as attributes.
The standard behavior is that all entity attributes are mapped to database table columns.
This behavior can be changed by using a compiler argument: graphine.attribute_detection_strategy
.
Example:
<compilerArgs>
<!-- Default behavior - ALL fields are detected as attributes -->
<arg>-Agraphine.attribute_detection_strategy=ALL_FIELDS</arg>
<!-- or -->
<!-- Only annotated fields with @Attribute annotation are detected as attributes -->
<arg>-Agraphine.attribute_detection_strategy=ANNOTATED_FIELDS</arg>
</compilerArgs>
By default, the entity attribute name is mapped to table column name.
But this behavior can be overridden using the @Attribute
annotation.
For example:
@Getter // Lombok annotation.
@Setter // Lombok annotation.
@Entity(table = "users")
public class User {
@Id
@Attribute(column = "user_id")
private UUID id;
private String login;
@Attribute(column = "email_address")
private String email;
}
CREATE TABLE users(
user_id CHAR(36) NOT NULL,
login VARCHAR(64) NOT NULL,
email_address VARCHAR(64) NOT NULL,
PRIMARY KEY (user_id)
);
Supported Java types as entity attributes:
- Primitive types:
boolean
,byte
,short
,int
,long
,float
,double
- Primitive wrapper types:
Boolean
,Byte
,Short
,Integer
,Long
,Float
,Double
java.math
types:BigDecimal
,BigInteger
java.sql
types:Date
,Time
,Timestamp
java.time
types:Instant
,LocalDate
,LocalTime
,LocalDateTime
,Year
,YearMonth
,MonthDay
,Period
,Duration
- Array types:
byte[]
String
,UUID
- Enumeration types
- Custom Java classes annotated with
@Embeddable
annotation;
The mapping of embeddable entity attributes to table columns is the same as for entities.
But an embeddable entity can be reused in different entities.
@AttributeOverride
annotation is used to override attribute mapping across entities.
For example:
@Getter // Lombok annotation.
@Setter // Lombok annotation.
@Embeddable
public class Amount {
private BigDecimal value;
private Currency currency;
}
@Getter // Lombok annotation.
@Setter // Lombok annotation.
@Entity
public class Transaction {
@Id
private UUID id;
@AttributeOverride(name = "value", attribute = @Attribute(column = "amount"))
@AttributeOverride(name = "currency", attribute = @Attribute(column = "currency"))
private Amount amount;
}
@Getter // Lombok annotation.
@Setter // Lombok annotation.
@Entity
public class Purchase {
@Id
private UUID id;
@AttributeOverride(name = "value", attribute = @Attribute(column = "price"))
@AttributeOverride(name = "currency", attribute = @Attribute(column = "currency"))
private Amount price;
}
CREATE TABLE transaction(
id CHAR(36) NOT NULL,
amount DECIMAL(10, 2) NOT NULL,
currency CHAR(3) NOT NULL,
PRIMARY KEY (id)
);
CREATE TABLE purchase(
id CHAR(36) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
currency CHAR(3) NOT NULL,
PRIMARY KEY (id)
);
Embeddable entity requirements:
- Embeddable entity must be a Java class;
- Embeddable entity class must be annotated with
@Embeddable
annotation; - Embeddable entity class must be
public
; - Embeddable entity class must have public no-arg constructor;
- Embeddable entity class must have getters/setters for all persistent fields.
Repository requirements:
- Repository must be a Java interface;
- Repository interface must be annotated with
@Repository
annotation indicating the entity class of it manages;
Supported queryable method names:
Method name prefix | Supported return type | Supported parameter type |
---|---|---|
findBy... | T , Optional<T> |
* |
findAll | Iterable<T> , Collection<T> , List<T> , Set<T> , Stream<T> |
void |
findAllBy... | Iterable<T> , Collection<T> , List<T> , Set<T> , Stream<T> |
* |
findFirstBy... | T , Optional<T> |
* |
countAll | int , long , Integer , Long |
void |
countAllBy... | int , long , Integer , Long |
* |
save | void |
T |
saveAll | void |
Iterable<T> , Collection<T> , List<T> , Set<T> |
update | void |
T |
updateAll | void |
Iterable<T> , Collection<T> , List<T> , Set<T> |
delete | void |
T |
deleteBy... | void |
* |
deleteAll | void |
Iterable<T> , Collection<T> , List<T> , Set<T> |
deleteAllBy... | void |
* |
T
- Your entity type.*
- Any type of parameters.
Prerequisite to use: All generated implementations of repositories use DataSource as dependency;
Here is a small snippet that shows Graphine in action.
/**
* Your model.
*/
@Getter // Lombok annotation.
@Setter // Lombok annotation.
@ToString // Lombok annotation.
@Entity(table = "users")
public class User {
@Id
private UUID id;
private String login;
private String password;
private String email;
}
/**
* Repository for your model.
*/
@Repository(User.class)
public interface UserRepository {
Optional<User> findByLogin(String login);
}
/**
* After building, Graphine will generate the following class.
*/
@Generated("io.graphine.processor.GraphineProcessor")
public class GraphineUserRepository implements UserRepository {
private final DataSource dataSource;
public GraphineUserRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Optional<User> findByLogin(String login) {
try (Connection _connection = dataSource.getConnection()) {
String _query = "SELECT id, login, password, email FROM users WHERE login = ?";
try (PreparedStatement _statement = _connection.prepareStatement(_query)) {
AttributeMappers.setString(_statement, 1, login);
try (ResultSet _resultSet = _statement.executeQuery()) {
if (_resultSet.next()) {
User _user = new User();
_user.setId(AttributeMappers.getUuid(_resultSet, 1));
_user.setLogin(AttributeMappers.getString(_resultSet, 2));
_user.setPassword(AttributeMappers.getString(_resultSet, 3));
_user.setEmail(AttributeMappers.getString(_resultSet, 4));
if (_resultSet.next()) {
throw new NonUniqueResultException();
}
return Optional.of(_user);
}
return Optional.empty();
}
}
}
catch (SQLException _e) {
throw new GraphineException(_e);
}
}
}
/**
* Application launch.
*/
public class Application {
private static final DataSource DATA_SOURCE = createDataSource();
private static DataSource createDataSource() {
return null; // Your configured data source.
}
public static void main(String[] args) {
UserRepository userRepository = new GraphineUserRepository(DATA_SOURCE);
Optional<User> user = userRepository.findByLogin("oleh.marchenko");
System.out.println(user);
}
}
But this example is too simple. More complex examples can be found in the graphine-test module.
Graphine is Open Source software released under the Apache 2.0 license.