Mapping between tables and XML documents
We describe the above by using an example. Let's assume we
have a table "httpSession" in which we store the sessions
of an HTTP servers current users. Each user has choosen a
language in which he wants his HTML pages to be displayed.
Thus we have the following tables:
CREATE TABLE language (
id INT NOT NULL IDENTITY,
language VARCHAR(10) NOT NULL,
country VARCHAR(10) NOT NULL,
PRIMARY KEY (id),
UNIQUE (language, country)
);
CREATE TABLE httpSession (
id INT NOT NULL IDENTITY,
ipaddress VARCHAR(15) NOT NULL,
logintime TIMESTAMP NOT NULL,
lastaction TIMESTAMP NOT NULL,
expiretime SMALLINT NOT NULL,
cookie VARCHAR(25) NOT NULL,
languageid INT NOT NULL,
PRIMARY KEY (id),
UNIQUE (cookie),
FOREIGN KEY (languageid) REFERENCES language (id)
);
A Schema for JDBC mapping
This situation is just what JaxMe was made for: The secondary
table "language" will typically be subject to manual modifications
only. The "httpSession" table, however, is updated all the time.
To access these tables, we again create some kind of XML schema:
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema
xmlns:xs="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://jaxme.ispsoft.de/namespaces/examples/Session"
xmlns:jm="http://ispsoft.de/namespaces/jaxme/schema"
xmlns:jdbcm="http://ispsoft.de/namespaces/jaxme/jdbc-mapping">
<xs:annotation>
<xs:appinfo>
<jm:defaults package="com.mycompany.session"/>
<jdbcm:defaults driver="org.hsqldb.jdbcDriver"
url="jdbc:hsqldb:build/db/db"
user="sa" password=""/>
</xs:appinfo>
</xs:annotation>
<xs:element name="Session">
<xs:annotation>
<xs:appinfo>
<jdbcm:select
tables="httpSession JOIN language ON httpSession.languageid=language.id"
/>
<jdbcm:table name="httpSession"/>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element name="Language">
<xs:annotation>
<xs:appinfo>
<jdbcm:table name="language"/>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence/>
</xs:complexType>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Now suggest the following row in the "httpSession" table:
But now, how do we get this? We'll use some examples to demonstrate
working with the Session document.
Creating classes for JDBC
Using the source generator is not so different. In short, you
have to change the SchemaReader and SourceGenerator:
java de.ispsoft.jaxme.generator.Main Session.xsd
--schemaReader=de.ispsoft.jaxme.generator.XsdJdbcSchemaReader
--sourceWriter=de.ispsoft.jaxme.generator.JdbcJavaSourceWriter
As before, you will find classes "ClsSession", "ClsSessionHandler",
"ClsLanguage" and "ClsLanguageHandler" being generated. But this
time, there is an additional class "ClsSessionManager". It is
responsible for access to the database.
Note: If you are using an Oracle database, you might
need an additional argument
--option=jdbc.dbmode=Oracle
Reading data from a relational database
First of all, we have to specify database driver, JDBC URL,
user name and password, like in any other JDBC application. This
is done in an XML file:
<?xml version="1.0" encoding="UTF-8" ?>
<JMManagerConfiguration
xmlns="http://jaxme.ispsoft.de/namespaces/JMManagerConfiguration">
<Defaults>
<JMJdbcManager>
<Driver>org.hsqldb.jdbcDriver</Driver>
<Url>jdbc:hsqldb:build/db/db</Url>
<User>sa</User>
<Password/>
</JMJdbcManager>
</Defaults>
<Configuration
localName="Session"
namespaceURI="http://jaxme.ispsoft.de/namespaces/examples/Session">
<ManagerClass>com.mycompany.session.ClsSessionManager</ManagerClass>
<ElementClass>com.mycompany.session.ClsSession</ElementClass>
<HandlerClass>com.mycompany.session.ClsSessionHandler</HandlerClass>
</Configuration>
</JMManagerConfiguration>
Note that the given values are just defaults and can be overwritten
per document type by adding subelements Driver, Url,
and so on to the respective Configuration section. For
example, to use the same settings for the Session Document,
but a different user name, you would use
<Configuration
localName="Session"
namespaceURI="http://jaxme.ispsoft.de/namespaces/examples/Session">
<ManagerClass>com.mycompany.session.ClsSessionManager</ManagerClass>
<ElementClass>com.mycompany.session.ClsSession</ElementClass>
<HandlerClass>com.mycompany.session.ClsSessionHandler</HandlerClass>
<User>otheruser</User>
</Configuration>
This file is best inserted into your classpath somewhere, for example,
as a part of a JAR file. Assuming, that your configuration file is
in com/mycompany/Configuration.xml, you could write the
following program, that writes all current HTTP sessions to System.out,
much like the "AddressPrinter":
import com.mycompany.session.ClsSession;
import de.ispsoft.jaxme.*;
public class SessionPrinter {
public static void main(String[] args) throws Exception {
System.setProperty(JMFactory.URI_PROPERTY,
"resource:com/mycompany/Configuration.xml");
JMManagerFactory factory = JMFactory.getJMManagerFactory();
JMManager manager = factory.getManager(ClsSession.NAMESPACE_URI,
ClsSession.LOCAL_NAME);
String query = "LASTACTION + EXPIRETIME > NOW()"; // A WHERE clause
for (java.util.Iterator iter = manager.select(query);
iter.hasNext(); ) {
ClsSession session = (ClsSession) iter.next();
System.out.println(session.toXML());
}
}
}
You could also read the configuration from a file or URL by changing
the property value to
file://c:\temp\Configuration.xml
or
http://configurations.company.com/Configuration.xml
And you could, of course, specify the property value from the command
line and not within the program.For details, see the
Writing data to a relational database
Now an example that creates a new HTTP session. It uses
the same property file as before:
import com.mycompany.session.ClsSession;
import de.ispsoft.jaxme.*;
public class SessionAdd {
public static void main(String[] args) throws Exception {
System.setProperty(JMFactory.URI_PROPERTY,
"resource:com/mycompany/Configuration.xml");
JMManagerFactory factory = JMFactory.getJMManagerFactory();
JMManager manager = factory.getManager(ClsSession.NAMESPACE_URI,
ClsSession.LOCAL_NAME);
ClsSession session = (ClsSession) manager.create();
session.eID = new Integer(126776);
session.eIPADDRESS = "134.23.1.7";
session.eLOGINTIME = new java.sql.Timestamp(System.currentTimeMillis());
session.eLASTACTION = session.eLOGINTIME;
session.eEXPIRETIME = new Short((short) (15*60));
session.eCOOKIE = "jH63fGdx";
session.eLANGUAGEID = new Integer(3);
manager.insert(session);
}
}
A similar example, the update of a row:
import com.mycompany.session.ClsSession;
import de.ispsoft.jaxme.*;
public class SessionUpdate {
public static void main(String[] args) throws Exception {
System.setProperty(JmFactory.URI_PROPERTY,
"resource:com/mycompany/Configuration.xml");
JMManagerFactory factory = JMFactory.getJMManagerFactory();
JMManager manager = factory.getManager(ClsSession.NAMESPACE_URI,
ClsSession.LOCAL_NAME);
java.util.Iterator iter = manager.select("COOKIE='jH63fGdx'");
if (iter.hasNext()) {
ClsSession session = (ClsSession) iter.next();
session.eLASTACTION = new java.sql.Timestamp(System.currentTimeMillis());
manager.update(session);
}
}
}
And finally, as you have probably guessed, an example for
deleting a row:
import com.mycompany.session.ClsSession;
import de.ispsoft.jaxme.*;
public class SessionDelete {
public static void main(String[] args) throws Exception {
System.setProperty(JMFactory.URI_PROPERTY,
"resource:com/mycompany/Configuration.xml");
JMManagerFactory factory = JMFactory.getJMManagerFactory();
JMManager manager = factory.getManager(ClsSession.NAMESPACE_URI,
ClsSession.LOCAL_NAME);
java.util.Iterator iter = manager.select("COOKIE='jH63fGdx'");
if (iter.hasNext()) {
ClsSession session = (ClsSession) iter.next();
manager.delete(session);
}
}
}
Creating a primary key for immediate use
If you want to create a primary key that should be used
immediately after the INSERT statement, then you have to
specify an additional SELECT statement. For example:
<jdm:table name="httpSession" keys="ID">
<jdm:createKeyQuery>
SELECT httpSessionSequence.nextval()
</jdm:createKeyQuery>
</jdm:table>
The above example would issue the SELECT statement before an
insert. The generated value would be written into the ID
column. Finally the INSERT statement would be performed.
Once the operation is done, the application could access the
generated value in the ID column.
In some cases the order of INSERT and SELECT has to be
reversed. For example the MySQL database has a nice feature
called autoincrement column. With autoincrement columns the
above example might look like
<jdm:table name="httpSession" keys="ID">
<jdm:createKeyQuery postInsert="true">
SELECT LAST_INSERT_ID()
</jdm:createKeyQuery>
</jdm:table>
This example would work quite the same, except that the
SELECT statement would be performed after the INSERT. (postInsert)
Again, the application might read the generated value from
the ID column.
For combined keys the example could work quite the same.
The only difference would be, that the SELECT statement had
to return multiple values, one for each column.
TODO: Provide an example for createKeyQuery.
Find methods
In most cases you would like to specify certain fixed queries,
that are performed frequently. This is possible by specifying
find methods:
<jdbcm:select
tables="httpSession JOIN language ON httpSession.languageid=language.id"
/>
<jdbcm:findMethod name="findByIpAddress" query="IPADDRESS = ?">
<jdbcm:param name="pIpAddress" type="xs:string"/>
</jdbcm:findMethod>
<jdbcm:findMethod name="findByLanguageId" query="LANGUAGEID = ?"
maxResultSize="1">
<jdbcm:param name="pLanguage" type="xs:integer"/>
</jdbcm:findMethod>
<jdbcm:table name="httpSession"/>
In the above case the generated manager would have additional methods
java.util.List findByIpAddress(String pIpAddress);
ClsSession findByLanguage(Integer pLanguage);
These are mainly shortcuts for
java.util.List select("IPADDRESS = ?",
new Object[]{JMManager.VARCHAR, pIpAddress});
java.util.List select("LANGUAGE = ?",
new Object[]{JMManager.INTEGER, pLanguage});
However, note that in the findByLanguage example the interface is
specified to return a single instance, which is much more convenient.
It is also possible to specify an attribute minResultSize.
This would throw a SAX exception, if the generated result size is
below the given number.
Find methods are particularly useful in conjunction with the
BeanWriter: The generated entity beans have the same find methods,
that the manager has.
As in the case of the views, find methods may have a dynamic
part:
<jdbcm:findMethod name="findByMatchingColumn"
query="{MatchColumn} LIKE ?">
<jdbcm:pattern name="pMatchColumn" sample="SomeColumn"
match="{MatchColumn}/>
<jdbcm:param name="pValue" type="xs:integer"/>
</jdbcm:findMethod>
And, of course, you may also make use of additional placeholder
values:
<jdbcm:findMethod name="findByWhatever"
query="{Whatever}">
<jdbcm:pattern name="pWhatever" sample="1=1"
match="{Whatever}/>
<jdbcm:placeholders name="pValues"/>
</jdbcm:findMethod>
Mapping between column names and elements or attributes
By default the generator maps table columns to elements
or attributes with the same name. If a matching element
or attribute cannot be found, it creates one. This is
convenient, but in most cases not what you actually want.
The main problem is that the generator sees column names
in all uppercase letters. (This is how most RDBMS supply
their metadata.) But you don't want your attribute be called
IPADDRESS, you want IpAddress. This can be fixed by
explicitly specifying a certain spelling:
<xs:element name="Session">
<xs:annotation>
<xs:appinfo>
<jdbcm:table name="httpSession"/>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<element name="Id"/>
<element name="Language"/>
<element name="Country"/>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
This example will generate a class ClsSession with elements
Id
, Language
and Country
,
mapped to the columns with the same names. Originally the
elements have been named ID
, LANGUAGE
and COUNTRY
, so that looks much better now.
However, we are still not satisfied. What it we want the
element Country
to have the name
CountryCode
? This can be done by using an
jdm:column
clause:
<xs:element name="Session">
<xs:annotation>
<xs:appinfo>
<jdbcm:table name="httpSession"/>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<element name="Id"/>
<element name="Language"/>
<element name="CountryCode">
<xs:annotation>
<xs:appinfo>
<jdbcm:column name="Country">
</xs:appinfo>
</xs:annotation>
</element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>
Mapping of table names
Table name mapping is a feature which you will use very rarely. It is best explained
by looking at an instance of the "organization" document, as specified in the
../examples/misc/jdbcref.xsd
example:
<organization id="4" name="Foo Inc." emailId="3">
<email id="3" emailAddress="a3@foo.com"/>
<person id="57" fullName="Smith, Harry" emailId="2">
</email id="2" emailAddress="a2@foo.com"/>
</person>
</organization>
The example uses three tables: "organization", "person", and "email". The important
point is that the "email" table is being used twice: It is referenced by both the
"organization" and "person" tables.
But why is this causing trouble? To understand that point, we take a look at the
definition of the person document:
<xs:element name="person">
<xs:annotation>
<xs:appinfo>
<jdm:table name="Person"/>
<jdm:select tables="Person left outer join Email on Person.emailId=Email.Id"/>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence><xs:element ref="email"/></xs:sequence>
<xs:attribute name="id"/>
<xs:attribute name="fullName"/>
<xs:attribute name="emailId"/>
</xs:complexType>
</xs:element>
To read instances of this document, JaxMe will generate the following query:
SELECT EMAIL.ID, EMAIL.EMAILADDRESS, PERSON.ID, PERSON.FULLNAME, PERSON.EMAILID
FROM Person left outer join Email on Person.emailId=Email.Id
In other words: It takes the table and column names, and creates a list
of these names. But in the case of the "organization" table, this
list would look like the following:
SELECT EMAIL.ID, EMAIL.EMAILADDRESS, EMAIL.ID, EMAIL.EMAILADDRESS, PERSON.ID,
PERSON.FULLNAME, PERSON.EMAILID, ORGANIZATION.ID, ORGANIZATION.NAME,
ORGANIZATION.EMAILID, ORGANIZATION.PERSONID FROM Organization left
outer join Email on Person.emailId=Email.id left outer join Person
on Organization.personId=Person.id left outer join Email on
Person.emailId=Email.id
The first pair of EMAIL.ID and EMAIL.EMAILADDRESS would be to read the
organizations email address, and the second pair is reading the
persons. But this is obviously wrong, because they would always be the
same!
To fix the problem, SQL suggests the use of a table alias. The above
query is fine, if we reformulate it as follows:
SELECT EMAIL.ID, EMAIL.EMAILADDRESS, Email0.ID, Email0.EMAILADDRESS, PERSON.ID,
PERSON.FULLNAME, PERSON.EMAILID, ORGANIZATION.ID, ORGANIZATION.NAME,
ORGANIZATION.EMAILID, ORGANIZATION.PERSONID FROM Organization left
outer join Email on Person.emailId=Email.id left outer join Person
on Organization.personId=Person.id left outer join Email as Email0 on
Person.emailId=Email0.id
And this is exactly what JaxMe assumes: If the same table name
tableName is used multiple times in a single, complex
document, then the generator will use the aliases tableName0,
tableName1, ... for the second, third, ... instance. In
other words, a proper declaration of the "organization" document would
look like this:
<xs:element name="organization">
<xs:annotation>
<xs:appinfo>
<jdm:table name="Organization"/>
<jdm:select
tables="Organization left outer join Email on Organization.emailId=Email.Id
left outer join Person on Organization.personId=Person.Id
left outer join Email AS Email0 on Person.emailId=Email0.Id"/>
</xs:appinfo>
</xs:annotation>
<xs:complexType>
<xs:sequence>
<xs:element ref="email"/>
<xs:element ref="person"/>
</xs:sequence>
<xs:attribute name="id"/>
<xs:attribute name="name"/>
<xs:attribute name="emailId"/>
<xs:attribute name="personId"/>
</xs:complexType>
</xs:element>