Lightweight ORM for Java, providing:
.select(...).where(...)
dialectModel represents the model of a specific database schema. It comprises entities (tables) as long as attributes (named user-defined queries).
There are several ways of configuring and initializing a model, but you could do:
Model bookstore = new Model("bookstore"); bookstore.setDatasource("jndi/my_bookstore").setReverseMode("joins").initialize();
Entities represent the business logic concepts of the model. They can be reverse enginered from the working schema tables, and can be associated with a user provided POJO class.
For instance, let’s get the entity for our “books” table:
Entity books = bookstore.getEntity("books");
Instances are individual representations of entities, with specific values, with typed getters. They often correspond to a table row values.
When they belong to an entity with a primary key:
entity.fetch(PK values...)
and created using entity.newInstance({optional initial values map})
insert()
, update()
, upsert()
, delete()
, refresh()
methodsLet’s do some CRUD operations
Instance mobyDick = books.newInstance(); mobyDick.put("title", "Moby Dick"); mobyDick.insert(); long id = mobyDick.getLong("book_id"); ... Instance check = books.fetch(id); assertEquals("Moby Dick", check.getString("title")); check.put("title", "The Whale"); check.update(); ... check.delete();
Attributes are named SQL queries which appear as properties of the model itself (root attributes) or properties of a specific entity instances. There are three types of attributes:
evaluate()
method, returning a scalar (string, number, boolean).retrieve()
method, returning an instance.query()
method, returning an iterator over instances.Join attributes (aka $book.author
) can be reverse enginered : 1-n for joins
reverse mode and both 1-1 and n-n for extended
reverse mode.
Other attributes are defined via the XML model definition file. Row and rowset attributes can be given a result
XML attribute, referencing an entity of the model. This way, you can chain such attributes, for instance:
$book.author.birth_country.name
Let’s define a new root attribute to get all the books published after a certain date in the XML model definition file
<rowset name="published_after" result="books"> SELECT * FROM books WHERE publication_date > DATE <publication_date/>; </rowset>
and use this attribute from java:
Iterator<Instance> bookIterator = model.query("published_after", "2018-01-01"); while (bookIterator.hasNext()) { Instance book = bookIterator.next(); System.out.println(book.getString("title") + " / published on " + dateFormat.format(book.getDate("publication_date"))) }
Let’s define a new book attribute returning the number of total books for the book author
<books> <scalar name="same_author_count"> SELECT count(*) FROM books WHERE author_id = <author_id/> </scalar> </books>
and use this attribute from java:
int melvilleBooksCount = mobyDick.evaluateInteger("same_author_count");
Actions, corresponding to the perform()
method, are named SQL queries performing an atomic or transactionnal database change.
Actions with more than one statement are Transactions. Actions return the number of changed database rows.
Each of the above methods can be invoked from the model object itself or from an instance, and can take additional query parameter arguments (or a { name => value } map of those arguments).
Manual transactions can be performed via the Model.attempt()
method which expects a ModelRunnable functional object and handles commit and rollback operations:
try {
model.attempt(() -> {
... crud operations ...
});
} catch(SQLException sqle) {
... handle error ...
}
Here’s the Javadoc (wip).
You can either:
extended
to even get standard n-n joins reverse enginered) of a database schema (implicit method). In this case, an entity is
created for each table in the schema, plus optionally two attributes at both ends of each join.Configuration can take place:
Map
given to Model.configure(Map)
or by caling specific Model configuration setters.modality.properties
(prefixed by model.
), if you use the Velocity Engine. This affects all models. In a web context,
this file is typically located in /WEB-INF/
.tools.xml
tools’s attribute (without the model.
prefix), if you use VelocityTools. This affects a specific ModelTool.model.xml
, the model definition file (see below), as XML attributes of the <model>
tag, without the model.
prefix. This affects all tools using this ModelTool.The model definition itself (model.xml
) defines the accessible model objects and gather the SQL code - it is distinct from the above configuration (model.properties
),
which defines the model generic behavior. This definition contains all entities (which can or not correspond to a database table), attributes (sql queries returning a scalar, a row or rowset)
and actions (single or multi modification statements, always kept within a single transaction).
Small catch: in model.xml
, you will have to escape the <
character into <
.
Instances classes can use the generic Instance
class, or be any Java POJO with standard getters and setters.
Models can be given a string id, allowing them to be accessed in a static Java context (using the ModelRepository).
Once configured, the model needs to be initialized against a definition file, named by default model.xm
.
If a path is given using the configuration key model.definition
, it will be searched for instead of the default. It is searched:
You can get an existing model:
[TODO - add more examples]
Filters mappings associate a table or column name or pattern (or a Java or SQL data type) to a filter which can be:
If a stock filter name is prefixed with ‘-‘, the corresponding filter is never applied to the specified mapping.
Filters can be used to transform:
Apart from custom closures, filters can be defined from within modality.properties
(or from a schema-specific model properties file).
Available stock filters: lowercase
, uppercase
, snake_to_camel
, plural_en
Examples:
# use lowercase everywhere, pluralize tables names
model.identifiers.mapping.* = lowercase, plural_en
model.identifiers.mapping.*.* = lowercase
# remove foo_ prefix from table names
model.identifiers.mapping.foo_* = /foo_(.*)/$1/
Available stock filters: lowercase
, uppercase
, calendar_to_date
, date_to_calendar
, number_to_boolean
, obfuscate
, deobfuscate
, base64_encode
, base64_decode
, mask
, no_html
, escape_html
Examples:
# Work with Calendar objects, let the database see Date objects
model.filters.write.java.util.calendar = calendar_to_date
model.filters.read.java.sql.Date = date_to_calendar
# Never return the users.password field
model.filters.read.user.password = mask
# disallow html by default in all fields
model.filters.write.*.* = no_html
# but allow html in fields prefixed by "html_"
model.filters.write.*.html_* = -no_html
# and escape html at rendering for those field
model.filters.read.*.html_* = escape_html
Modality provides a basic schema versioning feature.
Migration scripts are specific to each database schema. They are searched in the migations/$modelId
directory, which can be changed using the model.migration_scripts
configuration property.
Scripts are supposed to be alphanumerically ordered, with .sql
extension (typically named 001_do_something.sql
).
This location is searched:
WEB-INF
directory when running in a J2EE webapp contextWEB-INF
when running in a J2EE webapp context)When found, the following happens at the end of model initialization:
model_version
table in the target schema if it doesn’t exist, containing a single varchar(200)
script
field, which is meant to receive all the successfully applied scripts filenames (without path).model_version
table.That’s it. No checksum tests as in Flyway or Liquibase, which I found rather counterproductive with time.
Be aware that when using an engine which is not able to handle rollbacks of DDL statements (like mysql
and mariadb
), you will have to manually revert those if something wrong happens.
When updating a schema, if you are maintaining global creation scripts, you would typically add the model_version
table and populate it with the considered scripts.
Note that for now, Modality requires the target schema to already exist.