Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Many-to-Many relational support in JSData

Tags:

jsdata

Is there any way to define a many-to-many relationship in JSData?

For example, I have these 3 tables:

entity entityFile file

On the "entity", i want to have a relationship called "files" which does a join THROUGH entityFile.

like image 224
Sean Lynch Avatar asked Oct 28 '15 17:10

Sean Lynch


1 Answers

Good question. A typical many-to-many relationship is just two one-to-many relationships:

One of the more important details in any implementation is: Where is the relationship information stored? The answer to this question determines how an entity's relations can be accessed. Let's explore a few options.

Premise:

A hasMany B

B hasMany A

Option 1

Relationship information is stored on instances of A.

In this scenario, once you have an instance of A you can find its associated instances of B, because the IDs of the associated B instances are stored on A. This also means that if you only have an instance of B, the only way to find all of the instances of A that the B instance relates to would be to search all of the instances of A for those whose b_ids field contains the id of the B instance.

An example

var Player = store.defineResource({
  name: 'player',
  relations: {
    hasMany: {
      team: {
        // JSData will setup a "teams" property accessor on
        // instances of player which searches the store for
        // that player's teams
        localField: 'teams',
        localKeys: 'team_ids'
      }
    }
  }
})

var Team = store.defineResource({
  name: 'team',
  relations: {
    hasMany: {
      player: {
        localField: 'players',
        // Since relationship information is stored
        // on the player, in order to retrieve a
        // team's players we have to do a O(n^2)
        // search through all the player instances
        foreignKeys: 'team_ids'
      }
    }
  }
})

Now let's see it in action:

var player = Player.inject({
  id: 1,
  team_ids: [3, 4]
})

// The player's teams aren't in the store yet
player.teams // [ ]

var player2 = Player.inject({
  id: 2,
  team_ids: [4, 5],
  teams: [
    {
      id: 4
    },
    {
      id: 5
    }
  ]
})

// See the property accessor in action
player2.teams // [{ id: 4 }, { id: 5 }]

// One of player one's teams is in the store now
player.teams // [{ id: 4 }]

// Access the relation from the reverse direction
var team4 = Team.get(4) // { id: 4 }

// The property accessor makes a O(n^2) search of the store because
// the relationship information isn't stored on the team
team4.players // [{ id: 1, team_ids: [3, 4] }, { id: 2, team_ids: [4, 5] }]

Let's load a relation from a persistence layer:

// To get an authoritative list of player one's 
// teams we ask our persistence layer.
// Using the HTTP adapter, this might make a request like this:
// GET /team?where={"id":{"in":[3,4]}} (this would be url encoded)
//
// This method call makes this call internally:
// Team.findAll({ where: { id: { 'in': player.team_ids } } })
player.DSLoadRelations(['team']).then(function (player) {

  // The adapter responded with an array of teams, which
  // got injected into the datastore.

  // The property accessor picks up the newly injected team3
  player.teams // [{ id: 3 }, { id: 4 }]

  var team3 = Team.get(3)

  // Retrieve all of team3's players.
  // Using the HTTP adapter, this might make a request like this:
  // // GET /player?where={"team_ids":{"contains":3}} (this would be url encoded)
  //
  // This method call makes this call internally:
  // Player.findAll({ where: { team_ids: { 'contains': team3.id } } })
  return team3.DSLoadRelations(['player'])
})

If you're using the HTTP adapter, then it's up to your server to parse the querystring and respond with the right data. If you're using any one of the other adapters then the adapter already knows how to return the right data. Using JSData on the frontend and backend just makes this too easy.

Option 2

Relationship information is stored on instances of B.

This is just the inverse of option 1.

Option 3

"A hasMany B" relationship information is stored on instances A, and "B hasMany A" relationship information is stored on instances of B.

This is just option 1 except that it now works in both directions.

An advantage of this approach is that you can access relation from both directions without the need to use the foreignKeys option. A disadvantage of this approach is that when relationships change to have to modify data in multiple places.

Option 4

Relationship information is stored in a pivot (junction) table.

A hasMany C and C belongsTo A, where the actual relationship information is stored in C.

B hasMany C and C belongsTo B, where the actual relationship information is stored in C.

An example:

var Player = store.defineResource({
  name: 'player',
  relations: {
    hasMany: {
      membership: {
        localField: 'memberships',
        // relationship information is stored on the membership
        foreignKey: 'player_id'
      }
    }
  }
})

var Team = store.defineResource({
  name: 'team',
  relations: {
    hasMany: {
      membership: {
        localField: 'memberships',
        // relationship information is stored on the membership
        foreignKey: 'team_id'
      }
    }
  }
})

And the pivot Resource:

var Membership = store.defineResource({
  name: 'membership',
  relations: {
    belongsTo: {
      player: {
        localField: 'player',
        // relationship information is stored on the membership
        localKey: 'player_id'
      },
      team: {
        localField: 'team',
        // relationship information is stored on the membership
        localKey: 'team_id'
      }
    }
  }
})

Now let's see it in action:

var player = Player.inject({ id: 1 })
var player2 = Player.inject({ id: 2 })
var team3 = Team.inject({ id: 3 })
var team4 = Team.inject({ id: 4 })
var team4 = Team.inject({ id: 5 })

player.memberships // [ ]
player2.memberships // [ ]
team3.memberships // [ ]
team4.memberships // [ ]
team5.memberships // [ ]

Notice at this point we can't yet access any relations

// The relationships stored in our pivot table
var memberships = Membership.inject([
  {
    id: 997,
    player_id: 1,
    // player one is on team three
    team_id: 3
  },
  {
    id: 998,
    player_id: 1,
    // player one is also on team four
    team_id: 4
  },
  {
    id: 999,
    player_id: 2,
    // team four also has player 2
    team_id: 4
  },
  {
    id: 1000,
    player_id: 2,
    // player 2 is also on team 5
    team_id: 5
  }
])

Now we have membership information

player.memberships // [{ id: 997, ... }, { id: 998, ... }]
player2.memberships // [{ id: 998, ... }, { id: 999, ... }]
team3.memberships // [{ id: 997, ... }]
team4.memberships // [{ id: 998, ... }, { id: 999, ... }]
team5.memberships // [{ id: 1000, ... }]

Now, it's a bit clunky to send your pivot table data to your frontend and require your JavaScript to sort through the relations. For that you would want some helper methods:

var Player = store.defineResource({
  name: 'player',
  relations: {...},
  computed: {
    teams: {
      get: function () {
        return store.filter('membership', {
          player_id: this.id
        }).map(function (membership) {
          return store.get('team', membership.team_id)
        })
      }
    }
  },
  // Instance methods
  methods: {
    getTeams: function () {
      return Player.getTeams(this.id)
    }
  }
  // Static Class Methods
  getTeams: function (id) {
    return this.loadRelations(id, ['membership']).then(function (memberships) {
      return store.findAll('team', {
        where: {
          id: {
            'in': memberships.map(function (membership) {
              return membership.team_id
            })
          }
        }
      })
    })
  }
})

I'll let you figure out the analogous methods for the Team resource.

If you don't want to go to the trouble of the helper methods, then you might just implement them on the backend to make your pivot table invisible to the frontend and make your many-to-many relationship look more like option 1, 2 or 3.

Useful links

  • JSData Documentation
  • Article about many-to-many
  • Simple plunker demonstrating option 4
like image 70
jdobry Avatar answered Nov 03 '22 01:11

jdobry