Skip to content
tavoda edited this page Feb 21, 2013 · 1 revision
        <h1><a name="3.AdvancedTutorial%28CSC%29-SculptorAdvancedTutorial"></a>Sculptor Advanced Tutorial</h1>

This tutorial describes the features of Sculptor business tier. It presents how Sculptor works out-of-the-box, customization is often needed and that is the topic of the Developer's Guide.

Before you start you must follow the instructions in the Installation Guide. It is also recommended that you try the Hello World Tutorial to get a feeling of the environment.

Table of Contents:

Library Example

The example used in this tutorial is a simple system for a library of movies and books. It is "over designed" compared with the simple functionality it provides. The reason for this is to be able to illustrate many code generation ideas.

The core of the system is a Domain Model, see Figure 1. A Library consists of PhysicalMedia. Books and Movies are different types of Media, which are stored on a PhysicalMedia, e.g. DVD, VHS, paper books, eBooks on CD. A Media has Characters, e.g. James Bond, which can be played by a Person, e.g. Pierce Brosnan. A person can be involved (Engagement) in different Media, actually a Person can have several Engagements in the same Media. E.g. Quentin Tarantino is both actor and director in the movie 'Reservoir Dogs'.


Figure 1. Domain model of the Library example. This diagram is generated by Sculptor

Setup Project

In this part we will setup the project structure for maven and eclipse.

1. Use the following command (one line) to create a maven pom and file structure.

mvn archetype:generate -DarchetypeGroupId=org.fornax.cartridges -DarchetypeArtifactId=fornax-cartridges-sculptor-archetype-standalone -DarchetypeVersion=2.1.0 -DarchetypeRepository=http://fornax-platform.org/nexus/content/repositories/public
Fill in groupId and archetypeId:
Define value for groupId: : org.library
Define value for artifactId: : library
Define value for version:  1.0-SNAPSHOT: :
Define value for package:  org.library: :

2. In the new directory, run mvn eclipse:eclipse to create an Eclipse project with the same dependencies as in the pom.

3. Open Eclipse and import the project.

Generate Code

In this part we will write a Sculptor DSL file and generate code from it.

1. Modify the file named model.btdesign in the folder
src/main/resources/model

2. Open the design file with Scupltor DSL editor, double-click on it.
Add the following code to the design file. You can see that it defines two Modules, containing one Service each. The operations in the Services delegates directly to the Repositories. It defines the same Domain Objects, including attributes and references, as in Figure 1. The details will be explained further on.

model.btdesign
Application Library {
    basePackage = org.library
Module media {

    Service LibraryService {
      findLibraryByName =&gt; LibraryRepository.findLibraryByName;
      findMediaByName =&gt; MediaRepository.findMediaByName;
      findMediaByCharacter =&gt; MediaRepository.findMediaByCharacter;
      findPersonByName =&gt; PersonService.findPersonByName;
    }

    Entity Library {
      scaffold
      <span class="code-object">String</span> name key
      - Set&lt;@PhysicalMedia&gt; media &lt;-&gt; library

      Repository LibraryRepository {
        findByQuery;
        @Library findLibraryByName(<span class="code-object">String</span> name) <span class="code-keyword">throws</span> LibraryNotFoundException;
      }
    }

    Entity PhysicalMedia {
      scaffold
      <span class="code-object">String</span> status length=<span class="code-quote">"3"</span>
      <span class="code-object">String</span> location
      - @Library library nullable &lt;-&gt; media
      - Set&lt;@Media&gt; media &lt;-&gt; physicalMedia
    }

    Service MediaService {
      findAll =&gt; MediaRepository.findAll;
    }

    <span class="code-keyword">abstract</span> Entity Media {
      gap
      <span class="code-object">String</span> title !changeable
      - Set&lt;@PhysicalMedia&gt; physicalMedia inverse &lt;-&gt; media
      - Set&lt;@Engagement&gt; engagements cascade=<span class="code-quote">"all-delete-orphan"</span> &lt;-&gt; media
      - Set&lt;@MediaCharacter&gt; mediaCharacters &lt;-&gt; existsInMedia

      Repository MediaRepository {
        &gt; @MediaCharacterRepository
        <span class="code-object">int</span> getNumberOfMovies(<span class="code-object">Long</span> libraryId) =&gt; AccessObject;
        List&lt;@Media&gt; findMediaByCharacter(<span class="code-object">Long</span> libraryId, <span class="code-object">String</span> characterName);
        findById;
        save;
        findAll;
        findByQuery;
        <span class="code-keyword">protected</span> findByKeys(Set&lt;<span class="code-object">String</span>&gt; keys, <span class="code-object">String</span> keyPropertyName, <span class="code-object">Class</span> persistentClass);
        List&lt;@Media&gt; findMediaByName(<span class="code-object">Long</span> libraryId, <span class="code-object">String</span> name);
        Map&lt;<span class="code-object">String</span>, @Movie&gt; findMovieByUrlIMDB(Set&lt;<span class="code-object">String</span>&gt; keys);
      }
    }

    Entity Book <span class="code-keyword">extends</span> @Media {
      !auditable
      <span class="code-object">String</span> isbn key length=<span class="code-quote">"20"</span>
    }

    Entity Movie <span class="code-keyword">extends</span> @Media {
      !auditable
      <span class="code-object">String</span> urlIMDB key
      <span class="code-object">Integer</span> playLength
      - @Genre category nullable
    }

    <span class="code-keyword">enum</span> Genre {
        ACTION,
        COMEDY,
        DRAMA,
        SCI_FI
    }

    ValueObject Engagement {
      <span class="code-object">String</span> role
      - @Person person
      - @Media media &lt;-&gt; engagements
    }

    Service MediaCharacterService {
      findAll =&gt; MediaCharacterRepository.findAll;
    }

    ValueObject MediaCharacter {
      <span class="code-object">String</span> name !changeable
      - Set&lt;@Person&gt; playedBy
      - Set&lt;@Media&gt; existsInMedia &lt;-&gt; mediaCharacters

      Repository MediaCharacterRepository {
        findByQuery;
        findAll;
      }
    }
}


Module person {
    Service PersonService {
      findPersonByName =&gt; PersonRepository.findPersonByName;
    }

    Entity Person {
      gap
      scaffold
      Date birthDate past
      - @Gender sex !changeable
      - @Ssn ssn key
      - @PersonName name

      Repository PersonRepository {
          List&lt;@Person&gt; findPersonByName(<span class="code-object">String</span> name) =&gt; AccessObject;
          save;
          save(Collection&lt;@Person&gt; entities);
          findByQuery;
          findByExample;
          findByKeys;
      }
    }

    BasicType Ssn {
      <span class="code-object">String</span> number key length=<span class="code-quote">"20"</span>
      - @Country country key
    }

    BasicType PersonName {
      <span class="code-object">String</span> first
      <span class="code-object">String</span> last
    }

    <span class="code-keyword">enum</span> Gender {
        FEMALE(<span class="code-quote">"F"</span>),
        MALE(<span class="code-quote">"M"</span>)
    }

    <span class="code-keyword">enum</span> Country {
        <span class="code-object">String</span> alpha2 key
        <span class="code-object">String</span> alpha3
        <span class="code-object">int</span> numeric
        SWEDEN(<span class="code-quote">"SE"</span>, <span class="code-quote">"SWE"</span>, <span class="code-quote">"752"</span>),
        NORWAY(<span class="code-quote">"NO"</span>, <span class="code-quote">"NOR"</span>, <span class="code-quote">"578"</span>),
        DENMARK(<span class="code-quote">"DK"</span>, <span class="code-quote">"DNK"</span>, <span class="code-quote">"208"</span>),
        US(<span class="code-quote">"US"</span>, <span class="code-quote">"USA"</span>, <span class="code-quote">"840"</span>)
    }
}

}

3. Run mvn clean install to generate code and build. The JUnit test will fail.

Domain-Driven Design

Now it is time to explain the fundamental Domain-Driven Design concepts in the DSL. If you don't have the book you can download and read more in DDD Quickly. It is highly recommended that you study one of these books, at least the concepts mentioned in this part of the tutorial. The below quotes are from DDD Quickly.

Domain Objects

In the DSL there are three types of Domain Objects.

Entity

Entities have an identity and the state of the object may change during the lifecycle.

"There is a category of objects which seem to have an identity,
which remains the same throughout the states of the software.
For these objects it is not the attributes which matter, but a
thread of continuity and identity, which spans the life of a
system and can extend beyond it. Such objects are called Entities."

ValueObject

For Value Objects the values of the attributes are interesting, and not which object it is. Value Objects are typically immutable.

"There are cases when we need to contain some attributes of a
domain element. We are not interested in which object it is, but
what attributes it has. An object that is used to describe certain
aspects of a domain, and which does not have identity, is named
Value Object."

Note that Value Object is not the same as Data Transfer Object. Core J2EE patterns caused a lot of confusion when they used the term Value Object. They have renamed it to Transfer Object.

BasicType

BasicType is typically used for fundamental types, for example different quantities such as Money,
or range of values.

BasicType is a ValueObject which is stored in the same table as the Domain Object referencing it. It corresponds to JPA @Embeddable.

Aggregate

"An Aggregate is a group of
associated objects which are considered as one unit with regard
to data changes. The Aggregate is demarcated by a boundary
which separates the objects inside from those outside. Each
Aggregate has one root. The root is an Entity, and it is the only
object accessible from outside. The root can hold references to
any of the aggregate objects, and the other objects can hold
references to each other, but an outside object can hold
references only to the root object."

With Sculptor each Entity is by default an aggregate root, but you can use !aggregateRoot to define that it is not. Sculptor will validate the reference constraints described in the quote above.

Repository

A repository is used for:

  • retrieving domain objects from the underlying database
  • persisting new domain objects
  • deleting domain objects

The interface of the Repository should always speak the Ubiquitous Language of the domain. It provides domain centric operations to the client. Repositories provide controlled access to the underlying data in the sense that it exposes only the Aggregate roots of the domain model.

"Databases are part of the infrastructure. A poor solution is for
the client to be aware of the details needed to access a database.
...
Therefore, use a Repository, the purpose of which is to
encapsulate all the logic needed to obtain object references. The
domain objects won't have to deal with the infrastructure to get
the needed references to other objects of the domain. They will
just get them from the Repository and the model is regaining its
clarity and focus."

You might feel confused regarding the difference between DAOs and repositories. The DAO is at a lower level of abstraction than the Repository and can contain plumbing codes to pull out data from the database. We typically have one DAO per database table, but one repository per domain type or aggregate.

I can recommend that you read Inject Repositories, not DAOs in Domain Entities.

Service

The Services act as a Service Layer around the domain model. It provides a well defined interface with a set of available operations to the clients.

The transaction boundary is at the service layer.

Module

"Modules are used as a method of organizing related concepts
and tasks in order to reduce complexity.
...
Another reason for using modules is related to code quality. It is
widely accepted that software code should have a high level of
cohesion and a low level of coupling."

With Sculptor the Modules are realized as Java packages. By default they are subpackages of the application package, but it is possible to define another package by specifying the basePackage attribute of the Module.

Circular references between Modules are not allowed. Interaction between a Service in one Module and a Repository in another Module is not allowed, it must go via a Service. Sculptor will validate these constraints.

How to Generate Domain Objects

In the context of Sculptor, Domain Object is a common term for Entity, ValueObject and BasicType.

Gap Class

Separation of generated and manually written code is done by a generated base class and manually written subclass, a gap class. It is in the subclass you add methods to implement the behavior of the Domain Object. The subclass is also generated, but only once, it will never be overwritten by the generator. You can of course remove it to regenerate it.

The gap class is not generated initially. When you need a gap class you specify that in the DSL with gap.

    Entity Person {
        gap
        Date birthDate
        - @Gender sex !changeable
        - @Ssn ssn key
        - @PersonName name
    }

Note that in the gap class some annotations are specified, e.g. @Entity and @Table. Those are only generated once, and must be maintained manually. E.g. when changing the natural key attributes the @UniqueConstraint must be modified. In the JavaDoc of the generated base class the correct annotations are defined, for convenient copy to the hand written subclass.

It is possible to configure that gap classes are always to be generated, except when specified otherwise. Then you add the following property in sculptor-generator.properties:

generate.gapClass=true

In the DSL you can specify that a gap class is not needed:

    BasicType PersonName {
        nogap
        String first
        String last
    }

Operations

Domain Objects can of course contain behavior, otherwise it wouldn't be a rich domain model. However, the behavior logic is always written manually in the gap class or in a trait. You can write the methods directly in the java gap class. Optionally you can define the signature of important methods in model.btdesign. You still have to implement it manually in the gap class. The advantage of defining the operation in the model is that then it is included in generated documentation and diagrams.

Operations are defined with a java like syntax. To mark it as an operation you must use def or * in front of it. It must end with ;.

    Entity Person {
        Date birthDate
        def int age;
        - @PersonName name
        def changeName(String first, String last);
        - Set<@Person> children
        def List<@Person> getChildrenSortedByAge;
    }

Traits

Traits provide a mixin composition mechanism that is missing in Java. Similar to interfaces in Java, traits are used to define object types by specifying the signature of the supported methods. Unlike interfaces, traits can be partially implemented; i.e. it is possible to define implementations for some methods. Sculptor has support for traits, inspired by Scala traits, but with some limitations compared to Scala traits.

Let us say that the Library system also should be used to sell products.

        Entity Book extends Media with Product {
            String isbn
            int pages
        }
    Trait Product {
        <span class="code-object">String</span> title
        def <span class="code-object">int</span> price();
        def <span class="code-keyword">abstract</span> <span class="code-object">int</span> priceFactor();
    }

This means that you can implement the logic for the price operation in one place, in the trait class. It will be available in classes that mixin the trait. Book with Product. The internal implementation is based on delegation, no magic.

Attributes and references in the trait will be mixed in to the domain object, i.e. stored together with the Book entity.

abstract priceFactor in the example above means that that operation has to be implemented in the domain object containing the trait, i.e. Book gap class.

Attributes, references and operations can be defined as protected if you for example don't want to expose them in the interface of the trait.

It is possible to mixin several traits.

        Entity Person with Worker with Student {
        }
    Trait Worker {
        <span class="code-object">String</span> company
        def <span class="code-object">boolean</span> busy(Date day);
    }

    Trait Student {
        <span class="code-object">String</span> school
        def <span class="code-object">boolean</span> busy(Date day);
    }

In the case the same operation, attribute or reference is defined in several of the traits the last one will dominate. In this example, if you invoke the busy method on a Person instance it will end up in the Student implementation. There is no support for stackable traits, i.e. to be able to call super from one trait and that will invoke next trait. In the above example that would have been useful, since you would probably want to check the schedule for both Student and Worker.

There is a special self() method that you can use if you from the trait need to get hold of the instance containing the trait.

Another example of traits is illustrated in this article.

Attributes and References

Domain Objects can have simple attributes and references to other Domain Objects and enums.

Primitive types or any fully qualified Java class can be used as type of the attributes. Built in types:

  • String
  • Integer (int), Long (long), Float (float), Double (double)
  • BigDecimal
  • Boolean (boolean)
  • Date
  • DateTime, Timestamp
  • Clob
  • Blob

It is easy to add your own DSL types and mapping to database and Java types. See Developer's Guide. Sculptor supports Joda Time instead of the Java date and time classes. It is also described in the Developer's Guide how to activate Joda Time.

To distinguish references from simple attributes, declarations of references starts with a -. In the same way as in other places you must also use an @ in front of the declaration when referring to a Domain Object. When the relation is one-to-many or many-to-many you define a collection as the type of the reference. Bidirectional associations are defined with the opposite <-> syntax.

Entity PhysicalMedia {
    String status
    String location
    - @Library library <-> media
    - Set<@Media> media <-> physicalMedia
}

There is an alternative notation for references and bidirectional. Instead of - you can use reference and instead of <-> you can use opposite.

Collections

Supported collection types:

  • Set - unordered collection
  • List - ordered collection using order column (additional options available for JPA2, see below)
  • Bag - ordered collection using orderby sorting

For Bag collections you can specify orderby

ValueObject MediaCharacter {
    String name !changeable
    - Bag<@Person> playedBy orderby="birthDate asc"
    - Set<@Media> existsInMedia <-> characters
}

For bi-directional many-to-many associations it is possible to define the JPA/Hibernate inverse side.

abstract Entity Media {
    String title !changeable
    - Set<@PhysicalMedia> physicalMedia inverse <-> media

Associations with cardinality "many" (Set, Bag, List) that are not bidirectional are by default generated as many-to-many, with a separate relation table.

Entity Library {
    String name key
    - Set<@PhysicalMedia> physicalMedia
}

By defining the reference as inverse it will be generated as an ordinary foreign key in the child table, i.e. an one-to-many relation.

Entity Library {
    String name key
    - Set<@PhysicalMedia> physicalMedia inverse
}

In a JPA2 environment there are a few new possibilities sorting a List collection.
For List collections you can now specify orderby="properties". properties is a string that contains one or more comma-separated attributes of the object to be ordered or in a referenced BasicType or Entity.

Entity Library {
    - List<@Person> persons orderby="name.last desc, name.first asc"
}

To define a persistently ordered List you can specify orderColumn="columnName". columnName is contains the name of the database column that is added, to store the order. columnName is optional. If not given, a default name is used (reference name + "_INDEX").

Entity Library {
    - List<@Person> persons orderColumn="PERSONS_ORDER"
}

You can use orderby or orderColumn, but not both. It is possible not to specify a special order. The result is a default order. If the List references Entities, the List will be ordered by the primary keys. In other cases the ordering is undefined.

Element Collections

This feature correspondents to the JPA @ElementCollection.

In addition to define collections (Set, List) as relationships to other entities, it is possible to specify collections of BasicTypes and simple types (String, ...). This elements will be stored in a separate collection table. This collection table is a kind of one-to-many relation (aggregation) with a join column that refers to the containing entity table.

Example using a collection of Strings.

Entity Person {
    Set<String> nicknames
}

Example using a BasicType.

Entity Spy extends Person {
    - Set<@PersonName> aliases
}

In both cases the contents of the collection will be stored in a collection table automatically.
If the collection should not be persistent use the transient keyword.

Entity Person {
    Set<String> nicknames transient
}

Key

equals and hashCode requires some thought when used with JPA/Hibernate, see the discussion at the Hibernate site. Sculptor takes care of the details. You only have to mark the attributes that is the natural key of the Domain Object, or if there is no natural key Sculptor will generate a UUID automatically.

If the key is a composite key, consisting of several attributes, a separate internal key class will be generated.

It is also possible to use a separate BasicType object as key. This key object may have one or several attributes marked as key. Some keys are important objects in a domain model and may contain logic. It is also more clarifying to pass around a real type, instead of a String or Integer, e.g. AccountNumber.

Entity Person {
  Integer age
  - @Ssn ssn key
  - @PersonName name
}

BasicType Ssn { String number key String country key }

All persistent Entities and Value Objects will also have a surrogate id attribute, which is the primary key in the database.

Changeable

Attributes and References can be defined as changeable or not. By default Entities are mutable, and ValueObjects and BasicType immutable. ValueObjects and BasicTypes can be defined as !immutable to be changeable. Individual Attributes and References can be declared as !changeable. Natural keys are never changeable.

ValueObject ChangeableColor {
    !immutable
    String name !changeable;
    int red;
    int green;
    int blue;
}

Attributes and References that are defined as !changeable are included in the constructor and have no setter method.

There is an alternative notation for !. Instead of !immutable you can use not immutable.

Required

As a complement to changeable you can use required for Attributes and References that are to be included in the constructor, but still have a setter method.

ValueObject ChangeableColor {
    !immutable
    String name !changeable;
    int red required;
    int green required;
    int blue required;
}

Nullable

An Attribute can be defined as nullable which means that it may have null as value when stored in database, i.e. no NOT NULL declaration for the database column. By default Attributes are not nullable.

Copy mutator

It may feel inconvenient to use constructors with many parameters for immutable ValueObjects. Therefore copy mutator methods with a fluent interface are generated in immutable ValueObjects.

    ValueObject Address {
        String street
        String streetNumber nullable
        String city nullable
        String zipCode nullable
        - @Country country nullable
    }

The above Address may be constructed like this:

    new Address("Drottninggatan")
        .withStreetNumber("17")
        .withCity("Stockholm")
        .withZipCode("10101")
        .withCountry(Country.SWEDEN);

Domain Object Builders

Sculptor generates a builder class for each domain object. The builder class provides a fluent interface to build domain objects in a manner that can be easier to work with and read. The following example additionally uses static imports to make the builder code a bit more succinct.

import static org.fornax.cartridges.sculptor.examples.library.media.domain.BookBuilder.book;
import static org.fornax.cartridges.sculptor.examples.library.media.domain.MediaCharacterBuilder.mediaCharacter;
...
    Book book = book()
        .createdBy("me")
        .createdDate(now)
        .title("Ender's Game")
        .isbn("Some-ISBN")
        .addMediaCharacter(mediaCharacter()
            .name("Ender")
            .build())
        .build();

The builder can also be useful to get around restrictions on access to no-argument constructors and changing immutable attributes.

BookBuilder bookBuilder = new BookBuilder();

bookBuilder.createdBy(currentUserId) .createdDate(now) .title(title) .isbn(isbn);

if( isSpecialCase(bookBuilder) ) { bookBuilder.isbn(SPECIAL_ISBN); }

Book book = bookBuilder.build();

Generation of domain object builder classes is an optional feature that is enabled by default, but can be disabled via the generate.domainObject.builder property.

generate.domainObject.builder=false

By default the builder classes are generated in the domain package. To generate the builder classes in a different package use the package.builder property.

package.builder=builder

Validation

Sculptor supports bean validation by adding Hibernate Validator annotations to the Domain Objects.

To validate a Domain Object add validation constraints to the model. You can add constraints to Entities, Attributes and References.

Entity Person {
    Date birthDate past
    String ssn key length="15"
    String country key length="2" pattern="'[DE|SE|US]'"
    Integer age nullable min="18,'must be an adult'"
    - Set<@Address> addresses notEmpty size="min=1,message='at least 1 address is needed'"
}

All built-in Hibernate validation constraints are supported by keywords. Each keyword can be used in several ways, e.g.

     range="1"
     range="1,10"
     range="1,10,'must be between 1 - 10'"

or qualified like

     range="min=1"
     range="min='1',max=10"
     range="min='1',max=10,message='must be between 1 - 10'"

If you want to omit a parameter you have to use the qualified constraint like

    range="max=10"

It's also possible to define custom validation annotations. Create your own constraints and add it to the model via the validate keyword.

Entity Person {
    Date birthDate past validate="@customDateValidation(message='date violation')"
}

You can use fully qualified constraints or shortcuts. If want to use a shortcut add it to your sculptor-generator.properties, e.g.

validation.annotation.CustomDateValidation=foo.bar.CustomDateValidation

Of course combinations of built-in and custom constraints and multiple custom constraints are possible.

Sculptor will automatically validate Domain Objects on every save operation (create or update). In case of a constraint violation a ValidationException is thrown, which gives you detailed information about the cause. If you need more control over the validation process, a validation by hand is supported by using the frameworks DomainObjectValidator class.

It is also possible to define validation at the class level, which is useful for validation of related properties, associations, or aggregate of objects.

Entity Person {
    validate="@foo.bar.CustomValidation"
    String name
    Date birthDate
}

An alternative way of implementing class level validation is to trigger your own validation method using JPA @PrePersist and @PreUpdate annotations.

Movie.java
    @PreUpdate
    @PrePersist
    public void validatePlayLength() {
        if (getPlayLength() != null && Genre.SHORT.equals(getCategory()) && getPlayLength() > 15) {
            throw new ValidationException(
                    "Short movies should be less than 15 minutes");
        }
    }

Auditable

Entities are by default auditable, which means that when the objects are saved an interceptor will automatically update properties 'lastUpdated', 'lastUpdatedBy', 'createdDate' and 'createdBy'. These attributes are automatically added for auditable Domain Objects. You can turn off auditing for an Entity with !auditable.

Entity Book extends Media {
    !auditable
    String isbn key
}

Optimistic Locking

By default a version attribute is automatically added to each Entity and mutable persistent ValueObject. This is used for optimistic locking checks by Hibernate.
You can skip this feature by specifying !optimisticLocking for the Domain Object.

Entity Book extends Media {
    !optimisticLocking
    !auditable
    String isbn key
}

Aggregate

By default all Entities are considered to be aggregate roots, but you can use !aggregateRoot to specify that the Entity is part of an aggregate and not the root of it. The default values for cascade and fetch features take the aggregate into account.

Read more about the example below in the Domain-Driven Design book, page 134.

Entity PurchaseOrder {
    - @Money approvedLimit
    - List<@PurchaseOrderLineItem> items
}

Entity PurchaseOrderLineItem { !aggregateRoot Integer quantity - @Money price - @Part part }

Entity Part { - @Money price }

Basic Type

A BasicType corresponds to JPA @Embeddable / Hibernate Component, i.e. the properties are stored in the same table as the parent. It makes it easy to create object types.

In the Library example PersonName is an example of a BasicType. Look at annotations of the PersonName class.

A classical example is Money.
BasicType Money {
    String currency;
    BigDecimal amount;
}

Entity Account { String accountNumber key; - @Money balance; }

ValueObject Transaction { DateTime timePoint; - @Money money; - @Account debitAccount; - @Account creditAccount; }

BasicTypes can contain nested BasicTypes, relationships to Entities and collections of simple types, BasicTypes and Entities.

Entity Person {
    - @Contact contact;
}

Entity Phone { String type; String phoneNumber; }

BasicType Contact { - Set<@Phone> phones - @Address address }

BasicType PersonName { String firstName; String lastName; }

BasicType Address { - String city; }

Non-persistent ValueObject

It is possible to specify that a ValueObject is not persistent, i.e. not stored in database. This is for example useful for some parameters and return values for service operations. It can also be used for domain objects containing only logic, without any persistent state. E.g. an algorithm you want to make explicit.

Service PersonService {
    Set<@Person> findPersonsMatching(@PersonCriteria personCriteria);
}

ValueObject PersonCriteria { !persistent String name String country - @AgeInterval ageBetween }

BasicType AgeInterval { Integer minAge Integer maxAge }

Enum

Enums are defined like this:

enum Genre {
    ACTION,
    COMEDY,
    DRAMA,
    SCI_FI
}

Entity Movie extends @Media { !auditable String urlIMDB key Integer playLength

  • @Genre category nullable }

An enum can have attributes. One attribute may be marked as key and that value will be stored in the database as identifier of the enum value. Otherwise the name of the enum values are stored.

enum Gender {
    String code key
    FEMALE("F"),
    MALE("M")
}

It is possible to define an implicit value without defining an attribute. This attribute is automatically named 'value' and can be of String or int type. This attribute will be stored in database as identifier for the enum.

enum Gender {
    FEMALE("F"),
    MALE("M")
}

An example with several attributes:

enum WindSpeed {
    int beaufortNumber key;
    double minMps;
    double maxMps;
CALM(1, <span class="code-quote">"0"</span>, <span class="code-quote">"0.2"</span>),
STORM(10, <span class="code-quote">"24.5"</span>, <span class="code-quote">"28.4"</span>),
HURRICAN(12, <span class="code-quote">"32.7"</span>, <span class="code-quote">"40.8"</span>);

}

The needed length for database storage will be calculated automatically. It's possible to override this calculation by adding hint="databaseLength=length" to the enum definition.

enum Gender {
    hint="databaseLength=10"
    FEMALE("F"),
    MALE("M")
}

It is also possible to store the ordinal value representing the enum instead of the name. Use ordinal to do this. ordinal is only allowed for enums without attributes or without an identifier (key) attribute.

enum Genre {
    ordinal
    ACTION,
    COMEDY,
    DRAMA,
    SCI_FI
}
JPA
JPA has a built in support for enums. It is recommend to use this default handling to avoid provider specific generation of code and configuration files. Therefore try not to use enums that have an identifier (key) attribute.

Inheritance

An Entity may extend another Entity. A ValueObject may extend another ValueObject. Inheritance is not supported for BasicType. By default JOINED inheritance strategy is used, i.e. fields that are specific to a subclass are mapped to a separate table than the fields that are common to the parent class.

abstract Entity Project {
    String name key
}

Entity LargeProject extends @Project { BigDecimal budget nullable }

You can also use SINGLE_TABLE strategy, i.e. a single table per class hierarchy.

abstract Entity Project {
    inheritanceType=SINGLE_TABLE
    String name key
}

Entity LargeProject extends @Project { BigDecimal budget nullable }

When using SINGLE_TABLE you can also define several things to customize the mapping, similar to JPA.

abstract Entity Project {
    inheritanceType=SINGLE_TABLE
    discriminatorType=CHAR
    discriminatorColumn="TYPE"
    String name key
}

Entity LargeProject extends @Project { discriminatorValue="L" BigDecimal budget nullable }

Package

By default the Domain Objects are located in a package named domain, which is a sub-package to the package of the module. However, it is possible to specify another package name for an individual Domain Object.

ValueObject PersonCriteria {
    package=param
    !persistent
    String name
    String country
    - @AgeInterval ageBetween
}

Cache

Domain Objects and collection references can be defined to be cached by JPA/Hibernate second level cache. EhCache is the default cache provider. It is described in the Developer's Guide how to change to another cache provider.

ValueObject Engagement {
  cache
  String role
  - @Person person
  - @Media media <-> engagements
}

abstract Entity Media { String title !changeable

  • Set<@PhysicalMedia> physicalMedia <-> media
  • Set<@Engagement> engagements cache <-> media
  • Set<@MediaCharacter> characters <-> existsInMedia }

Database Definitions

The names of database tables and columns are normally derived from the names of the domain objects, but it is possible to specify other names by using databaseTable and databaseColumn attributes.

Entity PhysicalMedia {
    databaseTable="PHMED"
    String status databaseColumn="STAT"
    String location databaseColumn="LOC"
    - @Library library databaseColumn="LIB" <-> media
    - Set<@Media> media databaseColumn="MED" <-> physicalMedia
}

It is possible to define many-to-many join table with databaseJoinTable and its columns with databaseColumn at both sides of the bidirectional association:

    - Set<@Media> existsInMedia databaseJoinTable="MED_CHR" databaseColumn="CHR" <-> mediaCharacters

Those keywords are also useful for unidirectional to-many associations. Additionally databaseJoinColumn is used, since there is no opposite side to define the column on.

    - Set<@Person> playedBy databaseJoinTable="CHR_PERS" databaseColumn="PERS" databaseJoinColumn="CHR"

For Attributes you can define nullable, index, databaseColumn, databaseType and length, which all
relates to the database definition.

Entity Person {
    String ssn key length="15"
    String country key databaseType="CHAR" length="2"
    Integer age nullable
    - @PersonName name
}

BasicType PersonName { String first index String last index }

Fetch

For References you can define fetch which corresponds to JPA/Hibernate feature.

ValueObject MediaCharacter {
    String name !changeable
    - List<@Person> playedBy fetch="eager"
    - Set<@Media> existsInMedia <-> characters
}

Possible values for fetch:

fetch= Generated feature
eager javax.persistence.FetchType.EAGER
join javax.persistence.FetchType.EAGER
lazy javax.persistence.FetchType.LAZY
subselect org.hibernate.annotations.FetchMode.SUBSELECT

Cascade

For References you can define cascade which corresponds to JPA/Hibernate feature.

ValueObject MediaCharacter {
    String name !changeable
    - List<@Person> playedBy cascade="persist,merge"
    - Set<@Media> existsInMedia <-> characters
}

Possible values for cascade:

cascade= Implementation type
persist javax.persistence.CascadeType.PERSIST
merge javax.persistence.CascadeType.MERGE
remove javax.persistence.CascadeType.REMOVE
refresh javax.persistence.CascadeType.REFRESH
all javax.persistence.CascadeType.ALL
all-delete-orphan javax.persistence.CascadeType.ALL, org.hibernate.annotations.CascadeType.DELETE_ORPHAN
delete-orphan org.hibernate.annotations.CascadeType.DELETE_ORPHAN
delete org.hibernate.annotations.CascadeType.DELETE
save-update org.hibernate.annotations.CascadeType.SAVE_UPDATE
evict org.hibernate.annotations.CascadeType.EVICT
replicate org.hibernate.annotations.CascadeType.REPLICATE
lock org.hibernate.annotations.CascadeType.LOCK

Several cascade types can be defined by separating them with comma, e.g. cascade="persist,merge".

The default value for cascade and fetch takes the aggregate and module into account. When cascade is not explicitly defined in the DSL the convention is:

  • all-delete-orphan is used if both Entities are in the same Aggregate.
  • all is used if both Entities are in the same Module.

Default values for cascade can be defined in sculptor-generator.properties, see Developer's Guide.

Try the different features of the Domain Objects. Add a few more Entities and Value Objects to model.btdesign. Add different types of Attributes and References. For example you can add a new Module named customer, with a Customer entity, which has a Reference to rented Media. Maybe with a rental contract Value Object in between, containing time period and price.

Regenerate with mvn generate-sources -Dfornax.generator.force.execution=true and look at the generated Java code and JPA annotations.

Documentation of domain model

Sculptor generates summary documentation of domain model in clickable HTML format. The output is placed in src/generated/resources/DomainModelDoc*.html

The descriptions are defined in model.btdesign. Almost all elements in the model can have documentation. It is a simple quoted string before the element.

        "Book is one of the products that..."
        Entity Book extends @Media {
          "International Standard Book Number"
          String isbn key length="20"
        }

The documentation is also included as JavaDoc of generated Java code.

Diagram of domain model

Sculptor generates several UML diagrams for the domain model. The above diagram for the Library domain is generated.
We are using Graphviz.
Sculptor generates textual Graphviz .dot files, which is then used to generate images.

There is a maven plugin fornax-graphviz-m2-plugin that generates images (.png) from the .dot files. This plugin is included in the pom.xml created by the archetypes, but you need to install Graphviz and have dot executable in path.

It is possible to mark some domain objects with hint="umlgraph=core" to generate a special diagram with those domain objects in focus.

        Entity Book extends @Media {
          hint="umlgraph=core"
          String isbn key
        }
    Entity Movie <span class="code-keyword">extends</span> @Media {
      hint=<span class="code-quote">"umlgraph=core"</span>
      <span class="code-object">String</span> urlIMDB key
      <span class="code-object">Integer</span> playLength
      - @Genre category nullable
    }

The colours are customizable as described in Developer's Guide

Subject area class diagrams

It is also possible to create additional class diagrams to portray specific subject areas. One diagram is created per subject area you define, and the classes that belong to each subject area are defined via the "umlgraph.subject" hint:

        ValueObject Itinerary {
        	hint="umlgraph.subject=routingsvc"
        	belongsTo Cargo
            not optimisticLocking
            not immutable
            - @Cargo cargo nullable opposite itinerary
            - List<@Leg> legs inverse orderColumn
        }

To include a class in more than one subject area, list each subject area in the hint, separated by a '|'. e.g. hint="umlgraph.subject=routingsvc|cargoext"

How to Generate Services

In the DSL an operation of a Service almost looks like an ordinary Java method with return type, parameters and throws declaration. You don't have to declare the visibility, it is public by default.

Service LibraryService {
    List<@Movie> findBestMovies(Long libraryId);
}

When referring to a Domain Object (Entity or ValueObject) you use an @ in front of the declaration. Primitive types or any fully qualified Java class can also be used as parameters and return types. The same built in types as can be used for the Domain Objects can be used in the service operations.

It is also possible to use Data Transfer Objects as parameters and return types.

Service LibraryFacade {
  List<BookDto> topTenBooks;
}

DataTransferObject BookDto { String title String isbn int rating }

Collections use the ordinary Java generics syntax. Built in collection types:

  • Set
  • List
  • Map
  • Collection

You can easily delegate to an operation in a Repository or another Service. Then you only have to declare the name of the operation. Return type and parameters are "copied" from the delegate operation.

findLibraryByName => LibraryRepository.findLibraryByName;

Sometimes it can be convenient to declare a delegating operation with protected visibility. Such an operation is typically used by another manually coded operation in the Service.

@Library findLibraryByName(String name);
protected findLibraryByExample => LibraryRepository.findByExample;

There is an alternative notation for delegation. Instead of => you can use delegates to.

It is also possible to specify a dependency injection of a Repository or any other Spring bean, without having a generated delegation method.

Service LibraryService {
  > @MediaCharacterRepository
  addCharacter(@Person person, @Media media, String role);
}

The @ in front of the Repository name indicates that it is an internal reference and the DSL editor will validate that it exists. Skip the @ when you inject other Spring beans, which are not defined in the DSL model.
When you inject other beans you have to implement the setter method yourself in the Service implementation class. You define other Spring beans in more.xml or a Spring file imported from there.

There is an alternative notation for dependency injection. Instead of > you can use inject.

The generated code consists of:

  • Interface, eg. LibraryService
  • Implementation class, e.g. LibraryServiceImpl
  • Base class for the implementation, e.g. LibraryServiceImplBase
  • Test class, e.g. LibraryServiceTest
  • Base class for the test, e.g. LibraryServiceTestBase

Separation of generated and manually written code is done by a generated base class and manually written subclass. The subclass is also generated, but only once, it will never be overwritten by the generator. You can of course remove it to regenerate it. There will not be any subclass, gap class, when the service only consists of operations that delegate to repositories and other other services.

If you have configured to use EJB there will also be Stateless session EJB and Client side proxy for the EJB.

Try to add one more operation to the PersonService, e.g. findPersonsByCountry. Generate with mvn generate-sources -Dfornax.generator.force.execution=true. Note that PersonServiceImpl is not overwritten and therefore you will get compilation errors for the new method. You have to add it manually in PersonServiceImpl. Tips: use ctrl+1 in Eclipse.

How to Generate Repositories

The default implementation of a Repository consists of an implementation class and Access Objects. The intention is a separation of concerns between the domain and the data layer. Repository is close to the business domain and Access Objects are close to the data layer. The JPA/Hibernate specific code is located in the Access Object, and not in the Repository.

Guidelines:

  • The interface of the Repository should always speak the Ubiquitous Language of the domain. It provides domain centric operations to the client.
  • Repositories provide controlled access to the underlying data in the sense that it exposes only the Aggregate roots of the domain model.
  • The Repository operation can typically use several Access Objects or other Repositories to collect the final result.
  • The Access Objects are at a lower level of abstraction than the Repository and can contain plumbing code to pull out data from the database. Even though JPA/Hibernate does a great job, there is often technical details of fetching, joining, caching, etc. that is not part of domain layer.

A Repository generated by Sculptor uses Access Objects, which implement the specialization needed for persistence. Access Objects are implemented as Commands and have separated interface and implementation. The JPA/Hibernate specific code is encapsulated in the implementation classes. A Factory is used to create instances of the Access Objects.

It is possible to use another design approach and implement everything directly in the Repository. This is described in the section Hibernate Repository in the Developer's Guide.

Generic Access Objects

Sculptor runtime framework provides generic access objects for the following operations:

  • findById
  • findAll
  • findByExample
  • findByQuery
  • findByCondition
  • findByCriteria (use findByCondition instead)
  • findByKey
  • findByKeys
  • save
  • delete
  • countAll
  • populateAssociations
JPA2
All generic access objects, including findByExample, findByCondition and findByCriteria, are implemented for all supported JPA2 providers.
To use a generic Access Object you simply have to specify the name of it in the Repository in the DSL.
Repository LibraryRepository {
    findById;
    save;
    delete;
}

Note that you can define operations as protected to not expose them in the Repository interface and only use them from other methods with a more domain centric interface.

Repository PersonRepository {
    List<@Person> findPersonByName(String name);
    protected findByQuery;
}

See the source of the corresponding classes for more information of the functionality of these Access Objects.

Note that setter methods in the interface corresponds to method parameters in the repository operation. For example the findAll operation can be used with parameters for sorting and caching.

Repository LibraryRepository {
    //  deprecated using jpa2
    //  findAll(String orderBy, boolean orderByAsc, boolean cache);
    findAll(String orderBy, boolean cache);
}

You would often like to define sorting and caching without exposing it as parameters, i.e. always use caching and always sort in a specific way. Therefore this is supported with hints.

Repository PersonRepository {
    //  deprecated using jpa2
    //  findAll hint="orderBy=name.last, orderByAsc=false";
    findAll hint="orderBy=name.last asc, name.first desc" or findAll orderBy="name.last asc, name.first desc"
    findByKey hint="cache";
}

Custom Access Objects

You can also use Access Objects containing manually written code. You specify => AccessObject in the DSL.

Repository MediaRepository {
    int getNumberOfMovies(Long libraryId) => AccessObject;
    ...
}

There is an alternative notation for delegation. Instead of => you can use delegates to.

Note that you define return type and parameters in the same way as for Service operations. When referring to a Domain Object (Entity or ValueObject) you use a @ in front of the declaration.

By default the Access Object has a similar name as the repository operation, but you can define another name.

Repository MediaRepository {
    int getNumberOfMovies(Long libraryId) => MovieCounter;
    ...
}

It is in the performExecute method in the AccessImpl class you place the hand written code.

public class GetNumberOfMoviesAccessImpl extends GetNumberOfMoviesAccessImplBase {
    public void performExecute() {
        DetachedCriteria criteria = DetachedCriteria.forClass(Movie.class);
        criteria.setProjection(Projections.rowCount());
        List result = getHibernateTemplate().findByCriteria(criteria);
        Number count = (Number) result.get(0);
        setResult(count.intValue());
    }
}

It is also possible to use, and specialize the generic Access Objects.

public class FindPersonByNameAccessImpl extends FindPersonByNameAccessImplBase {
    public void performExecute() {
        // use the generic FindByCriteria, but with a special restriction
        FindByCriteriaAccessImpl<Person> finder =
                new FindByCriteriaAccessImpl<Person>(Person.class) {
            protected void addRestrictions(Criteria criteria) {
                List names = Arrays.asList(getName().split(" "));
                criteria.add(Restrictions.or(
                        Restrictions.in(NAME + "." + FIRST, names),
                        Restrictions.in(NAME + "." + LAST, names)));
            }
        };
        finder.setSessionFactory(getSessionFactory());
        finder.setCache(true);
        finder.setOrderBy(NAME + "." + LAST);
        finder.execute();
        setResult(finder.getResult());
    }
}

Repository Operation without Access Object

There is also a third variant of Repository operations, which neither delegates to a generic nor a custom Access Object. It is manually written in the Repository implementation. To avoid automatic delegation you simply don't specify => AccessObject in the DSL.

Repository LibraryRepository {
    @Library findLibraryByName(String name);
    ...
}

It is possible to specify a dependency injection of a another Repository, which can be useful for this variant of Repository operation. It is used in the library example to implement findMediaByCharacter in the MediaRepository.

Repository MediaRepository {
  > @MediaCharacterRepository
  List<@Media> findMediaByCharacter(Long libraryId, String characterName);
  ...
}

The @ in front of the Repository name indicates that it is an internal reference and the DSL editor will validate that it exists. Skip the @ when you inject other Spring beans, which are not defined in the DSL model.
When you inject other beans you have to implement the setter method yourself in the Repository implementation class. You define other Spring beans by annotating the classes with @Repository, @Service, or @Component.

There is an alternative notation for dependency injection. Instead of > you can use inject.

Take a look at the Java code for findLibraryByName in LibraryRepositoryImpl.
As you can see you have to implement it manually, this can be done by something like this:

public Library findLibraryByName(String name) {
    Map<String, Object> parameters = new HashMap<String, Object>();
    parameters.put(LibraryNames.NAME, name);
List&lt;Library&gt; result = findByQuery(<span class="code-quote">"Library.findLibraryByName"</span>, parameters);

<span class="code-keyword">if</span> (result.isEmpty()) {
    <span class="code-keyword">return</span> <span class="code-keyword">null</span>;
} <span class="code-keyword">else</span> {
    <span class="code-keyword">return</span> result.get(0);
}

}

Scaffold

It is possible to mark a Domain Object with scaffold to automatically generate some predefined CRUD operations in the Repository and corresponding Service.

Entity Person {
    scaffold
    String ssn key
    String name
}

The above DSL definition would result in the same as the following:

Service PersonService {
    findById => PersonRepository.findById;
    findAll => PersonRepository.findAll;
    save => PersonRepository.save;
    delete => PersonRepository.delete;
}

Entity Person { String ssn key String name

Repository PersonRepository {
    findById;
    findAll;
    save;
    delete;
}

}

The Repository and Service is also added automatically if they are not defined.

Which scaffolding operations to use can be defined in sculptor-generator.properties, see Developer's Guide.

Pagination

Paging of large result sets is supported for findAll, findByQuery, findByCriteria and custom access objects.
Usage of paging is defined by adding a PagingParameter as parameter to a repository operation.

    findAll; // without paging
    findAll(PagingParameter pagingParameter); // with paging

When using a paged operation it looks like this:

        // fetch first page, and request for number of pages
        int page = 1;
        int pageSize = 25;
        boolean countTotalPages = true;
        PagingParameter pagingParameter = PagingParameter.pageAccess(pageSize, page, countTotalPages);
        PagedResult<Person> pagedResult = personRepository.findAll(pagingParameter);
        int totalPages = pagedResult.getTotalPages();
        List<Person> values = pagedResult.getValues();
    <span class="code-comment">// later fetch another page, without calculating number of pages

int page = 2; int pageSize = 25; PagingParameter pagingParameter = PagingParameter.pageAccess(pageSize, page); PagedResult<Person> pagedResult = personRepository.findAll(pagingParameter); List<Person> values = pagedResult.getValues();

In the initial request you typically ask for number of available pages. To answer this the system must count total number of rows. For findAll there is a built in countAll access object. For findByQuery, findByCriteria custom access objects you must provide a count operation, which can be done in several ways.

For findByQuery the default is to use a naming convention of the named query for the counting query. find is replaced with count.
findByQuery on named query "Person.findByCountry" will use "Person.countByCountry".

For findByCriteria and custom access objects you must define the count operation or named query to use. That is done with hint countOperation or countQuery. Operation parameters are passed to the count operation or count query. Try the following and look at the generated code.

    findByQuery(PagingParameter pagingParameter);
myPagedFindOperation(<span class="code-object">String</span> searchFor, PagingParameter pagingParameter)
    hint=<span class="code-quote">"countQuery=Person.myPagedCountQuery"</span>
    delegates to AccessObject;

findByCriteria(PagingParameter pagingParameter)
    hint=<span class="code-quote">"countOperation=findByCriteriaCountOperation"</span>;
<span class="code-keyword">protected</span> <span class="code-object">int</span> findByCriteriaCountOperation(Map&lt;<span class="code-object">String</span>, <span class="code-object">Object</span>&gt; restrictions);

Those hints are available for findAll and findByQuery also.

For findAll and findByCondition it is possible to use a config property in sculptor-generator.properties to say that paging should always be used, i.e. not necessary to add the PagingParameter to model.

findAll.paging=true
findByCondition.paging=true

findByCondition

findByCondition is one of the built in repository operations. It is used like this.

It takes a list of ConditionalCriteria objects as parameter. Use the fluent api of ConditionalCriteriaBuilder to define the criteria.

List<ConditionalCriteria> conditions = ConditionalCriteriaBuilder.criteriaFor(Person.class)
    .withProperty(PersonProperties.primaryAddress().city()).eq("Stockholm")
    .build();

Note that for each Domain Object a corresponding Properties class is generated. It contains definitions of all attributes and references of the the Domain Object. It serves two purposes. It is refactoring safe, since you will get compilation errors when something is changed. It provides convenient code completion (ctrl+space) support in your IDE.

You would typically use static imports to make the expression more compact and readable:

List<ConditionalCriteria> conditions = criteriaFor(Person.class)
    .withProperty(primaryAddress().city()).eq("Stockholm")
    .build();

The builder supports conditions such as eq, like, between, lessThan, greaterThan, in. The order of the result can be specified with orderBy. Use code completion (ctrl+space) to see all available methods. It also supports logical expressions and, or and not, which can be grouped with lbrace and rbrace.

List<ConditionalCriteria> conditions = criteriaFor(Person.class)
    .withProperty(ssn().country()).eq(Country.SWEDEN)
    .and().withProperty(birthDate()).between(new LocalDate("1970-01-01")).to(new LocalDate("1979-12-31"))
    .and().lbrace().withProperty(primaryAddress().city().eq("Stockholm")
        .or().withProperty(primaryAddress().city().eq("Malmö")).rbrace()
    .orderBy(name().last()).orderBy(name().first())
    .build();

ConditionalCriteria supports multiple column selects with column aliases, aggregate functions and grouping.

List<ConditionalCriteria> conditions = criteriaFor(Person.class)
    .select(PersonProperties.name().last()).alias("lastName")
    .select(PersonProperties.salary()).alias("sal").min()
    .withProperty(PersonProperties.sex()).eq(Gender.MALE)
    .groupBy(PersonProperties.name().last())
    .orderBy(PersonProperties.name().last()).ascending()
    .build();

It is also possible to construct the ConditionalCriteria without the builder, using the static factory methods in ConditionalCriteria.

List<ConditionalCriteria> conditions = new ArrayList<ConditionalCriteria>();
ConditionalCriteria conditionalCriteria =
    ConditionalCriteria.equal(PersonProperties.primaryAddress().city().toString(), "Stockholm");
conditions.add(conditionalCriteria);

Generation of Finder Operations

Widely used repository operations are operations to find domainobjects. Often this operations use static queries to get the results. In this cases it is now possible to specify the related query in the model file and let sculptor generate the code for the repository operations.

This feature is not activated by default. To allow sculptor to generate finder operations add the following line into your sculptor-generator.properties file.

generate.repository.finders=true

After activation all repository operations starting with 'find', except built in operations, are potential candidates for finder generation. To prevent a generation and to use handwritten code for a special operation, add the keyword 'gap'.

PersonRepository {
    List<@Person> findPersonByName(String name) gap;
}

There two variants of generated finder operations.
One is based on the the built in repository operation findByQuery the other uses findByCondition.

Both variants actually do not support pagination.

Based on findByQuery

This kind of finder operation is using a JPQL query or named query and delegates the query to the built in findByQuery access object. It is not necassary to add a findByQuery operation to the model. It will be added to the repository automatically in case it is not defined.

Here are typical examples.

PersonRepository {
    List<@Person> findByNamedQuery(@Country country) query="Person.findByCountry";
    List<@Person> findBy(@Country country) query="select object(p) from Person p where p.ssn.country = :country";
    findBy(@Country country) query="ssn.country = :country";
}

All these examples will produce the same results.
findByNamedQuery is using a named query defined in the Person entity class.
findBy will execute the given JPQL statement to get the result list.

The second findBy operation is a short form of the first. The generated code is indetical. This means specifing the result type of an operation is optional. The default is List<@AggregateRoot> (in the example List<Person>). For simple queries you can omit some parts of the JPQL query and only add the restrictions.

The parameters of the operation will be used to set the query parameters with matching names.

You are free to choose a name for your finder operations
(it is good style to have a consistent naming, but names must not start with 'find')

PersonRepository {
    getResidentIn(@Country country) query="Person.findByCountry";
    getResidentInGermany() query="ssn.country = 'DE'";
}

Next to the aggregate root any appropriate return type is allowed. This can be simple types, referenced domain objects (BasicType/Entity) and collections (List/Set) of simple types and domain objects. Returning multiple columns is just as possible as using aggregate functions and ordering.

PersonRepository {
    @Contact findContact(String number, @Country)
                         query="select p.contact from Person p where p.ssn.number = :number and p.ssn.country = :country";
    List<@Country> findCountries()
                         query="select p.ssn.country from Person p";
    Set<@Contact> findContacts(@Country country)
                         query="select p.contact from Person p where p.ssn.country = :country";
    List<Object[]> findLastNameAndMinSalary()
                         query="select p.name.last, min(p.salary) from Person p group by p.name.last order by p.name.last";
}

Based on findByCondition

This kind of generated finder operation builds a ConditionalCriteria from a JQPL like syntax and delegate the execution of the query to the built in findByCondition operation. Also here it is not necessary to add a findByCondition operation to the model.

Here is a typical example.

PersonRepository {
    List<@Person> findBy(String first, String last) condition="name.first = :first and name.last = :last";
    findBy(String first, String last);
}

Both findBy operations will produce the same results. The second findBy operation is a short form of the first. The generated code is indetical. Specifing the result type of an operation is optional. If absent the default List<@AggregateRoot> is assumed.

The parameters of the operation will be used to set the condition parameters with matching names. If the operation name starts with 'find' you can omit the condition keyword. In this case the list of operation parameters is used to build the condition. For all parameters sculptor tries to find a property (attribute or reference) in the root entity with a name that matches the parameter name. If not found referenced domain objects (BasicType/Entity) are searched for matching properties. In the above example 'first' and 'last' are attributes in a referenced BasicType 'PersonName'. Operation parameters and domain objects properties are compared with '=' and all parameters are combined with an 'and' operator.

If no matching property could be found you will receive a generation time error.

Always the first matching property is used to build the condition. If there is more than one you may need to clarify what property should be used. You can do this as follows (both ways are identical).

PersonRepository {
    findUsingMother(String mother_first, String mother_last);
    findUsingMother(String first, String last) condition="mother.first = :first and mother.last = :last";
}

You are free to choose a name for your finder operations as long as you provide the dsl keyword condition (also select, groupBy, etc. will do the trick).

PersonRepository {
    getResidentIn(@Country) condition="ssn.country = :country";
}

Of course you can use more comparison operators than '='. All operators supported by ConditionalCritria are possible ('<','>','<=','>=','!=','is null', etc.). Even braces are supported. You can use a JPQL like syntax to set a condition.

PersonRepository {
    findBornBefore(Date bd) condition="birthDate < :bd";
    findMissingContactInfo(String name) condition="(contact.zip is null or contact.city is null) and name.last like :name";
}

Two special operators can be used to support case insensitve comparison, 'i=' (ignore case equal) and 'ilike' (ignore case like).

PersonRepository {
    findByName(String name) condition="name.last i= :name or name.first i= :name";
    findByNameLike(String name) condition="name.last ilike :name or name.first ilike :name";
}

Any appropriate return type is allowed. This can be simple types, referenced domain object (BasicType/Entity) and collections (List/Set) of simple types and domain objects. Returning multiple columns is just as possible as using aggregate functions and ordering.

You can add a complete JPQL statement or only the needed parts.
(not all JPQL features are supported, e.g you can not use subselects or explicit joins)

PersonRepository {
    Set<@Country> findAllCountries() condition="select p.ssn.country from Person p"
    Set<@Country> findAllCountries() select="ssn.country"
    Set<@Country> findAllCountries()
}

Again all above examples are identical. Sculptor tries to guess the desired select part by the given return type. The root entity is searched for a referenced domain object that matches the return type (simple types are not supported). If none is found all references are searched for a matching type. In case there is no matching domain object you receive an error.

In combination with building the restrictions from a operation parameter list the amount of needed keywords is significantly reduced.

PersonRepository {
    @Contact findContactByKeyAttributes(String number, @Country country);
    Set<@Contact> findAllContactsByCountry(@Country country);
}

Here are some more advanced examples.

Entity Person {
    ...
    Long salary nullable;
PersonRepository {
    getRankingOfTopEarners(<span class="code-object">Long</span> income) condition=<span class="code-quote">"salary &gt; :income"</span> orderBy=<span class="code-quote">"salary desc, name.last asc"</span>;
    <span class="code-object">long</span> annualSalaries() select=<span class="code-quote">"sum(salary)"</span>;
}

}

Multiple column results are supported (and only this columns will be selected).
Similar to operations using findByQuery you can use an object array to store the values of the result set.
In addition to an object array a Tuple (javax.persistence.Tuple, JPA2) can be used, which provides some useful methods to extract the elements of the query result.

PersonRepository {
    List<Tuple> findByGender(@Gender sex) select="name.last, birthDate" orderBy="name.last";
}

This is the common way to handle multiple column results. But there is also another option.
You can directly use another domain object as return type, usually a DataTransferObject (DTO), and map the query result columns to the attributes of that return type. Actually only simple attribute types (String,Long,...) are supported.

A usual case is that the DTO and the Entity have matching attributes. In this case it is unnecassary to specify a select part.

DataTransferObject PersonDto {
    String last;
    String first;
    Date birthDate;
    String notMatching nullable;
}

Entity Person { ... PersonRepository { List<@PersonDto> findPersonsBy(@Country country) orderBy="birthDate"; @PersonDto findPersonBy(Long id); } }

In the above example sculptor will generate a query that selects the three matching attributes 'last' (name.last), 'first' (name.first) and 'birthDate'. Attributes that can not be found in the root entity type are ignored and therefore must be nullable in the result type.

If the attribute names of the entity type and the return type do not match, it is possible to use aliases in the select part to map the query results.

Here is an examples using a DTO as return type and attribute mapping with column aliases.

DataTransferObject PersonDto {
    String lastName;
    String firstName;
    Date birthDate;
}

Entity Person { ... PersonRepository { List<@PersonDto> findPersonsBy(@Country country) select="name.last as lastName, name.first as firstName, birthDate"; @PersonDto findPersonBy(Long id) select="name.last as lastName, name.first as firstName, birthDate"; } }

Attribute mapping is the default case using a DataTransferObject as return type. Sometimes it is useful to use a constructor instead of setting attributes directly, e.g. to do additional initializations and/or type conversions. Using a constructor of the return type is provided by the keyword 'construct'.

Here is a simple example. Of course column aliases are useless.

DataTransferObject PersonDto {
    String lastName required;
    String firstName required;
    Long income nullable required;
}

Entity Person { Long salary nullable; ... PersonRepository { List<@PersonDto> findPersonsBy(@Country country) construct select="name.last, name.first, salary" orderBy="name.last"; @PersonDto findPersonBy(Long id) construct select="name.last, name.first, salary"; } }

And now putting some things together.

DataTransferObject PaymentsDto {
    String zip;
    Long payments nullable;
}

Entity Person { Long salary nullable; ... PersonRepository { List<@PaymentsDto> paymentsPerCityIn(@Country country) select="contact.zip, sum(salary) as payments" groupBy="contact.zip" orderBy="contact.zip"; } }

JUnit

For each Service there is a generated JUnit test class that you have to implement. It extends IsolatedDatabaseTestCase which means that DBUnit it used to load the in memory HSQLDB database with test data from the XML file specified in the getDataSetFile method. The database is refreshed for each test method.

Spring beans are injected in the test with ordinary @Autowired annotations.

AbstractDbUnitJpaTests also provides a method to retrieve the ServiceContext, which is always passed in as the first parameter of the service methods.

List<Person> persons = personService.findPersonByName(
  getServiceContext(), "Skarsgård");

You can implement the same kind of JUnit tests for the Repositories.

Above tests are kind of integration tests and you should do ordinary unit tests for domain objects and other classes of importance.

Mocking

Sometimes it is useful to test the Services by stubbing dependencies, e.g. to Repositories.

It is possible to use Mockito or some other mocking framework together with Spring if you do as follows.

As example we have a MessageSender, which is implemented using JMS. The MessageSender implementation is normally injected using @Autowired annotation. It is this implementation we want to replace with a mock when testing. We can create the mock instance using the FactoryBean that is included in Sculptor so we only need to add the xml definition in more-test.xml:

<bean id="messageSenderMockFactory"
  class="org.fornax.cartridges.sculptor.framework.test.MockitoFactory"
  primary="true" >
    <property name="type" value="org.foo.MessageSender"/>
</bean>

In the junit test:

public class MyFacadeTest extends AbstractDbUnitJpaTests
    implements MyFacadeTestBase {
<span class="code-keyword">private</span> MessageSender messageSender;
<span class="code-keyword">private</span> MyFacade myFacade;

@Autowired
<span class="code-keyword">public</span> void setMyFacade(MyFacade myFacade) {
    <span class="code-keyword">this</span>.myFacade = myFacade;
}

@Autowired
<span class="code-keyword">public</span> void setMessageSender(MessageSender messageSender) {
    <span class="code-keyword">this</span>.messageSender = messageSender;
}

@Before
<span class="code-keyword">public</span> void initMock() {
    when(messageSender.sendMessage(anyString()))
      .thenReturn(<span class="code-keyword">true</span>);
}


@Test
<span class="code-keyword">public</span> void testDoSomething() <span class="code-keyword">throws</span> Exception {
    <span class="code-object">int</span> countBefore = countRowsInTable(<span class="code-quote">"SOMEDATA"</span>);
    myFacade.doSomething(<span class="code-quote">"17"</span>);
    <span class="code-object">int</span> countAfter = countRowsInTable(<span class="code-quote">"SOMEDATA"</span>);

    assertEquals(countBefore + 1, countAfter);

    verify(messageSender).sendMessage(anyString());
}

}

In the previous section about How to Generate Services you added the operation findPersonsByCountry in PersonService. Think about how to implement that method using one of the three different types of Repository operations. It is possible to do it with any of the three variants. Select the alternative you find most attractive and give it a try. Implement a test method also.

You might find the following DBUnit test data useful:

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
  <PERSON ID="1" SSN_NUMBER="123456" SSN_COUNTRY="us" NAME_FIRST="Aaaa" NAME_LAST="Bbbb"
    BIRTHDATE="1963-01-01" SEX="M" VERSION="1"/>
  <PERSON ID="2" SSN_NUMBER="123456" SSN_COUNTRY="se" NAME_FIRST="Xxxx" NAME_LAST="Yyyy"
    BIRTHDATE="1964-01-01" SEX="F" VERSION="1"/>
  <PERSON ID="3" SSN_NUMBER="987654" SSN_COUNTRY="us" NAME_FIRST="Cccc" NAME_LAST="Dddd"
    BIRTHDATE="1965-01-01" SEX="F" VERSION="1"/>
</dataset>

Error Handling

Two types of exceptions are used, a checked exception called ApplicationException and a runtime exception called SystemException.

ApplicationException is used for faults that the system is designed to take care of, i.e. recoverable errors at the level of the application, e.g. validation failures.

System exceptions are used to indicate unrecoverable, unexpected errors that are outside the control of the application, e.g. database failures. The current processing can't continue and the transaction is rolled back when they occur.

Effective Java Exceptions is a good article describing similar error handling strategy.

Both ApplicationException and SystemException contain an error code, which is used by the client to translate to appropriate error message. Subclasses to these exceptions defines specific error situations and it is in these subclasses the error codes are defined, i.e. an exception class can define several error codes to indicate different flavours of the fault.

In the throws clause of operations for Services or Repositories you can define a comma separated list of exceptions. It can be fully qualified or unqualified class names. When it is unqualified name an ApplicationException subclass with that name will be generated.

Repository LibraryRepository {
    @Library findLibraryByName(String name) throws LibraryNotFoundException;
    protected findByQuery;
  }

A Spring advice will catch all exceptions that are thrown from the Services. This advice will log all SystemExceptions to the error log. ApplicationExceptions are logged at debug level. Log4j is used for the logging.

The ServiceContext class is needed to support logging and audit trail functionality through the tiers of an application. A ServiceContext object will typically be sent through all tiers, and represents the context in which a business service is called. It contains information about the user and ids for the session and request.

The first parameter of each method in the Services is a ServiceContext parameter. This is generated automatically. In front of the Services there is an advice, which stores this ServiceContext object in a thread local variable, ServiceContextStore, to make sure that it is available everywhere within that request in the tier. When calling remote methods it must be passed as a method parameter.

It is possible to skip the generation of ServiceContext, see Developer's Guide.

Alternative Notation

There is an alternative notation for some things in the DSL. It is more verbose but maybe easier to understand. You can mix the verbose and compact syntax as you wish.

Compact Verbose
! not
=> delegates to
> inject
<-> opposite
- reference
model.btdesign with verbose syntax
Application Library {
    basePackage = org.library
Module media {

    Service LibraryService {
      findLibraryByName delegates to LibraryRepository.findLibraryByName;
    }

    Entity Library {
      not auditable
      <span class="code-object">String</span> name key
      reference Set&lt;@PhysicalMedia&gt; media opposite library

      Repository LibraryRepository {
        inject @MediaRepository
        @Library findLibraryByName(<span class="code-object">String</span> name);
      }
    }
}

}

Reserved keywords

If you need to define names that clash with the reserved keywords in the Sculptor DSL you can escape your name by prefixing with ^. Example:

    Entity ^Entity {
      String ^url
    }

Divide model into several files

It is possible to split model.btdesign and define one or more Modules in each file.

By using one file per module it is also possible for Sculptor to do a partial generate of the changed modules and the ones depending on the changed modules. The file must be named the same as the module (media.btdesign, person.btdesign) or prefixed with model_ or model- (model-person.btdesign). This partial generation can shorten the generation time for large projects. fornax-oaw-m2-plugin will detect which model files has changed since previous generator run when using mvn -o generate-sources. Full generate will be done when using -Dfornax.generator.force.execution=true or mvn clean generate-sources

Referenced files are imported with a URI syntax starting with classpath:/ followed by classpath path to the .btdesign file to be imported.

model.btdesign main file
import "classpath:/model-person.btdesign"

Application Library { basePackage = org.library

Module media {

    <span class="code-comment">// as usual ...

} }

model-person.btdesign containing person module in separate file
ApplicationPart PersonPart {
Module person {

    <span class="code-comment">// as usual ...

} }

The main file doesn't have to define a Module.

model.btdesign empty main file
import "classpath:/model-media.btdesign"
import "classpath:/model-person.btdesign"

Application Library { basePackage = org.library }

The partial generation and the fornax-checksum-m2-plugin doesn't play well together, so if you are using the partial generation feature, i.e. running mvn generate-sources without -Dfornax.generator.force.execution=true you need to remove the fornax-checksum-m2-plugin from your pom.xml.

Cross project references

In previous section it was described how the model could be separated into several files. It is also possible to define modules in a separate project to be used from other projects.

In that case the imported module must define a basePackage and use ApplicationPart.

model-sharedtypes.btdesign in separate file, in another project
ApplicationPart CommonPart {
Module sharedtypes {
    basePackage=org.foo.common.sharedtypes

    BasicType Money {
        <span class="code-object">String</span> currency;
        BigDecimal amount;
    }
}

}

You don't want to generate code for the imported module in your project, which is going to use the sharedtypes from the common project. To skip generation of a module you define the module (sharedtypes in this example) in sculptor-generator.properties:

generate.module.sharedtypes=false

You import and use it as normal, i.e.

import "classpath:/model-sharedtypes.btdesign"

Application Bank { basePackage = org.bank

Module accounting {
    Entity Account {
        <span class="code-object">String</span> accountNumber key;
        - @Money balance;
    }
}

}

The dependency to the classes must be added in pom.xml:

        <dependency>
            <groupId>org.foo.common</groupId>
            <artifactId>foo-common</artifactId>
            <version>1.0</version>
            <classifier>client</classifier>
        </dependency>

Note that if you use classifier client it will be dependency to the -client jar file, which was created by maven-jar-plugin with corresponding client classifier.

If you use ejb you will use <type>ejb-client</type> instead of classifier.

Source

The complete source code for this tutorial is available in Subversion.

Web Access (read only):
http://fisheye3.cenqua.com/browse/fornax/trunk/cartridges/sculptor/fornax-cartridges-sculptor-examples-library

Anonymous Access (read only):
https://fornax.svn.sourceforge.net/svnroot/fornax/trunk/cartridges/sculptor/fornax-cartridges-sculptor-examples-library