Ember Models — Attributes, Transforms, and Relationships
In this tutorial, you will learn about Ember Models. We cover key concepts, practical examples, and best practices to help you master this topic.
Ember models define the structure and behavior of your application data. They specify attribute types, transform values to and from the server format, define relationships between models, and compute derived values through getters and methods.
What You'll Learn
You will learn how to define model attributes with type transforms, establish relationships, create computed properties, and use model lifecycle hooks.
Why It Matters
Well-defined models ensure data consistency across the application. Attribute types enforce data shape. Relationships model real-world connections. Transforms handle Serialization so templates always receive the right format.
Real-World Use
A Compliance tracking system models Audit, Finding, Remediation, and Auditor. A finding belongs to an audit and has many remediations. Each auditor has many audits. This relationship graph powers dashboards, reports, and compliance scores.
flowchart TD
A[Auditor] -->|hasMany| B[Audit]
B -->|hasMany| C[Finding]
C -->|hasMany| D[Remediation]
D -->|belongsTo| C
D -->|belongsTo| E[Assignee]
Defining Attributes
Use @attr with a type transform.
// app/models/product.js
import Model, { attr } from '@ember-data/model';
export default class ProductModel extends Model {
// Basic types
@attr('string') name;
@attr('number') price;
@attr('boolean') inStock;
@attr('date') releasedAt;
// No type — stored as-is
@attr() metadata;
// Custom transform
@attr('currency') cost;
// Computed from attributes
get formattedPrice() {
return `$${this.price.toFixed(2)}`;
}
get isNew() {
let daysOld = (Date.now() - this.releasedAt) / (1000 * 60 * 60 * 24);
return daysOld < 30;
}
}
Built-in Transforms
Ember Data includes standard transforms: string, number, boolean, date.
// String transform always returns a string
@attr('string') title; // null becomes ''
// Number transform converts to number
@attr('number') count; // '42' becomes 42
// Boolean transform converts to true/false
@attr('boolean') active; // 'true' becomes true
// Date transform converts to Date object
@attr('date') createdAt; // ISO string becomes Date
// Without type — raw value
@attr() tags; // Stored as-is
Custom Transforms
Create transforms for domain-specific types.
ember generate transform currency
// app/transforms/currency.js
import Transform from '@ember-data/serializer/transform';
export default class CurrencyTransform extends Transform {
deserialize(serialized) {
// Server sends cents (12345) → display as dollars ($123.45)
if (serialized == null) return 0;
return serialized / 100;
}
serialize(deserialized) {
// Display dollars ($123.45) → server expects cents (12345)
if (deserialized == null) return 0;
return Math.round(deserialized * 100);
}
}
// Usage in model
@attr('currency') price; // Server: 1999 → Client: 19.99
One-to-Many: hasMany
// app/models/category.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class CategoryModel extends Model {
@attr('string') name;
@attr('string') slug;
@attr('text') description;
@hasMany('product') products;
get productCount() {
return this.products.length;
}
}
Many-to-One: belongsTo
// app/models/product.js
import Model, { attr, belongsTo, hasMany } from '@ember-data/model';
export default class ProductModel extends Model {
@attr('string') name;
@attr('number') price;
@attr('number') stock;
@belongsTo('category') category;
@hasMany('review') reviews;
}
Many-to-Many
Use hasMany on both sides:
// app/models/post.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class PostModel extends Model {
@attr('string') title;
@hasMany('tag') tags;
}
// app/models/tag.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class TagModel extends Model {
@attr('string') name;
@hasMany('post') posts;
}
Polymorphic Relationships
For models that can belong to different types:
// app/models/comment.js
import Model, { attr, belongsTo } from '@ember-data/model';
export default class CommentModel extends Model {
@attr('string') text;
@attr('date') createdAt;
// Polymorphic — can belong to a Post or a Video
@belongsTo('commentable', { polymorphic: true }) parent;
}
// app/models/post.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class PostModel extends Model {
@attr('string') title;
@hasMany('comment') comments;
}
// app/models/video.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class VideoModel extends Model {
@attr('string') title;
@attr('number') duration;
@hasMany('comment') comments;
}
Model Lifecycle Hooks
// app/models/invoice.js
import Model, { attr, hasMany } from '@ember-data/model';
export default class InvoiceModel extends Model {
@attr('string') number;
@attr('date') issuedAt;
@attr('string') status;
@hasMany('line-item') lineItems;
// Called when record is created
init() {
super.init(...arguments);
if (!this.issuedAt) {
this.issuedAt = new Date();
}
}
// Called after record is loaded from server
ready() {
super.ready();
console.log(`Invoice #${this.number} loaded`);
}
// Called when attributes become dirty
becameDirty() {
console.log('Invoice has unsaved changes');
}
get total() {
return this.lineItems.reduce((sum, item) => sum + item.total, 0);
}
get isOverdue() {
return this.status === 'issued' &&
this.issuedAt < new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
}
}
Saving Related Records
// Creating a product with category
let category = this.store.peekRecord('category', 1);
let product = this.store.createRecord('product', {
name: 'New Product',
price: 29.99,
category: category // Set relationship
});
await product.save();
// Add to existing relationship
let post = await this.store.findRecord('post', 1);
let comment = this.store.createRecord('comment', {
text: 'Great post!',
post: post
});
await comment.save();
// Or await post.comments.pushObject(comment) then post.save()
Common Mistakes
- Using
attr()without a type parameter. Without a type, no transformation happens. Always specifystring,number,boolean, ordate. - Mutating hasMany arrays directly. Use
pushObject,removeObject, or set the IDs viastore.createRecord. Direct mutation breaks tracking. - Forgetting
async: falseon relationships. By default, relationships are async. Access returns a promise. Addasync: falsefor sync loading. - Over-saving with circular relationships. Saving a model that belongs to another can trigger cascading saves. Control this with
{ save: false }on the relationship. - Not using
init()for defaults. Default attribute values go ininit()not in the class body. Class body defaults are not reactive.
Practice Questions
- What types does Ember Data provide for attributes?
- How do you create a custom transform?
- What is the difference between
belongsToandhasMany? - How do you handle polymorphic relationships?
- Challenge: Design models for a project management app:
Project(hasMany tasks, belongsTo manager),Task(belongsTo project, hasMany comments, belongsTo assignee),User(hasMany tasks, hasMany managedProjects),Comment(belongsTo task, belongsTo author). Include custom transforms for priority (enum) and status (enum). Add computed properties for task completion percentage per project.
FAQ
Mini Project
Create a complete model layer for a library management system: Book (title, author, ISBN, publishedYear, pages, genre), Member (name, email, joinDate, membershipType), Loan (book, member, borrowedAt, dueAt, returnedAt), Genre (name, description, hasMany books). Include custom transforms for ISBN validation and membership type enum. Add computed: isOverdue, daysRemaining, activeLoans.
What's Next
Now that you understand models, learn Ember Adapters for API configuration. Then explore Ember Serializers for data format handling.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro