Image for post
Image for post

Switching from Ember-Data to Ember-Orbit

Derek Gray
Sep 25 · 7 min read

Here’s a collection of tips and changes I had to consider while converting a large ember-data (“ED”) project over to ember-orbit (“EO”) and Orbit.js. I’m hoping this helps you to at least estimate the size of effort it might be to do the same on your own project. Our motivation was to leverage almost all of the abilities that Orbit enables (offline cache, transaction logs, background sync strategies) but the first priority was for providing an Undo/Redo feature in the application with otherwise existing behaviour. At this point, our conversion is still in progress and I’m still learning new things.

I’m not going to introduce Orbit, as others have already done a decent job of that. If you aren’t familiar with Orbit you could start with:

Note, I’m writing this at Ember-Orbit v0.16.8, while 0.17 beta is under development— be sure to check release notes (CHANGELOG.md) on GitHub for breaking changes or deprecations.

Getting Started

My strategy aimed at first keeping the existing ember-data behaviours we started with, before trying to integrate extra Orbit capabilities. The approach I took (we have many models) was to convert one or two models as a proof of concept end-to-end — I had both ember-data and ember-orbit models and stores co-existing until it I reached the point where the relationships between models were too great to manage across both.

  • Sync Strategies: You‘re likely going to want to start with pessimistic sync strategies to get as close to your existing ember-data logic as possible (e.g. wherever you were previously waiting for the promise of a .save() on a record.) (Actually tchak has been working on some pre-made strategies, one of which I understand should be pretty close to default ember-data pessimistic behaviour.) You could start with adding your remote source and the three pessimistic strategies suggested in todomvc-ember-orbit:
ember g data-source remote --from=@orbit/jsonapi
ember g data-strategy remote-store-sync
ember g data-strategy store-beforequery-remote-query
ember g data-strategy store-beforeupdate-remote-update
  • Buckets: I’d strongly suggest you start with no persisted buckets at all — this will save you some grief as you start debugging. When you’re rapidly migrating code and data-models, and refreshing constantly with ember serve, the last thing you want is for some cached requests to re-try with old models. That’s too big a feature leap for the start of your refactoring.
  • The Store: Thankfully ember-orbit has it’s own Store implementation that adds some more familiar forms of manipulation so that, for many simple calls, you need only tweak slightly; they’ll look more like ember-data and a little less like plain Orbit. If you’re going to use both ED and EO stores at the same time, take advantage of the environment configuration to set the injected EO store key to something other than ‘store’ (see the EO README.md.)

JSONAPI Adapters and Serializers

  • Customizing: If you need to tweak your model or attribute names as they become part of the JSONAPI ajax calls, then rather than adding /adapters/application.js , you can extend JSONAPISerializer and assign it to the SerializerClass option when creating the JSONAPISource instance in data-sources/remote.js. (May differ in 0.17)
  • The default inflector for ember-orbit does not pluralize that well (at 0.16)— we had a resource on the server called “approaches”, so fetching “approachs” failed. You can ember install ember-inflector and use its pluralize() and singularize() in your custom serializer instead of the default this.schema.pluralize(). Note, inflector customization has been improved in v0.17.
  • You can also set your URL namespace here (as you would have done in your application adapter previously.)

Models

  • Converting ED definitions to EO is pretty easy— sometimes just a change to your import statements.
  • Supported attributes types: Only primitive types are supported directly. You can use arrays or objects as values, but you have to manually ensure you transform the values by passing in complete clones. To specify this, simply omit the type property in your attribute definition.
  • Default attribute values: If you were accustomed to defining your model attributes with the { defaultValue } option, you’ll need another solution. I sort-of supported this by overriding the static attributes getter on Model to include all the meta (and thus options.defaultValue) on the attr computed property. I then extended Store to initialize a registry of default attribute values to consult during addRecord. The obvious downfall here is it only works with store.addRecord — if you go through pure Orbit via store.update(t => t... you’re out of luck. You could probably consult the registry in an adapter closer to the metal though.
  • Ember-Data uses dasherized model name references, while ember-orbit uses camel-case.
  • Polymorphic relationships in ED are defined with a parent class and the { polymorphic: true } flag. In plain Orbit, the relationship type is specified as an array of types instead (e.g.@hasMany(['cat', ‘dog']) pets;). (You must have ember-orbit 0.16.7 or greater for this support.)
  • Currently it naively traverses the data-models directory to load model schema. If you have non-Orbit models in there, you’ll have to move them to a sibling directory. The default, but configurable, model directory name is /data-models, which could help you transition gradually while maintaining /models for ED.
  • The most significant thing to remember now is that relationships retrieved via getter no longer return a Promise — it only returns what’s in the cache, synchronously. More about that later…
  • Octane get/set: You can use dot-notation getters, and, if you use 0.17, setters. But I find the explicit calls (replaceAttribute, replaceRelatedRecord, etc.) to be more clear and useful, especially with pessimistic data strategies since those versions return a promise, while set does not.
  • Saving: Remove all your .save()s. This may take a while, especially if you have a lot of what we had: Promise.all(anArrayOfAWholeBunchOfSaves) and save().then(). One technique for groups of changes would be to use store.fork(), make the large group of changes, then store.merge() the fork. It was pointed out to me, to prevent leaking memory, to remember to .destroy() the fork when you’re through with it.

Creating, Updating, Deleting and Querying

  • Creating records : addRecord({ type: 'planet', …other attributes }) rather than ED’s createRecord(‘planet’, { attributes }).save() . You’ll have to now accept a Promise, rather than an unsaved (synchronous) record, in return.
  • Peeking: store.peekRecord(s) can stay the same or be changed to store.cache.findRecords(s).
  • Deleting:model.destroyRecord() becomes either model.remove() or store.removeRecord(record) with both returning a promise.
  • Update attributes: In ED: record.set('attribute', value) (or in Octane, record.attribute = value,) plus a record.save() to get a Promise. In EO, if you need to block on it, then use record.replaceAttribute('attribute', value) as it returns the Promise. record.set('attribute', value) can be used, and calls replaceAttribute under the hood, but doesn’t return the Promise. If you want to group a few together in a transaction, look uprecord.replaceAttributes(...). Recall also, if you are using attributes that have values of Array or Object you must pass in new copies.
  • Fetching related: solarSystem.get(planets) (or solarSystem.planets), and solarSystem.getRelatedRecord/getRelatedRecords are synchronous in ember-orbit. They only check the store’s cache for the related record(s), which might mean you only get the identity back ({ type, id }) right away, though it will be a live query that loads them in the background. If you need it to be real then you want to wait for a promise. So, await store.liveQuery(q => q.findRelatedRecords(planet, 'moons') gives you back a promise and a live record array (or store.query too.) It was recommended to me to guard against memory leaks by using liveQuery.unsubscribe(). The 0.17 documentation should mention more about this.
  • Updating related: planetarySystem.set('star', sunRecord) becomes planetarySystem.replaceRelatedRecord('star', sunRecord) or even store.update(t => t.replaceRelatedRecord(planetarySystem.identity, 'star', sunRecord.identity)).
  • Pushing Payloads: If you are also getting data outside of the regular ED pipeline, for example via websockets, you may have used store.pushPayload(). Tchak toyed with an example here: https://github.com/orbitjs/orbit/pull/640. So far, I’ve done this by (1) deserializing the resource, (2) building up a transforms array using the builder (this.source.transformBuilder ) then (3) patching the cache: this.source.cache.patch(transforms).
  • Two APIs: Something to keep in mind — when you are using a transform builder (e.g. store.update(t => t.replaceRelatedRecord(... , rather than the Model interfaces, the builder is in Orbit.js land in terms of API parameters. You must be careful not to pass Ember-Orbit models into those functions that expect identities ({ type, id }). I found it easy to make this mistake several times. If you find yourself with a “Maximum call stack size exceeded” exception, that could be the reason.

Tests

  • If your acceptance or integration tests have been relying on the ember-data store, then there is going to be some work involved here of course. I’ll point out that there is a test helper called waitForSource(store) that you can await on.

Thanks to the Orbit.js team for help answering some of the questions that led to this set of tips!

[Updated: Sept/27/2020 with feedback from tchak and dgeb]

The Startup

Medium's largest active publication, followed by +730K people. Follow to join our community.

Derek Gray

Written by

Director, Research Engineering at https://uncharted.software in Toronto

The Startup

Medium's largest active publication, followed by +730K people. Follow to join our community.

Derek Gray

Written by

Director, Research Engineering at https://uncharted.software in Toronto

The Startup

Medium's largest active publication, followed by +730K people. Follow to join our community.

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store