question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

3.0 proposal: remove the need for developers to think about idsField and relationshipsField when working with joins

See original GitHub issue

I’m working on the technical design for editing relationships in Apostrophe 3.x.

The hardest part of this is the way joins are represented in the database, and how we make developers think about it.

idsField: 'productsIds' , relationshipsField: 'productsRelationships' and all that stuff. These things have magic defaults but under the hood it’s complicated.

All other schema field types in apostrophe stay in their lane. The data is stored in the property of the same name in the database. But joins scatter their ids to one field, their relationships to another, and the actual join gets loaded dynamically under the field’s name.

This is pretty good if you’re not working on the guts, because you only care about the actual join anyway. But if you’re working on the guts it’s pretty crazymaking.

To make matters worse, our REST interface doesn’t really understand joins when writing, so you have to patch the idsField and relationshipsField directly.

I think we should definitely fix our REST interface so that if you PUT, POST or PATCH a join field by its name, like _products , and pass in an actual array of existing product pieces, it’s smart enough to say “oh they are updating the join, let’s just grab the _id and any _relationship property if present from each one. Cool.” It’s inefficient if they send the entire array fully populated, but it should work.

But beyond that, there’s a tougher call to make. Do we want to make the data model easy to understand internally, and make it easy to implement the backend UI? Or do we want to continue to make sacrifices there for slightly less frontend code?

I see two ways forward:

1. We continue to sweat idsField and relationshipsField, but we do all of that in the AposSchema Vue component, where it is a special case in order to allow the interface to the individual field components, including AposInputJoin, to stay simple and v-model based.

2. We reboot the whole approach to store joins under their own name. Want to join your salesperson with some products? Write this in your schema:

// Note no _ anymore
products: {
  type: 'join',
  withType: 'product,
  // use the optional relationship feature. Not always needed
  relationship: {
    // units of this product sold by this particular salesperson
    sold: {
      type: 'integer'
    }
  }
}

Apostrophe stores this in your salesperson doc:

{
  title: 'Willy Loman',
  type: 'salesperson',
  products: {
    ids: [ 1, 2, 7 ],
    relationships: {
      '2': {
        sold: 5
      }
    }
  }
}

And when you find salespeople, or use our GET REST API, you get back this:

{
  title: 'Willy Loman',
  type: 'salesperson',
  products: {
    // _docs subproperty is populated on the fly at load time
    _docs: [
      {
        title: 'Hair Tonic',
        type: 'product',
        _id: 2,
        _relationship: {
          sold: 5
        }
      },
      ...
    ],
    // the data that powers the join is here too
    ids: [ 1, 2, 7 ],
    relationships: {
      '2': {
        sold: 5
      }
    }
  }
}

What this does is give us a super clear model for how joins are stored. They are concretely stored under a property of the same name, just like other fields.

However, it makes frontend developers do something new. In a template, you would now write:

<h3>{{ data.salesperson.title }}: Assigned Products</h3>
{% for product of data.salesperson.products._docs %}
  <h4><a href="{{ product._url }}">{{ product.title }} (sold: {{ product._relationship.sold }})</a></h4>
{% endfor %}

You see what’s different here: we have to go the extra step to ._docs when we work with these joins in our frontend code.

This is this price we would pay for everything being stored in a sensible and intuitive way.

But, it’s a price. And, it’s a change for people to learn about.

What do you think? Input welcome!

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:28 (24 by maintainers)

github_iconTop GitHub Comments

2reactions
boutellcommented, Aug 25, 2020

Cool. I think Bea and I agreed with you already in our hearts but needed a little push 😄

I move we put this puppy to bed.

The final proposal would be:

  • The field type is relationship, not join.

  • Relationship fields, if any, are named and configured like this:

_products: {
  type: 'relationship',
  label: 'Products',
  withType: 'product',
  fields: {
    add: {
      sold: {
        type: 'integer',
        label: 'Units Sold'
      }
    }
  }
}
  • In a template, working with one would look like this (NO CHANGE from 2.x):
{% for product of data.salesperson._products %}
  <h4><a href="{{ product._url }}">{{ product.title }}</a></h4>
{% endfor %}
  • If there is relationship data we access it like this (previous decision for 3.x, it was more awkward in 2.x):
{% for product of data.salesperson._products %}
  <h4><a href="{{ product._url }}">{{ product.title }} ({{ product._relationship.sold }} units sold)</a></h4>
{% endfor %}
  • “What you get is what you set.” Updating a join, server side, looks like this (NEW - formerly this would have required fussing with the idsField):
// in modules/salesperson/index.js

const salesperson = await self.find(req, { _id: someIdOrOther }).toObject();
const products = await self.apos.product.find(req, { color: 'blue' }).toArray();
salesperson._products = products;
await self.update(req, salesperson);

You can also set the relationship fields by setting ._relationship on one or more of the products in that array.

  • Updating a join via REST works the same way — you can PUT a doc with a join array as one of its properties and it will Just Work. or PATCH it.

Can I get a hell yeah?

2reactions
localghost8000commented, Aug 25, 2020

+1 For renaming Join to Relationship.

From a technical marketing perspective, Relationship is standard vernacular. In addition, Relationship is interoperable across a wide array of stakeholders (Designers, PMs, Developers, Executives, etc) in its simplicity and basic recognition in English. As shown in our documentation, it’s difficult to describe Join without using the word relationship. As a rule of thumb, if we can simplify technical concepts without deeply violating their meaning, we should.

Read more comments on GitHub >

github_iconTop Results From Across the Web

punkave - Bountysource
3.0 proposal : remove the need for developers to think about idsField and ... the way joins are represented in the database, and...
Read more >
24 Optimization of Joins - Oracle Help Center
Optimizing Join Statements. To choose an execution plan for a join statement, the optimizer must make these interrelated decisions: access paths.
Read more >
How to Master Anti Joins and Apply Them to Business Problems
Read 3 examples of using anti joins in business situations. Learn how to perform an anti join using LEFT JOIN & WHERE in...
Read more >
How Relationships Differ from Joins - Tableau Help
Tableau first attempts to create the relationship based on existing key constraints and matching field names.
Read more >
Working with joins in LookML | Looker - Google Cloud
Joins connect one or more views to a single Explore, either directly, or through another joined view. Let's consider two database tables: ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found