# Join Lens
Join lenses can be used to combine multiple relations into one. Additionally, a filter expression can be provided as a join condition.
For this example, we will look at the tables museums and workers from the DuckDB database. For each individual of the :Worker class, we want to set its property :workAddress to the address of the museum they work at.
The tables have the following schemas:
museums
| column | type |
|---|---|
| museum_id | integer |
| name | string |
| address | string |
| yearly_income | integer |
| yearly_spendings | integer |
| ratings | array of floats |
workers
| column | type |
|---|---|
| worker_id | integer |
| full_name | string |
| role | string |
| museum_id | integer |
| titles | array of strings |
| access_level | integer |
Notably, the table workers has the column museum_id which references the primary key of the table museums. In SQL, we can run a JOIN query over these two tables to combine all rows of worker with their corresponding museums. In Ontop, we can instead create a join lens, that can be referenced by the mapping.
The join lens has the following structure:
{
"name": [String],
"join": {
"relations": [[String]],
"columnPrefixes": [String]
},
"columns": {
"added": [{
"name": String,
"expression": String
}],
"hidden": [String]
},
"filterExpression": String,
"type": "JoinLens"
}
The join field takes an object consisting of a list of relation references and a list of column prefixes. For each relation, its corresponding column prefix will be prepended to the names of all of its columns.
The fields columns and filterExpression work exactly the way they worked for basic lenses, with the only reference being that now, column names have to be combined with the individual relation's prefix when referencing them in expressions.
In this example, the relations we use are museums and workers, and we choose the prefixes m_ and w_ for them respectively. Since we want to perform a JOIN operation, rather than a cross-product, we also have to supply the filter expression m_museum_id = w_museum_id. After including these values, the lenses.json file should look like this:
{
"relations": [
{
"name": ["lenses", "museums_workers"],
"join": {
"relations": [
["museums"],
["workers"]
],
"columnPrefixes": [
"m_",
"w_"
]
},
"columns": ...,
"filterExpression": "m_museum_id = w_museum_id",
"type": "JoinLens"
}
]
}
The only remaining field is columns. As mentioned earlier, this field is handled analogously to the basic lens columns field, allowing the user to add and remove specific columns. For this example, we want to set the property :workAddress for each :Worker individual. Because of that, we only require the column worker_id from workers and address from museums - all other columns can be hidden, and no column has to be added:
{
"relations": [
{
"name": ["lenses", "museum_workers"],
"join": {
"relations": [
["museums"],
["workers"]
],
"columnPrefixes": [
"m_",
"w_"
]
},
"columns": {
"added": [],
"hidden": [
"m_museum_id",
"m_name",
"m_yearly_income",
"m_yearly_spendings",
"m_ratings",
"w_full_name",
"w_role",
"w_museum_id",
"w_titles",
"w_access_level"
]
},
"filterExpression": "m_museum_id = w_museum_id",
"type": "JoinLens"
}
]
}
# Mapping
Now that the lens file is created, we can construct our mapping. For this, we once again start from the mapping template file provided with the tutorial files.
We can add a mapping entry to it, referencing the newly created lens.
mappingId MAPID-museum-worker-address
target data:worker/{w_worker_id} :workAddress {m_address} .
source SELECT w_worker_id, m_address FROM lenses.museum_workers;
Now, we can test the lens and mapping, by copying the corresponding files to the Ontop endpoint directory and running the following query:
PREFIX : <http://example.org/museum_kg/>
SELECT ?name ?address WHERE {
?worker a :Worker .
?worker :name ?name .
?worker :workAddress ?address .
}
If everything was prepared correctly, this should result in a list of employee names, together with the address of the museum they work at.
← Basic Lens Union Lens →