Inheritance
Inheritance
Skyve supports inheritance between documents, to maximise re-use, and improve quality.
If you have a number of documents that have a subset of common attributes, you can declare an abstract document with the common attributes, and then other documents that extend this, inheriting behaviours and taking advantage of reusable view components.
The abstract document is declared with the abstract
attribute in the document, and with a persistence strategy - this defines the way in which the data will be persisted for the abstract, and inheriting documents.
Note that you can also inherit from documents that are not declared as abstract
using a persistence strategy.
There are three persistence strategies available:
- joined - inherited attributes will be in one table - and subtype documents will join to this table (handled automatically by Skyve)
- mapped - the document is a common superclass - all inherited attributes will be persisted as columns in tables for each subtype document, and there will be no table for the abstract document
- single - data for all documents will be persisted in a single table with a
bizDiscriminator
column (handled automatically by Skyve)
Example
Consider a situation where your project will handle a number of different finance items:
- invoice items
- fee items
with each that will require common attributes:
- description
- amount excluding tax
- amount of tax
- amount including tax
In this case, we can declare an abstract document AbstractFinanceItem with the common attributes:
<document name="AbstractFinanceItem" ...
<abstract>true</abstract>
<persistent strategy="mapped" />
...
<attributes>
<textField name="description">
<displayName>Description</displayName>
<length>100</length>
</textField>
<decimal2 name="amountExcludingTax">
<displayName>Ex Tax</displayName>
<converterName>Decimal2DollarsAndCents</converterName>
</decimal2>
<decimal2 name="amountOfTax">
<displayName>Tax</displayName>
<converterName>Decimal2DollarsAndCents</converterName>
</decimal2>
<decimal2 name="amountIncludingTax">
<displayName>Inc Tax</displayName>
<converterName>Decimal2DollarsAndCents</converterName>
</decimal2>
</attributes>
and the subtype (i.e. inheriting) documents are declared as extending that document with additional attributes if required:
e.g, for InvoiceItem
<document name="InvoiceItem" ...
<extends document="AbstractFinanceItem"
<persistent name="FIN_InvoiceItem" />
...
<attributes>
<integer name="quantity">
<displayName>Quantity</displayName>
</integer>
...
e.g, for FeeItem
<document name="FeeItem" ...
<extends document="AbstractFinanceItem"
<persistent name="FIN_FeeItem" />
...
<attributes>
<enum name="feeType">
<displayName>Fee Type</displayName>
<values>
...
...
This means that InvoiceItem will have all of the attributes of AbstractFinanceItem as well as the quantity attribute. Because the mapped
strategy is used, all columns for InvoiceItem (including the inherited columns) will be in the table FIN_InvoiceItem.
Similarly FeeItem will have all of the attributes of AbstractFinanceItem as well as the feeType attribute. Because the mapped
strategy is used, all columns for FeeItem (including the inherited columns) will be in the table FIN_FeeItem.
Persistence
Using the mapped
strategy, the database tables will have the following columns (for clarity other implicit columns are not shown here):
FIN_InvoiceItem | FIN_FeeItem |
---|---|
description | description |
amountExcludingTax | amountExcludingTax |
amountOfTax | amountOfTax |
amountIncludingTax | amountIncludingTax |
quantity | feeType |
If the joined
strategy had been selected, a persistence name (e.g. FIN_FinanceItem) would be declared for the abstract document as well as the subtype documents, the following tables would be created:
FIN_FinanceItem | FIN_InvoiceItem | FIN_FeeItem |
---|---|---|
description | ||
amountExcludingTax | ||
amountOfTax | ||
amountIncludingTax | ||
quantity | ||
feeType |
In the joined
strategy, corresponding rows in the participating tables will have the same bizId
value.
If the single
strategy had been selected, the following tables would be created, a persistence name is only declared for the abstract document:
FIN_FinanceItem |
---|
bizDiscriminator |
description |
amountExcludingTax |
amountOfTax |
amountIncludingTax |
quantity |
feeType |
Note - if you change persistence strategies after deploying, depending on the database dialect, automatic DDL may not be able to manage the change, so you may need to drop your tables and let the automatic DDL recreate them.
Module permissions
Permissions for the abstract document are not required to be declared explicitly in the module.xml, unless the abstract document includes content attributes - in this case, you will need to declare permissions so that the content attribute can be managed.
For example, with the mapped
strategy, (assuming the abstract has no content attributes), permissions would be declared for the subtype documents only:
<document name="FeeItem" permission="CRUDC"/>
<document name="InvoiceItem" permission="CRUDC"/>
Bizlet and extension
You can create an extension class for an abstract document in the usual way, for example:
public class AbstractFinanceItemExtension extends AbstractFinanceItem {
private static final Decimal2 TAX_RATE = new Decimal2("0.1");
public void calculate() {
if (getAmountIncludingTax() == null) {
if (getAmountExcludingTax() != null) {
setAmountOfTax(getAmountExcludingTax().multiply(TAX_RATE));
setAmountIncludingTax(getAmountExcludingTax().add(getAmountOfTax()));
}
} else if (getAmountExcludingTax() == null) {
if (getAmountIncludingTax() != null) {
setAmountOfTax(getAmountIncludingTax().divide(Decimal2.ONE.add(TAX_RATE)));
setAmountExcludingTax(getAmountIncludingTax().subtract(getAmountOfTax()));
}
}
}
}
Bizlets will not extend the abstract document bizlet by default, but you can define a Bizlet for the abstract document, then extend that manually. The Bizlet for the abstract document can then use methods from the extension class as follows.
In this example, the calculate method ensures tax calculations are done whenever the bean is saved.
public abstract class AbstractFinanceItemBizlet<T extends AbstractFinanceItemExtension> extends Bizlet<T>{
@Override
public void preSave(T bean) throws Exception {
bean.calculate();
super.preSave(bean);
}
}
As AbstractFinanceItem
is abstract
, it cannot be instantiated, so we also define an abstract
Bizlet
, AbstractFinanceItemBizlet
. We do not define which document the Bizlet
is for, as it can be used by all documents that extend AbstractFinanceItem
. This is done by using the generic type T
in the Bizlet
class definition, where T
we know must be a subclass of AbstractFinanceItemExtension
.
To inherit this same preSave
behaviour in the subtype (inheriting) documents, create Bizlet classes that extend the abstract Bizlet.
While the specific behaviour is defined in the abstract Bizlet
class, the behaviour won’t be inherited unless you create a Bizlet class that extends the abstract Bizlet (even if the subtype Bizlet class is empty) - so that Skyve can call the correct super class.
For example, for FeeItem
:
public class FeeItemBizlet extends AbstractFinanceItemBizlet<FeeItem>{
// behaviour inherited from super class
}
And for InvoiceItem
:
public class InvoiceItemBizlet extends AbstractFinanceItemBizlet<InvoiceItem> {
// behaviour inherited from super class
}
Because AbstractFinanceItem
does not define a class to satisfy the Bizlet <T extends Bean>
and passes on T, we can use the generic type T in the Abstract Bizlet methods so when a FeeItem
is preSaved by Skyve, FeeItem
is the type that gets passed to its super Bizlet (AbstractFinanceItem
) and satisifies the type <T extends AbstractFinanceItem>
defined by AbstractFinanceItemBizlet
.
However, because FeeItem
extends AbstractFinanceItem
, methods in AbstractFinanceItemExtension
are automatically inherited by FeeItem
whether or not a FeeItemExtension
class is declared.
So for example, the calculate() method can be used for FeeItem
as follows:
public static FeeItem createFee(Decimal2 exTaxAmount) {
FeeItem f = FeeItem.newInstance();
f.setAmountExcludingTax(exTaxAmount);
f.calculate(); // inherited from AbstractFinanceItemExtension
return f;
}
Explanation of extensions
Consider the following diagram:
For ClientAttachment as per the diagram which extends AbstractAttachment, the generated ClientAttachment.java actually extends AbstractAttachmentExtension, so it will inherit all the extension methods automatically. If we then went on to define a ClientAttachmentExtension that extends ClientAttachment, this would still inherit AbsractAttachmentExtension methods because ClientAttachment extends AbstractAttachmentExtension.
View components
You can take advantage of inheritance by creating a reusable view component for the common attributes declared in the abstract class. Using a view component is useful for ensuring a consistent experience for users.
For example, AbstractFinanceItem has attributes description, amountExcludingTax, amountOfTax, amountIncludingTax.
You can create a view component for AbstractFinanceItem as follows:
<?xml version="1.0" encoding="UTF-8"?>
<view xmlns="http://www.skyve.org/xml/view" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="_financeItem" title="Abstract Finance Item"
xsi:schemaLocation="http://www.skyve.org/xml/view ../../../../schemas/view.xsd">
<form>
<column percentageWidth="30" responsiveWidth="4" />
<column />
<row>
<item>
<default binding="description" />
</item>
</row>
<row>
<item>
<default binding="amountExcludingTax" />
</item>
</row>
<row>
<item>
<default binding="amountOfTax" />
</item>
</row>
<row>
<item>
<default binding="amountIncludingTax" />
</item>
</row>
</form>
</view>
The view component can then be used for FeeItem as follows:
<?xml version="1.0" encoding="UTF-8"?>
<view xmlns="http://www.skyve.org/xml/view" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="edit" title="FeeItem" xsi:schemaLocation="http://www.skyve.org/xml/view ../../../../schemas/view.xsd">
<component document="AbstractFinanceItem" name="_financeItem"/>
<form >
<column responsiveWidth="4"/>
<column/>
<row>
<item>
<default binding="feeType"/>
</item>
</row>
</form>
<actions>
<defaults/>
</actions>
<newParameters/>
</view>
The view component can also then be used for InvoiceItem as follows:
<?xml version="1.0" encoding="UTF-8"?>
<view xmlns="http://www.skyve.org/xml/view" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="edit" title="InvoiceItem" xsi:schemaLocation="http://www.skyve.org/xml/view ../../../../schemas/view.xsd">
<component document="AbstractFinanceItem" name="_financeItem"/>
<form >
<column responsiveWidth="4"/>
<column/>
<row>
<item>
<default binding="quantity"/>
</item>
</row>
</form>
<actions>
<defaults/>
</actions>
<newParameters/>
</view>
Next Routing and rendering
Previous Views, widgets and layout