DynamicCustomMapping

Last modified by Daniel Ullfig on 2020/01/28 14:35

Dynamic Custom Mapping

If you are administrating an instance of XWiki, do you want any of the following?

  • Have more control over the types of queries you use.
  • Import large amounts of structured data.
  • See small gains in performance with large numbers of objects of a given class.
  • Be able to manipulate XWiki on a Hibernate/Database level.
    If so, then you may find custom mapping useful.

For this to make sense, it would be helpful to have some knowledge of Hibernate database mapping.

How much simpler can the queries be with custom mapping?
Suppose you have a class called Employee and each Employee object has a familyName property. Now you want a list of the familyNames of the Employees.
Normally you would need a query like this:

SELECT prop.value FROM BaseObject AS obj, StringProperty AS prop
WHERE obj.className='XWiki.Employee' AND prop.id.id=obj.id AND prop.name='familyName'

But thanks to custom mapping your query could be as simple as:

SELECT e.familyName FROM Employee AS e

The simplistic query method currently does not work.

To learn about custom mapping and dynamic custom mapping, we will make a class of objects which will be custom mapped. For our class, we will use our Employee example.

Before we begin, you need to have a username with administrator privileges on your wiki, and you will need to change xwiki.cfg so that the line which says:
# xwiki.store.hibernate.custommapping.dynamic=0
instead says:
xwiki.store.hibernate.custommapping.dynamic=1

Objects of our Employee class will have 5 properties. Each property has a type, it may be a text string or an integer or a Date among many others. The properties for our Employee class will be as follows.

NameType
christianNameString
familyNameString
jobDescriptionTextArea
supervisorIdNumber
dateHiredDate

There is one other property which all objects have and that is a ID. the ID is always a number and the number type is integer. The supervisorId property will refer to the ID of the employee's supervisor.

First lets make the class. To create a class we need to create a document. Lets put it in the XWiki space because it has a special use. When we create the document we are prompted to add some content, instead of adding content we will leave the content window blank and click on the "Class" button on the right-hand menu. Now there is a panel in the right menu which says "Add Property" on that panel there is a drop-down menu to select the type and a text field to enter the name of our property. Select the type "String" and enter the name "christianName" and click the "Add Property" button. Repeat for each of the above properties.

Now there is one thing which must be changed or else you will get a confusing error when you try to use custom mapping. You should see a list of your properties on the left side, click on the supervisorId property and you will see a few test fields and a drop-down menu labelled "Number Type" change this from "long" to "integer". If this is different from the custom mapping, the database will not except the objects when you try to save them.

Now click "save and continue" and we will make our custom mapping.

There is no fancy custom mapping injector so we will do it with our own groovy script. Click on the wiki button in the right menu it edit the content of our class page, and paste in the following script.

{{groovy}}
import com.xpn.xwiki.doc.XWikiDocument;
//we don't want to inject mappings every time you view the page.
//nor do we want non-admin users running this script
if(xcontext.getRequest().get("injectMapping")!=null
  && xwiki.hasAdminRights()){
   //get some objects.
   xc = xcontext.getContext();
    wiki = xc.getWiki();
    thisClass = doc.getDocument().getxWikiClass()
   //set the mapping
   thisClass.setCustomMapping(
       "<property name=\"christianName\" type=\"string\" column=\"CHRISTIAN_NAME\" length=\"255\"/>"+
       "<property name=\"familyName\" type=\"string\" column=\"FAMILY_NAME\" length=\"255\" />"+
       "<property name=\"jobDescription\" type=\"string\" column=\"JOB_DESCRIPTION\" length=\"60000\" />"+
       "<property name=\"supervisorId\" type=\"integer\" column=\"SUPERVISOR_ID\" />"+
       "<property name=\"dateHired\" type=\"timestamp\" column=\"DATE_HIRED\" />"
   );

   //XWiki provides us with a way to test our custom mapping!
   if(thisClass.isCustomMappingValid(xc)){
       //We must save the document with the new custom mapping set.
       doc.save();
       //This will make Hibernate create the new database table.
       wiki.getHibernateStore().updateSchema(thisClass, xc);
        println("Done :)");
   }else{
        println("The mapping is not valid, consult your error log to find the problem.")
   }
}else{
    println("To inject custom mapping for this class, click [[here>>"+
            doc.getFullName()+"?injectMapping]]");
}
{{/groovy}}

A few notes: You are passing a string of text to setCustomMapping so it must be surrounded by quotes. I have put it on separate lines be ending the quote and adding a + to the end to make it concatenate the next line on to the last. Also because you must surround the text with quotes, you have to use backslashes to "escape" the quotation marks which are part of the custom mapping '"' becomes '\"' and that way Java doesn't become confused over which quotes it should honour.

Also notice how instead of TextArea, Number, and Date, the mappings say "string", "integer", and "timestamp"? This is because Hibernate and the underlying database don't use the same classes as XWiki. Remember I said you need to change the number type to integer? That is because the mapping specifies an integer. Also notice that our mapping specifies the length of the strings, some databases require the user to specify the maximum size of a string before using it, and our jobDescription textArea leaves lots of extra room with 60,000 characters of space.

XWiki will take our mappings and add something to the top and bottom of them.
to the top it will add:

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
   <class entity-name="XWiki.Employee" table="xwikicustom_xwiki_employee">
       <id name="id" type="integer" unsaved-value="undefined">
           <column name="XWO_ID" not-null="true" />
           <generator class="assigned" />
       </id>

To the bottom it will simply add:

    </class>
</hibernate-mapping>

The name and table are formed by the name of our class. It is named "XWiki.Employee" because our class is in the space "XWiki" the table is the same as class name except it is all lowercase, starts with "xwikicustom_" and has it's periods turned to underscores.

If you wanted to add this as regular (non-dynamic) custom mapping, you would save the following as a .hbm.xml file in your WEB-INF folder, reference it in your hibernate.hbm.xml file and set the custom mapping of the class to "internal".

<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD//EN"
        "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
   <class entity-name="XWiki.Employee" table="xwikicustom_xwiki_employee">
       <id name="id" type="integer" unsaved-value="undefined">
           <column name="XWO_ID" not-null="true" />
           <generator class="assigned" />
       </id>
       <property name="christianName" type="string" column="CHRISTIAN_NAME" length="255"/>
       <property name="familyName" type="string" column="FAMILY_NAME" length=\"255" />
       <property name="jobDescription" type="string" column="JOB_DESCRIPTION" length="60000" />
       <property name="supervisorId" type="integer" column="SUPERVISOR_ID" />
       <property name="dateHired" type="timestamp" column="DATE_HIRED" />
   </class>
</hibernate-mapping>

However, that is not the topic of this document, if you want more information on non-dynamic custom mapping, refer to CustomMapping.

Now that you have an understanding of how custom mapping works, lets hire some employees! Create a new wiki page and in the edit window click the "objects" button (just above the "class" button) and find the Add Object panel on the right. Click the drop-down menu labelled  "Select Class" and find your Employee class. When you have selected Employee, click "Add object from this class" and you should see the fields which you had made. If you recieve an error at this point, it is probably because either A. dynamic custom mapping is not turned on in your xwiki.cfg file (which requires restarting the server to reload) or B. the Number property for supervisorId is a long and not an integer, go back and check. You should see a set of fields for each of the properties which you created. Go ahead and set the values to something, remember supervisorId has to be a number.

Now lets test. Open up an sql command window in your database manager, and run this query:

SELECT * FROM xwikicustom_xwiki_employee;

You should see your new employee with the values you entered and a value called XWO_ID which contains a random looking number.

Now go back to the wiki, create a new page and put in this code:

At the moment, this query code doesn't work.

{{groovy}}
xc = xcontext.getContext();
for (String name : xc.getWiki().search("SELECT e.familyName from Employee e", xc)) {
    println(name);
}
{{/groovy}}

You should see the familyName of the employee which you added.

That is how dynamic custom mapping works, it can make your queries much smaller, give you small improvements in database load time and give you better control of your data. I hope you have found this tutorial helpful.

Tags:
   

Get Connected