Introducing One To Many Relationship in Angular & Akita
Tags: angular, akita, javascript
Published: 2019-06-17 10:43:39

---
title: Introducing One To Many Relationship in Angular & Akita
published: true
description: one to many relationship in Angular & Akita
tags: angular, akita, javascript
cover_image: https://thepracticaldev.s3.amazonaws.com/i/r88bhz121uezcgk9u2cv.png
---

In this article, I will show you how to create a blog with Angular and Akita. Along the way, we will learn about two strategies we can use to manage <a href="https://en.wikipedia.org/wiki/One-to-many_(data_model)"> One-to-many</a> relationships with Akita.

Our demo application will feature the main page where we show the list of articles and an article page where we show the complete article with its comments. We will add the functionality to add, edit and remove a comment. So our One-to-many relationship, in this case, is "an article has many comments" or "a comment belongs to an article".

Let's see how we tackle this, but first, let's see the response shape we get from the server:

```ts
[{
  id: string;
  title: string;
  content: string;
  comments: [{
    id: string;
    text: string;
  }]
}]
```

We get an array of articles, where each article holds its comments in a `comments` property.


## Strategy One - Unnormalized Data
We will start by looking at the unnormalized data version. This means that we will use the server response as is without modifying it. We will use one store, i.e., an `ArticleStore` that will store the article and its comments. Let's see it in action.

First, we need to add Akita to our project:

```
ng add @datorama/akita
```

The above command adds Akita, Akita's dev-tools, and Akita's schematics into our project. The next step is to create a store. We need to maintain a collection of articles, so we scaffold a new entity feature:

```
ng g af articles
```

This command generates a articles store, a articles query, a articles service, and an article model for us:

```ts
// article.model

import { ID } from '@datorama/akita';

export interface ArticleComment {
  id: ID;
  text: string;
}

export interface Article {
  id: ID;
  title: string;
  content: string;
  comments: ArticleComment[];
}

// articles.store
export interface ArticlesState extends EntityState<Article> {}

@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'articles' })
export class ArticlesStore extends EntityStore<ArticlesState, Article> {
  constructor() { super() }
}

// articles.query
@Injectable({ providedIn: 'root' })
export class ArticlesQuery extends QueryEntity<ArticlesState, Article> {

  constructor(protected store: ArticlesStore) {
    super(store);
  }
}
```

Now, let's define our routes:

```ts
const routes: Routes = [
  {
    component: HomePageComponent,
    path: '',
    pathMatch: 'full'
  },
  {
    component: ArticlePageComponent,
    path: ':id'
  }
];
```

Let's create the `HomePageComponent`:


```ts
@Component({
  templateUrl: './homepage.component.html',
  styleUrls: ['./homepage.component.css']
})
export class HomepageComponent implements OnInit {
  articles$ = this.articlesQuery.selectAll();
  loading$ = this.articlesQuery.selectLoading();

  constructor(private articlesService: ArticlesService, 
              private articlesQuery: ArticlesQuery) {
  }

  ngOnInit() {
    !this.articlesQuery.getHasCache() && this.articlesService.getAll();
  }
}
```
We use the built-in Akita query selectors. The `selectAll` selector which reactively get the articles from the store and the `selectLoading` selector as an indication of whether we need to show a spinner.

In the `ngOnInit` hook, we call the service's `getAll` method that fetches the articles from the server and adds them to the store.

```ts
@Injectable({ providedIn: 'root' })
export class ArticlesService {

  constructor(private store: ArticlesStore,
              private http: HttpClient) {
  }

  async getAll() {
    const response = await this.http.get('url').toPromise();
    this.store.set(response.data);
  }
}
```

In our case, we want to fetch them only once, so we use the built-in `getHasCache()` to check whether we have data in our store. The internal store's cache property value is automatically changed to `true` when we call the store's `set` method. Now, we can build the template:


```html
<section class="container">
  <h1>Blog</h1>

  <h3 *ngIf="loading$ | async; else content">Loading...</h3>

  <ng-template #content>
    <app-article-preview *ngFor="let article of articles$ | async;"
                         [article]="article"></app-article-preview>
  </ng-template>

</section>
```

Let's move on to the article page component.

```ts
@Component({
  templateUrl: './article-page.component.html',
  styleUrls: ['./article-page.component.css']
})
export class ArticlePageComponent implements OnInit {
  article$: Observable<Article>;
  articleId: string;
  selectedComment: ArticleComment = {} as ArticleComment;

  constructor(private route: ActivatedRoute,
              private articlesService: ArticlesService,
              private articlesQuery: ArticlesQuery) {
  }

  ngOnInit() {
    this.articleId = this.route.snapshot.params.id;
    this.article$ = this.articlesQuery.selectEntity(this.articleId);
  }

  async addComment(input: HTMLTextAreaElement) {
    await this.articlesService.addComment(this.articleId, input.value);
    input.value = '';
  }

  async editComment() {
    await this.articlesService.editComment(this.articleId, this.selectedComment);
    this.selectedComment = {} as ArticleComment;
  }

  deleteComment(id: string) {
    this.articlesService.deleteComment(this.articleId, id);
  }

  selectComment(comment: ArticleComment) {
    this.selectedComment = { ...comment };
  }

  trackByFn(index, comment) {
    return comment.id;
  }
}

```

First, we obtain the current article id from the `ActivatedRoute` provider snapshot property. Then, we use it to reactively select the article from the store by using the `selectEntity` selector. We create three methods for adding, updating and deleting a comment. Let's see the template:

```html
<div *ngIf="article$ | async as article">
  <h1>{{ article.title }}</h1>
  <p>{{ article.content }}</p>

  <h3>Comments</h3>
  <div *ngFor="let comment of article.comments; trackBy: trackByFn" 
       (click)="selectComment(comment)">
    {{ comment.text }} 
    <button (click)="deleteComment(comment.id)">Delete</button>
  </div>

  <h5>New Comment</h5>

  <div>
    <textarea #comment></textarea>
    <button type="submit" (click)="addComment(comment)">Add</button>
  </div>

  <h5>Edit Comment</h5>

  <div>
    <textarea [(ngModel)]="selectedComment.text"></textarea>
    <button type="submit" (click)="editComment()">Edit</button>
  </div>
</div>
```

And let's finish with the complete service implementation.

```ts
@Injectable({ providedIn: 'root' })
export class ArticlesService {

  constructor(private store: ArticlesStore,
              private http: HttpClient) {
  }

  async getAll() {
    const response = await this.http.get('url').toPromise();
    this.store.set(response.data);
  }

  async addComment(articleId: string, text: string) {
    const commentId = await this.http.post(...).toPromise();

    const comment: ArticleComment = {
      id: commentId,
      text
    };

    this.store.update(articleId, article => ({
      comments: arrayAdd(article.comments, comment)
    }));
  }

  async editComment(articleId: string, { id, text }: ArticleComment) {
    await this.http.put(...).toPromise();

    this.store.update(articleId, article => ({
      comments: arrayUpdate(article.comments, id, { text })
    }));
  }

  async deleteComment(articleId: string, commentId: string) {
    await this.http.delete(...).toPromise();

    this.store.update(articleId, article => ({
      comments: arrayRemove(article.comments, commentId)
    }));
  }
}
```

In each CRUD method, we first update the server, and only when the operation succeeded, we use Akita's built-in array <a href="https://netbasal.gitbook.io/akita/general/state-array-utils">utils</a> to update the relevant comment.

Now, let's examine the alternative strategy.

## Strategy Two - Data Normalization
This strategy requires to normalize the data we get from the server. The idea is to create two stores. `CommentsStore` which is responsible for storing the entire comments. `ArticlesStore` which is responsible for storing the articles where each article has a `comments` array property which contains the ids of the associated comments.


```
ng g af articles
ng g af comments
```

Let's see the models.


```ts
// article.model
export interface Article {
  id: ID;
  title: string;
  content: string;
  comments: (Comment | ID)[];
}

// commment.model
export interface Comment {
  id: ID;
  text: string;
}
```

Now, let's modify the `ArticleService` `getAll` method.

```ts
@Injectable({ providedIn: 'root' })
export class ArticlesService {

  constructor(private articlesStore: ArticlesStore,
              private commentsService: CommentsService,
              private commentsStore: CommentsStore,
              private http: HttpClient) {
  }

  async getAll() {
    const response = await this.http.get('url').toPromise();

    const allComments = [];

    const articles = response.data.map(currentArticle => {
      const { comments, ...article } = currentArticle;
      article.comments = [];

      for(const comment of comments) {
        allComments.push(comment);
        article.comments.push(comment.id);
      }
      return article;
    });

    this.commentsStore.set(allComments);
    this.articlesStore.set(articles);
  }
}
```

We create a new articles array where we replace the `comment` object from each article with the comment id. Next, we create the `allComments` array, which holds the entire comments. Finally, we add both of them to the corresponding store.

Now, lets' see what we need to change in the article page. As we need to show the article and its comments, we need to create a derived query which joins an article with its comments. Let's create it.

```ts
@Injectable({ providedIn: 'root' })
export class ArticlesQuery extends QueryEntity<ArticlesState, Article> {

  constructor(protected store: ArticlesStore, private commentsQuery: CommentsQuery) {
    super(store);
  }

  selectWithComments(articleId: string) {
    return combineLatest(
      this.selectEntity(articleId),
      this.commentsQuery.selectAll({ asObject: true })
    ).pipe(map(([article, allComments]) => ({
      ...article,
      comments: article.comments.map(id => allComments[id])
    })));
  }
}
```

We create the `selectWithComments` selector which takes the `articleId`, and creates a join between the article and the comments, and returns a mapped version with the comments based on the `comments`ids. Now, we can use it in the component:

```ts
export class ArticlePageComponent implements OnInit {
  article$: Observable<Article>;

  constructor(private route: ActivatedRoute,
              private articlesService: ArticlesService,
              private articlesQuery: ArticlesQuery) {
  }

  ngOnInit() {
    this.articleId = this.route.snapshot.params.id;
    this.article$ = this.articlesQuery.selectWithComments(this.articleId);
  }
}
```

Let's finish seeing the changes in the `ArticlesService`:
```ts
@Injectable({ providedIn: 'root' })
export class ArticlesService {

  constructor(private articlesStore: ArticlesStore,
              private commentsService: CommentsService,
              private commentsStore: CommentsStore,
              private http: HttpClient) {
  }

  async getAll() {}

  async addComment(articleId: string, text: string) {
    const commentId = await this.commentsService.add(articleId, text);

    this.articlesStore.update(articleId, article => ({
      comments: arrayAdd(article.comments, commentId)
    }));
  }

  async editComment(comment: Comment) {
    this.commentsService.edit(comment);
  }

  async deleteComment(articleId: string, commentId: string) {
    await this.commentsService.delete(commentId);

    this.articlesStore.update(articleId, article => ({
      comments: arrayRemove(article.comments, commentId)
    }));
  }

}
```

In this case, when we perform add or remove operations, we need to update both the `CommentsStore` and the `ArticlesStore`. In the case of an edit, we need to update only the `CommentsStore`. Here is the `CommentsService`.

```ts
@Injectable({ providedIn: 'root' })
export class CommentsService {

  constructor(private commentsStore: CommentsStore) {
  }

  async add(articleId: string, text: string) {
    const id = await this.http.post().toPromise();
    this.commentsStore.add({
      id,
      text
    });

    return id;
  }

  async delete(id: string) {
    await await this.http.delete(...).toPromise();
    this.commentsStore.remove(id);
  }

  async edit(comment: Comment) {
    await this.http.put(...).toPromise();
    return this.commentsStore.update(comment.id, comment);
  }
}
```

## Summary
We learn about two strategies of how we can manage One-to-many relationships with Akita. In most cases, I will go with the first strategy as it is cleaner, shorter, and more straightforward. The second strategy might be useful when you have massive edit operations in your application, and you care about performance. 

But remember, premature optimizations are the root of all evil.