Search code examples
javascriptember.jsmany-to-manyember-data

Ember - Many to many relationship data not being updated


I have a model for a speaker as follows:

import attr from 'ember-data/attr';
import ModelBase from 'open-event-frontend/models/base';
import { belongsTo, hasMany } from 'ember-data/relationships';

export default ModelBase.extend({

  /**
   * Attributes
   */

  name               : attr('string'),
  email              : attr('string'),
  photoUrl           : attr('string'),
  thumbnailImageUrl  : attr('string'),
  shortBiography     : attr('string'),
  longBiography      : attr('string'),
  speakingExperience : attr('string'),
  mobile             : attr('string'),
  location           : attr('string'),
  website            : attr('string'),
  twitter            : attr('string'),
  facebook           : attr('string'),
  github             : attr('string'),
  linkedin           : attr('string'),
  organisation       : attr('string'),
  isFeatured         : attr('boolean', { default: false }),
  position           : attr('string'),
  country            : attr('string'),
  city               : attr('string'),
  gender             : attr('string'),
  heardFrom          : attr('string'),

  /**
   * Relationships
   */

  user     : belongsTo('user'),
  event    : belongsTo('event'),
  sessions : hasMany('session')

});

And a model for a session as follows:

import attr from 'ember-data/attr';
import moment from 'moment';
import ModelBase from 'open-event-frontend/models/base';
import { belongsTo, hasMany } from 'ember-data/relationships';
import { computedDateTimeSplit } from 'open-event-frontend/utils/computed-helpers';

const detectedTimezone = moment.tz.guess();

export default ModelBase.extend({
  title         : attr('string'),
  subtitle      : attr('string'),
  startsAt      : attr('moment', { defaultValue: () => moment.tz(detectedTimezone).add(1, 'months').startOf('day') }),
  endsAt        : attr('moment', { defaultValue: () => moment.tz(detectedTimezone).add(1, 'months').hour(17).minute(0) }),
  shortAbstract : attr('string'),
  longAbstract  : attr('string'),
  language      : attr('string'),
  level         : attr('string'),
  comments      : attr('string'),
  state         : attr('string'),
  slidesUrl     : attr('string'),
  videoUrl      : attr('string'),
  audioUrl      : attr('string'),
  signupUrl     : attr('string'),
  sendEmail     : attr('boolean'),

  isMailSent: attr('boolean', { defaultValue: false }),

  createdAt      : attr('string'),
  deletedAt      : attr('string'),
  submittedAt    : attr('string', { defaultValue: () => moment() }),
  lastModifiedAt : attr('string'),
  sessionType    : belongsTo('session-type'),
  microlocation  : belongsTo('microlocation'),
  track          : belongsTo('track'),
  speakers       : hasMany('speaker'),
  event          : belongsTo('event'), // temporary
  creator        : belongsTo('user'),

  startAtDate : computedDateTimeSplit.bind(this)('startsAt', 'date'),
  startAtTime : computedDateTimeSplit.bind(this)('startsAt', 'time'),
  endsAtDate  : computedDateTimeSplit.bind(this)('endsAt', 'date'),
  endsAtTime  : computedDateTimeSplit.bind(this)('endsAt', 'time')
});

As is clear, session and speaker share a many-to-many relationship. So I am adding the session to the speaker and then saving them. Both the records are successfully created on the server but the link is not established. I have tested the server endpoint with postman and it works fine. So, I guess I am missing something here.

This is the controller code:

import Controller from '@ember/controller';
export default Controller.extend({
  actions: {
    save() {
      this.set('isLoading', true);
      this.get('model.speaker.sessions').pushObject(this.get('model.session'));

      this.get('model.session').save()
        .then(() => {
          this.get('model.speaker').save()
            .then(() => {
              this.get('notify').success(this.get('l10n').t('Your session has been saved'));
              this.transitionToRoute('events.view.sessions', this.get('model.event.id'));
            })
            .catch(() => {
              this.get('notify').error(this.get('l10n').t('Oops something went wrong. Please try again'));
            });
        })
        .catch(() => {
          this.get('notify').error(this.get('l10n').t('Oops something went wrong. Please try again'));
        })
        .finally(() => {
          this.set('isLoading', false);
        });
    }
  }
});

Solution

  • As @Lux has mentioned in comment, ember-data does not serialize has-many relationships in all cases by default. This is true for all Serializers that extend DS.JSONSerializer and not overriding shouldSerializeHasMany() method. This is the case for DS.JSONAPIAdapter and DS.RESTSerializer.

    The logic used to determine if a many-relationship should be serialized is quite complex and not well documented. So looking in source code is needed for details.

    In general a has-many relationship is serialized if and only if:

    1. If enforced by Serializer configuration. To configure Serializer to enforce serializing a specific relationship, the attrs option must contain a key with relationship name holding an object { serialize: true }.

    2. If not forbidden by attrs configuration of Serializer. It's the opposite of 1. The attrs option contains a key with relationship name holding an object { serialize: false }.

    3. If it's a many-to-none relationship meaning there is no inverse relationship.

    4. If it's a many-to-many relationship.

    Accordingly to your question, a many-to-many relationship is not serialized. There could be many things causing your issue but I bet it's this one:

    If I got you right, both records of the many-to-many relationship are not yet persisted on server. I assume you don't generate IDs client-side. So both records won't have an ID at beginning. Therefore the relationship can't be serialized on first create request (this.get('model.session').save()). Your API response well to this request. The response includes the information that this record does not have any related speaker. ember-data updates it's store with the information returned by your API. This update includes removing the relationship created before. The second create request (this.get('model.speaker').save()) serializes the relationship but as not existing since that's the current value in store.

    If that's the case, you could simply create one of the records before assigning the relationship and afterwards save the other one, which will persist the relationship on server.