Wednesday, August 7, 2013

Building the Association Plugin: Domain Model Design

Here we are going to develop a design for building and Spring Roo add-on that allows management of associations between JPA entities.  From prior implementation it's known that the add-on is going to have to manage code in both the domain and presentation model.

This post focus's on functionality of the add-on involving the domain model.  Design for the presentation models will be discussed in a later posts.

Introduction


Roo interacts with your add on largely with two types of processing requests, command operations and annotation processing.  During both of these processes the add on has the opportunity to modify project assets.

Project assets can be categorized as developer owned and Roo generated assets.  Developer owned assets are assets that the developer has either written by hand or instructed Roo to create,  These assets  define the project and are the system of reference during Roo code generation.  Add on generated assets on the other hand are Roo domain and can change at anytime.

During command processing the developer has requested Roo to perform an operation. This means that for the scope of the command request the add on has access to developer assets.  Generally those modifications are centered on either creating new assets for the developer or adding to existing assets.  In the case of the entity model this means adding annotations or entities.  Add on's should contribute to the construction of the project, this means that removal of assets is discouraged.

In contrast let's consider annotation processing.  During this process modification to developer assets should be considered forbidden. Here the programming sandbox allows you to do anything you want as long as it is limited to maintaining the contents of the file associated with the annotation.  Roo really prefers we design to a one to one relationship between annotation definitions and ITD files.  So, it is best to go with the flow and design solutions to conform to this relationship.

We then need our implementation to follow this structure.  In command processing the add on needs to set everything up in the developer assets so that after the resultant annotation processing all the intended functionality is available to the project.

High Level Design


Right now the add on is going to offer only one command, that of creating an association between two entities.  Functionally this means that;
  • A association entity between the user provided entities must be created.
  • Annotation markup on each of the user provided entities needs to be added.
  • New annotation handlers that inject entity operations that manage the association need to be provided.
For the association plugin the create associations we expect the command to look like;

association add many-to-many --between <entity-1> --and <entity-2>

where <entity-1> and <entity-2> are the two independent entities.

In response to that command the add on then needs to create the association entity.  This entity will contain a reference to each of the provided entities, existence of a entity denotes an association.  The name of the association entity will be built by concatenating the names of the two entities together.  This gives us two possible combinations for the association entity name.  Later we will need to derive that name later so to simplify this the name with the lowest alphabetical sort order will be used first followed by the other.

Next each annotations will need to be injected into each of the independent entities in the relationship.  An important factor here is that an independent entity can participate in more then one association.  While it would have been convenient to specific the association entity directly in the annotation.  There's a host of problems going that route.  But since we were careful about how we named the association entity allows us to derive it rather then have to have it explicitly stated.  We can keep it simple and just accept a list of independent entities that the entity is associated with.  So we can expect the annotation to have the form;

@association( <array of entity names> )

This annotation will then trigger the generation of the ITD's.  From prior development we have a example copy of the ITD that need to be generated already developed from prior posts.

Low Level Design


Now we need to tighten up the definition of the functionality that we need to construct.  First we have the association entity.  We can produce a example from prior development.

package com.repik.multitenant.security.domain;

import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import org.springframework.roo.addon.javabean.RooJavaBean;
import org.springframework.roo.addon.jpa.activerecord.RooJpaActiveRecord;
import org.springframework.roo.addon.tostring.RooToString;

@RooJavaBean
@RooToString
@RooJpaActiveRecord(identifierColumn = "ORGANIZATION_USER_ID", finders = { "findOrganizationUsersByOrganization", "findOrganizationUsersByAppUser" })
public class OrganizationUser {

    @ManyToOne
    @JoinColumn(name = "ORGANIZATION_ID")
    private Organization organization;

    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private AppUser appUser;
}


The association annotation describing the relationship is then injected into each end of the relationship.  That annotation triggers Roo to invoke the add-on's ITD generation code.  For the ITD implementation we can also produce and example from a prior implementation of the functionality.  The ITD is going to inject four methods into the relationship entity;
  • find instances of a the other entity associated with an entity.
  • find instances of the other entity not associated with the entity.
  • Create an association between the entities
  • Delete an association between the entities
So we also will need a copy of the prior queries to use as a template.

package com.repik.multitenant.security.domain;

import javax.persistence.EntityManager;
import javax.persistence.Query;
import javax.persistence.TypedQuery;

import org.springframework.transaction.annotation.Transactional;

public privileged aspect AppUser_Association {

    public void AppUser.createOrganizationUser(Long organizationId ) {
  Organization organization = Organization.findOrganization( organizationId ) ; 
        if (organization == null) throw new IllegalArgumentException("The organization argument is required");

        OrganizationUser ou = new OrganizationUser() ;
  ou.setOrganization( organization ) ;
  ou.setAppUser( this ) ;
  ou.persist() ;
    }
    
    public Integer AppUser.deleteOrganizationUser(Long organizationId ) {
        if (organizationId == null) throw new IllegalArgumentException("The organizationId argument is required");

        EntityManager em = OrganizationUser.entityManager();
  Query q = em.createQuery("DELETE FROM OrganizationUser ou WHERE ou.organization.id = :organizationId AND ou.appUser.id = :userId" ) ;
  q.setParameter("organizationId", organizationId);
  q.setParameter("userId", id );
  try {
   return q.executeUpdate();
  } catch (Exception e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
  return 0 ;
    }

 public static TypedQuery AppUser.findOrganizationsAssociatedWithAppUser(
   Long userId) {
  if (userId == null)
   throw new IllegalArgumentException("The userId argument is required");
  
  EntityManager em = AppUser.entityManager();
  TypedQuery q = em.createQuery(
    "SELECT ou.organization FROM OrganizationUser ou WHERE ou.appUser.id = :userId",
    Organization.class);
  q.setParameter("userId", userId);
  return q;
 }

 public static TypedQuery AppUser.findOrganizationsNotAssociatedWithAppUser(
   Long userId) {
  if (userId == null)
   throw new IllegalArgumentException("The userId argument is required");
  
  EntityManager em = AppUser.entityManager();
  TypedQuery q = em.createQuery(
    "SELECT o FROM Organization o WHERE o.id not in ( SELECT ou.organization.id FROM OrganizationUser ou WHERE ou.appUser.id = :userId )",
    Organization.class);
  q.setParameter("userId", userId);
  return q;
 }
}

With everything is in place from the design perspective we can now proceed to implementation.

No comments:

Post a Comment