Während der Arbeit an einer MAF Anwendung für einen unserer Kunden, hatten wir das Szenario, dass mehrere Benutzer mit dem gleichen Endgerät arbeiten mussten. Dabei sollten die Daten, die ein Benutzer offline gespeichert hatte nicht für andere Benutzer sichtbar oder editierbar sein.
Dieses bestimmte Szenario wird nicht in AMPA (A-Team Mobile Persistance Accelerator) standardmäßig unterstützt. Das Framework ist nicht auf einen Mehrbenutzerbetrieb ausgelegt. Es funktioniert nur für einen Benutzer alleine.

Dieser Artikel beschreibt die Lösung, die wir implementiert haben, um Mehrbenutzerfähigkeit mit AMPA zu gewährleisten. Um dies zu erzielen, mussten wir Änderungen am SQLite Objekt, das von AMPA generiert wird durchführen und Funktionalitäten des Frameworks erweitert, jedoch ohne dabei den Core Sourcecode von AMPA zu verändern.

Falls Sie nicht wissen was AMPA ist, können Sie sich hier ein Tutorial dazu ansehen:
http://docs.oracle.com/cd/E53569_01/tutorials/tut_jdev_maf_persistance/tut_jdev_maf_persistance.html
AMPA ist eine Open-Source Lösung und kann auf GitHub gefunden werden:
https://github.com/oracle/mobile-persistence

 

SQLite Datenbank & Model Changes

Beim Beenden des AMPA Wizards wird ein SQLite Skript erstellt. Dieses Skript wird bei der initialen Installation der Anwendung ausgeführt und wird dazu verwendet die Datenbankstruktur, die Tabellen, etc. zu erstellen.
Die entsprechende Datei findet man im ApplicationController Projekt, unter src/META-INF/<name_of_your_project>.sql.

Von der SQLite Datenbanksicht aus, ist die offensichtliche Lösung das Hinzufügen einer neuen Spalte für jede Tabelle, die für den Benutzer relevante Daten in Form des Benutzernamens speichert.
Zum besseren Verständnis betrachten wir das folgende Scenarioi: Eine Tabelle wird dazu verwendet die Projekte, in denen ein Benutzer arbeitet zu speichern. Es ist möglich, dass das gleiche Projekt zwei oder mehreren Benutzern zugeordnet ist; beziehungsweise zwei oder mehr Benutzer können in dem gleichen Projekt arbeiten.
Dies hat als Konsequenz, dass der Primärschlüssel der Datenbanktabelle um die neue Spalte Benutzernamen erweitert werden muss. So ist es möglich, das gleiche Projekt mehreren Benutzern zuzuweisen.

Für unsere Projekttabelle fügen wir daher Folgendes ein:

[dt_code]CREATE TABLE PROJECTS
(
PROJECT_DEF VARCHAR NOT NULL,
USERNAME VARCHAR NOT NULL,
PROJECT_NAME VARCHAR ,
— Weitere Attribute —
CONSTRAINT PROJECTS_PK PRIMARY KEY(PROJECT_DEF, USERNAME)
); [/dt_code]

Wir müssen dieses Attribut für den Benutzernamen in der Entity, die AMPA für uns erstellt hat anlegen mit den dazugehörigen Zugriffsmethoden.

public class Projects extends Entity {
  //….
  private String username;
 
  public void setUsername(String username) {
    this.username = username;
  }
 
  public String getUsername() {
    return username;
  }
  //…
}

Wenn wir nun das Projekt für einen bestimmten Benutzer abrufen möchten, würden wir normalweise eine Abfrage mit einer Where Klausel eingeben, die angibt, dass der Benutzername identisch ist mit dem Benutzername mit dem wir eingeloggt sind.
Eine Abfrage wie die Folgende:

[dt_code]select * from projects where username=”max_mustermann”; [/dt_code]

AMPA übernimmt jedoch die SQLite Interaktion für uns, also lassen Sie uns schauen wie wir das gleiche Ergebnisse mit AMPA erzielen können.
Für jede Entity mit der Sie arbeiten möchten – in diesem Fall, Projekt – erstellt AMPA eine Service-Klasse für die Interaktion mit der Datenbank und den WebServices. Diese Klasse wird immer erweitert von EntityCRUDService<T> (T – Entity Typ, in diesem Fall, Projekte).

EntityCRUDService beinhaltet eine Methode namens executeLocalFindAll(), die jedes Mal ausgeführt wird, wenn Projekte von der Datenbank abgerufen werden. Dies sieht nach einem guten Platz aus, um die Ergebnisse einzuschränken und zu filtern, damit wir nur die Projekte erhalten, die zum aktuellen Benutzernamen gehören.

Überschreiben Sie die Methode in der ProjectsService Klasse mit einem Code wie diesem:

@Override
protected List<Projects> executeLocalFindAll() {
  DBPersistenceManager pm = getLocalPersistenceManager();
 
  //find() method supports sending a list with criteria to filter the query on.
  //Add the username to the list criteria.
  ArrayList<String> searchAttrs = new ArrayList<String>();
  searchAttrs.add("username");
  //Username is retrieved from memory or some other place.
  String userName = SecurityUtils.getCurrentUsername();
  List<Projects> tempProjects = pm.find(Projects.class, userName, searchAttrs);
 
  //find() method uses a LIKE operator, which means that there could be a case
  //where a username string contains another. e.g: maxm and maxmustermann
  //To avoid this, we do a filtering on the Java objects, to only retain the
  //ones which fully-match the username, not only partially.
 
  tempProjects = filterProjectsByUsername(tempProjects, userName);
  return tempProjects;
}

filterProjectsByUsername() wird hier nicht explizit beschrieben oder dargestellt, aber es ist recht eindeutig was diese Methode genau macht. Wir erhalten nur Projekte, die die gleichen Eigenschaften beim Benutzernamen haben wie die übergebenen Parameter.

AMPA WebService Mappings

Bisher haben wir darüber gesprochen wie man benutzerspezifische Daten auf mobile Geräten speichert. Aber wir müssen auch Änderungen in Bezug auf die Abfrage von Daten der WebServices durch AMPA vornehmen.
In AMPA hat jede Entity – wie Projekte – einen dazugehörigen WebService Call, um diese vom Server abzurufen.
Jede Entity, die vom Server abgerufen wird und in der Datenbank beibehalten wird, muss ein One‑to-One Mapping zwischen persistierten Attributen und Attributen, die vom Server abgerufen werden besitzen.

Dieses Mapping zwischen Attributen in Java Entities, Spaltennamen und Feldern, die vom Server kommen, wird in der persistence-mapping.xml vorgenommen. Diese Datei wird automatisch vom AMPA Wizard generiert und muss in den meisten Fällen nicht bearbeitet werden. Jedoch müssen wir ein paar Änderungen vornehmen, um unser Szenario vollständig unterstützen zu können.
Die Datei ist zu finden im ApplicationController Projekt, unter src/META-INF/persistence-mapping.xml.

Jede Entity wird im XML durch das classMappingDescriptor Tag beschrieben. Unter diesem Tag kann die Tabelle, die WebService Endpunkte und das Attribut Mapping gefunden werden. Wir sind besonders an den Attribut Mappings interessiert.
Die Attribute sind unter dem <attributeMappings> Tag zu finden mit Einträgen zu jedem Attribut:

[dt_code]<directMapping attributeName=”projectName” payloadAttributeName=”project_name” columnName=”PROJECT_NAME” columnDataType=”VARCHAR” required=”false” persisted=”true” javaType=”java.lang.String” keyAttribute=”false”/> [/dt_code]

Ansteuern des RestJSONPersistenceManager zum Beibehalten des Benutzernamens bei der Übertragung

Nun haben wir das Problem, dass wir hier ein Attribut als Mapping festlegen müssen, um den aktuellen Benutzernamen in der Datenbank für jedes Projekt zu speichern. Der Benutzername wird jedoch nicht vom WebService Call zurückgegeben, wir erhalten nur die sonstigen Daten zum Projekt. Beim Benutzernamen handelt es sich um ein Feld, das wir manuell in unserer Datenbank und Entity erstellt haben.

Wir haben die Möglichkeit beim Attribut festzulegen, dass dies nicht im Payload vorhanden sein muss. Wir können dies über das required=”false” Attribut festlegen. Was nun fehlt ist einen Weg zu finden dem Framework mitzuteilen, dass auch ohne das Attribut für den Benutzernamen aus der Rückmeldung des WebService, der aktuelle Benutzername bei der Übertragung in die Datenbank gespeichert werden soll.

Wir können kein kundenspezifisches Attribut hinzufügen, da es gegen das Schema verstößt, aber es gibt folgenden Trick:

[dt_code]<directMapping attributeName=”username” payloadAttributeName=”username” columnName=”USERNAME” columnDataType=”VARCHAR” required=”false” persisted=”true” javaType=”java.lang.String” keyAttribute=”true” xml:ELExpression=”#{MbSecurityContext.username}”/> [/dt_code]

Beachten Sie den xml:ELExpression=”#{MbSecurityContext.username}” Teil.

Im Grund möchten wir, dass der Ausdruck ausgeführt wird, wenn das Attribut verarbeitet wird. Dies würde uns ermöglichen durch die Ausführung des Ausdrucks den aktuellen Benutzernamen zu erhalten. Wir sehen als nächstes was wir dafür ändern müssen, um dies zu erzielen.

Beim Durchsuchen des AMPA Codes bemerkten wir eine Methode im RestJSONPersistenceManager, welche aufgerufen wird nachdem die Antwort vom WebService empfangen und bevor die Entities in der Datenbank gespeichert wurden. Diese Methode heißt: createBindParamInfoFromPayloadAttribute(). Sie erstellt eine sogenannte BindParamInfo, die Informationen über gespeicherte Werte für jedes Attribut in der Datenbank enthält.
Wir müssen diese Methode in unserer eigenen Kundenklasse überschreiben. Hierfür müssen wir eine Klasse erstellen, die erweitert wird durch RestJSONPersistenceManager.
Erstellen Sie diese Klasse im ApplicationController Projekt.
Als nächstes müssen wir das AMPA Framework darüber in Kenntnis setzen, dass wir unsere neue Kundenklasse anstellen von RestJSONPersistenceManager verwenden möchten. Wir setzen dies um durch Änderung des remotePersistenceManager Attributs im crudServiceClass Tag im persistence-mapping.xml:

[dt_code]<crudServiceClass className=”de.virtual7.mobile.model.service.ProjectsService” autoIncrementPrimaryKey=”false” localPersistenceManager=”oracle.ateam.sample.mobile.v2.persistence.manager.DBPersistenceManager” remotePersistenceManager=”de.virtual7.ampa.persistence.manager.CustomRestPersistenceManager” remoteReadInBackground=”true” remoteWriteInBackground=”true” showWebServiceInvocationErrors=”false” autoQuery=”true”/> [/dt_code]

Nun der Code für die Methode, die überschrieben wird:

@Override
protected BindParamInfo createBindParamInfoFromPayloadAttribute(Class entityClass, AttributeMapping mapping, Object rawValue) {
  if (rawValue == null || "".equals(rawValue.toString().trim()) || rawValue == JSONObject.NULL) {
    //Normally, the super method would return null here. But we need to check if    
    //the attribute mapping contains a mapping that we can process.
 
    String elExpressionValue = mapping.getAttributeStringValue("xml:ELExpression");
    if (elExpressionValue != null) {
      rawValue = AdfmfJavaUtilities.getELValue(elExpressionValue);
    }
  }
  BindParamInfo bindParamInfo = super.createBindParamInfoFromPayloadAttribute(entityClass, mapping, rawValue);
  return bindParamInfo;
}

Wir unterbrechen nun die Attributsberechnung, prüfen den Attributsnamen, ob es unser Kundenname ist und falls zutreffend, berechnen wir den Ausdruck. Auf diese Weise ordnen wir den Wert für den Benutzernamen bei der Übertragung zu.