简体   繁体   English

Redux中如何处理关系数据?

[英]How to deal with relational data in Redux?

The app I'm creating has a lot of entities and relationships (database is relational).我正在创建的应用程序有很多实体和关系(数据库是关系型的)。 To get an idea, there're 25+ entities, with any type of relations between them (one-to-many, many-to-many).为了得到一个想法,有 25 个以上的实体,它们之间有任何类型的关系(一对多、多对多)。

The app is React + Redux based.该应用程序基于 React + Redux。 For getting data from the Store, we're using Reselect library.为了从 Store 获取数据,我们使用了Reselect库。

The problem I'm facing is when I try to get an entity with its relations from the Store.我面临的问题是当我尝试从 Store 获取一个实体及其关系时。

In order to explain the problem better, I've created a simple demo app, that has similar architecture.为了更好地解释这个问题,我创建了一个简单的演示应用程序,它具有类似的架构。 I'll highlight the most important code base.我将重点介绍最重要的代码库。 In the end I'll include a snippet (fiddle) in order to play with it.最后,我将包含一个片段(小提琴)以便使用它。

Demo app演示应用

Business logic商业逻辑

We have Books and Authors.我们有书籍和作者。 One Book has one Author.一本书有一个作者。 One Author has many Books.一位作者拥有多本书。 As simple as possible.尽可能简单。

const authors = [{
  id: 1,
  name: 'Jordan Enev',
  books: [1]
}];

const books = [{
  id: 1,
  name: 'Book 1',
  category: 'Programming',
  authorId: 1
}];

Redux Store Redux 商店

Store is organized in flat structure, compliant with Redux best practices - Normalizing State Shape . Store 以扁平结构组织,符合 Redux 最佳实践 - Normalizing State Shape

Here is the initial state for both Books and Authors Stores:以下是 Books 和 Authors Stores 的初始状态:

const initialState = {
  // Keep entities, by id:
  // { 1: { name: '' } }
  byIds: {},
  // Keep entities ids
  allIds:[]
};

Components组件

The components are organized as Containers and Presentations.这些组件被组织为容器和演示文稿。

<App /> component act as Container (gets all needed data): <App />组件充当容器(获取所有需要的数据):

const mapStateToProps = state => ({
  books: getBooksSelector(state),
  authors: getAuthorsSelector(state),
  healthAuthors: getHealthAuthorsSelector(state),
  healthAuthorsWithBooks: getHealthAuthorsWithBooksSelector(state)
});

const mapDispatchToProps = {
  addBooks, addAuthors
}

const App = connect(mapStateToProps, mapDispatchToProps)(View);

<View /> component is just for the demo. <View />组件仅用于演示。 It pushes dummy data to the Store and renders all Presentation components as <Author />, <Book /> .它将虚拟数据推送到 Store 并将所有 Presentation 组件呈现为<Author />, <Book />

Selectors选择器

For the simple selectors, it looks straightforward:对于简单的选择器,它看起来很简单:

/**
 * Get Books Store entity
 */
const getBooks = ({books}) => books;

/**
 * Get all Books
 */
const getBooksSelector = createSelector(getBooks,
    (books => books.allIds.map(id => books.byIds[id]) ));


/**
 * Get Authors Store entity
 */
const getAuthors = ({authors}) => authors;

/**
 * Get all Authors
 */
const getAuthorsSelector = createSelector(getAuthors,
    (authors => authors.allIds.map(id => authors.byIds[id]) ));

It gets messy, when you have a selector, that computes / queries relational data.当你有一个计算/查询关系数据的选择器时,它会变得混乱。 The demo app includes the following examples:演示应用程序包括以下示例:

  1. Getting all Authors, which have at least one Book in specific category.获取所有作者,其中至少有一本书属于特定类别。
  2. Getting the same Authors, but together with their Books.获得相同的作者,但与他们的书籍在一起。

Here are the nasty selectors:以下是讨厌的选择器:

/**
 * Get array of Authors ids,
 * which have books in 'Health' category
 */  
const getHealthAuthorsIdsSelector = createSelector([getAuthors, getBooks],
    (authors, books) => (
    authors.allIds.filter(id => {
      const author = authors.byIds[id];
      const filteredBooks = author.books.filter(id => (
        books.byIds[id].category === 'Health'
      ));

      return filteredBooks.length;
    })
)); 

/**
 * Get array of Authors,
 * which have books in 'Health' category
 */   
const getHealthAuthorsSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors],
    (filteredIds, authors) => (
    filteredIds.map(id => authors.byIds[id])
)); 

/**
 * Get array of Authors, together with their Books,
 * which have books in 'Health' category
 */    
const getHealthAuthorsWithBooksSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors, getBooks],
    (filteredIds, authors, books) => (
    filteredIds.map(id => ({
        ...authors.byIds[id],
      books: authors.byIds[id].books.map(id => books.byIds[id])
    }))
));

Summing up加起来

  1. As you can see, computing / querying relational data in selectors gets too complicated.如您所见,在选择器中计算/查询关系数据变得太复杂了。
    1. Loading child relations (Author->Books).加载子关系(作者->书籍)。
    2. Filtering by child entities ( getHealthAuthorsWithBooksSelector() ).按子实体过滤 ( getHealthAuthorsWithBooksSelector() )。
  2. There will be too many selector parameters, if an entity has a lot of child relations.如果一个实体有很多子关系,就会有太多的选择器参数。 Checkout getHealthAuthorsWithBooksSelector() and imagine if the Author has a lot of more relations.检查getHealthAuthorsWithBooksSelector()并想象作者是否有更多关系。

So how do you deal with relations in Redux?那么在 Redux 中如何处理关系呢?

It looks like a common use case, but surprisingly there aren't any good practices round.它看起来像一个常见的用例,但令人惊讶的是没有任何好的做法。

* I checked redux-orm library and it looks promising, but its API is still unstable and I'm not sure is it production ready. *我检查了redux-orm库,它看起来很有希望,但它的 API 仍然不稳定,我不确定它是否准备好生产。

 const { Component } = React const { combineReducers, createStore } = Redux const { connect, Provider } = ReactRedux const { createSelector } = Reselect /** * Initial state for Books and Authors stores */ const initialState = { byIds: {}, allIds:[] } /** * Book Action creator and Reducer */ const addBooks = payload => ({ type: 'ADD_BOOKS', payload }) const booksReducer = (state = initialState, action) => { switch (action.type) { case 'ADD_BOOKS': let byIds = {} let allIds = [] action.payload.map(entity => { byIds[entity.id] = entity allIds.push(entity.id) }) return { byIds, allIds } default: return state } } /** * Author Action creator and Reducer */ const addAuthors = payload => ({ type: 'ADD_AUTHORS', payload }) const authorsReducer = (state = initialState, action) => { switch (action.type) { case 'ADD_AUTHORS': let byIds = {} let allIds = [] action.payload.map(entity => { byIds[entity.id] = entity allIds.push(entity.id) }) return { byIds, allIds } default: return state } } /** * Presentational components */ const Book = ({ book }) => <div>{`Name: ${book.name}`}</div> const Author = ({ author }) => <div>{`Name: ${author.name}`}</div> /** * Container components */ class View extends Component { componentWillMount () { this.addBooks() this.addAuthors() } /** * Add dummy Books to the Store */ addBooks () { const books = [{ id: 1, name: 'Programming book', category: 'Programming', authorId: 1 }, { id: 2, name: 'Healthy book', category: 'Health', authorId: 2 }] this.props.addBooks(books) } /** * Add dummy Authors to the Store */ addAuthors () { const authors = [{ id: 1, name: 'Jordan Enev', books: [1] }, { id: 2, name: 'Nadezhda Serafimova', books: [2] }] this.props.addAuthors(authors) } renderBooks () { const { books } = this.props return books.map(book => <div key={book.id}> {`Name: ${book.name}`} </div>) } renderAuthors () { const { authors } = this.props return authors.map(author => <Author author={author} key={author.id} />) } renderHealthAuthors () { const { healthAuthors } = this.props return healthAuthors.map(author => <Author author={author} key={author.id} />) } renderHealthAuthorsWithBooks () { const { healthAuthorsWithBooks } = this.props return healthAuthorsWithBooks.map(author => <div key={author.id}> <Author author={author} /> Books: {author.books.map(book => <Book book={book} key={book.id} />)} </div>) } render () { return <div> <h1>Books:</h1> {this.renderBooks()} <hr /> <h1>Authors:</h1> {this.renderAuthors()} <hr /> <h2>Health Authors:</h2> {this.renderHealthAuthors()} <hr /> <h2>Health Authors with loaded Books:</h2> {this.renderHealthAuthorsWithBooks()} </div> } }; const mapStateToProps = state => ({ books: getBooksSelector(state), authors: getAuthorsSelector(state), healthAuthors: getHealthAuthorsSelector(state), healthAuthorsWithBooks: getHealthAuthorsWithBooksSelector(state) }) const mapDispatchToProps = { addBooks, addAuthors } const App = connect(mapStateToProps, mapDispatchToProps)(View) /** * Books selectors */ /** * Get Books Store entity */ const getBooks = ({ books }) => books /** * Get all Books */ const getBooksSelector = createSelector(getBooks, books => books.allIds.map(id => books.byIds[id])) /** * Authors selectors */ /** * Get Authors Store entity */ const getAuthors = ({ authors }) => authors /** * Get all Authors */ const getAuthorsSelector = createSelector(getAuthors, authors => authors.allIds.map(id => authors.byIds[id])) /** * Get array of Authors ids, * which have books in 'Health' category */ const getHealthAuthorsIdsSelector = createSelector([getAuthors, getBooks], (authors, books) => ( authors.allIds.filter(id => { const author = authors.byIds[id] const filteredBooks = author.books.filter(id => ( books.byIds[id].category === 'Health' )) return filteredBooks.length }) )) /** * Get array of Authors, * which have books in 'Health' category */ const getHealthAuthorsSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors], (filteredIds, authors) => ( filteredIds.map(id => authors.byIds[id]) )) /** * Get array of Authors, together with their Books, * which have books in 'Health' category */ const getHealthAuthorsWithBooksSelector = createSelector([getHealthAuthorsIdsSelector, getAuthors, getBooks], (filteredIds, authors, books) => ( filteredIds.map(id => ({ ...authors.byIds[id], books: authors.byIds[id].books.map(id => books.byIds[id]) })) )) // Combined Reducer const reducers = combineReducers({ books: booksReducer, authors: authorsReducer }) // Store const store = createStore(reducers) const render = () => { ReactDOM.render(<Provider store={store}> <App /> </Provider>, document.getElementById('root')) } render()
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.24/browser.js"></script> <script src="https://npmcdn.com/reselect@3.0.1/dist/reselect.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.3.1/redux.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.6/react-redux.min.js"></script>

JSFiddle . JSFiddle

This reminds me of how I started one of my projects where the data was highly relational.这让我想起了我是如何开始我的一个数据高度相关的项目的。 You think too much still about the backend way of doing things, but you gotta start thinking of more of the JS way of doing things (a scary thought for some, to be sure).您仍然对后端的做事方式考虑太多,但您必须开始更多地考虑 JS 的做事方式(对于某些人来说,这是一个可怕的想法,可以肯定)。

1) Normalized Data in State 1) 状态下的标准化数据

You've done a good job of normalizing your data, but really, it's only somewhat normalized.您在规范化数据方面做得很好,但实际上,它只是稍微规范化了。 Why do I say that?我为什么这么说?

...
books: [1]
...
...
authorId: 1
...

You have the same conceptual data stored in two places.您在两个地方存储了相同的概念数据。 This can easily become out of sync.这很容易变得不同步。 For example, let's say you receive new books from the server.例如,假设您从服务器收到新书。 If they all have authorId of 1, you also have to modify the book itself and add those ids to it!如果他们的authorId为 1,您还必须修改书本身并将这些 ID 添加到其中! That's a lot of extra work that doesn't need to be done.这是许多不需要完成的额外工作。 And if it isn't done, the data will be out of sync.如果不完成,数据将不同步。

One general rule of thumb with a redux style architecture is never store (in the state) what you can compute . redux 风格架构的一个一般经验法则是永远不要存储(在状态中)您可以计算的内容 That includes this relation, it is easily computed by authorId .这包括这个关系,它很容易由authorId计算。

2) Denormalized Data in Selectors 2) 选择器中的非规范化数据

We mentioned having normalized data in the state was not good.我们提到在该州进行标准化数据并不好。 But denormalizing it in selectors is ok right?但是在选择器中对其进行非规范化可以对吗? Well, it is.嗯,是的。 But the question is, is it needed?但问题是,有必要吗? I did the same thing you are doing now, getting the selector to basically act like a backend ORM.我做了你现在正在做的同样的事情,让选择器基本上像一个后端 ORM。 "I just want to be able to call author.books and get all the books!" “我只是希望能够调用author.books并获取所有书籍!” you may be thinking.你可能在想。 It would be so easy to just be able to loop through author.books in your React component, and render each book, right?能够在 React 组件中循环访问author.books并渲染每本书会很容易,对吧?

But, do you really want to normalize every piece of data in your state?但是,您真的想规范化您所在州的每条数据吗? React doesn't need that . React 不需要那个 In fact, it will also increase your memory usage.事实上,它也会增加你的内存使用量。 Why is that?这是为什么?

Because now you will have two copies of the same author , for instance:因为现在您将拥有同一个author两个副本,例如:

const authors = [{
  id: 1,
  name: 'Jordan Enev',
  books: [1]
}];

and

const authors = [{
  id: 1,
  name: 'Jordan Enev',
  books: [{
      id: 1,
      name: 'Book 1',
      category: 'Programming',
      authorId: 1
  }]
}];

So getHealthAuthorsWithBooksSelector now creates a new object for each author, which will not be === to the one in the state.所以getHealthAuthorsWithBooksSelector现在为每个作者创建一个新对象,它不会是===状态中的那个。

This is not bad.这还不错。 But I would say it's not ideal .但我会说这并不理想 On top of the redundant (<- keyword) memory usage, it's better to have one single authoritative reference to each entity in your store.除了冗余(<- 关键字)内存使用之外,最好对商店中的每个实体都有一个单一的权威引用。 Right now, there are two entities for each author that are the same conceptually, but your program views them as totally different objects.现在,每个作者都有两个概念上相同的实体,但您的程序将它们视为完全不同的对象。

So now when we look at your mapStateToProps :所以现在当我们查看你的mapStateToProps

const mapStateToProps = state => ({
  books: getBooksSelector(state),
  authors: getAuthorsSelector(state),
  healthAuthors: getHealthAuthorsSelector(state),
  healthAuthorsWithBooks: getHealthAuthorsWithBooksSelector(state)
});

You are basically providing the component with 3-4 different copies of all the same data.您基本上为组件提供了所有相同数据的 3-4 个不同副本。

Thinking About Solutions思考解决方案

First, before we get to making new selectors and make it all fast and fancy, let's just make up a naive solution.首先,在我们开始制作新的选择器并让它变得快速和花哨之前,让我们先制定一个简单的解决方案。

const mapStateToProps = state => ({
  books: getBooksSelector(state),
  authors: getAuthors(state),
});

Ahh, the only data this component really needs!啊,这个组件真正需要的唯一数据! The books , and the authors . booksauthors Using the data therein, it can compute anything it needs.使用其中的数据,它可以计算任何它需要的东西。

Notice that I changed it from getAuthorsSelector to just getAuthors ?请注意,我将其从getAuthorsSelector更改为仅getAuthors This is because all the data we need for computing is in the books array, and we can just pull the authors by id one we have them!这是因为我们计算所需的所有数据都在books数组中,我们可以通过我们拥有的id 1 拉出作者!

Remember, we're not worrying about using selectors yet, let's just think about the problem in simple terms.请记住,我们还没有担心使用选择器,让我们简单地考虑一下问题。 So, inside the component, let's build an "index" of books by their author.因此,组件内部,让我们构建一个作者书籍的“索引”。

const { books, authors } = this.props;

const healthBooksByAuthor = books.reduce((indexedBooks, book) => {
   if (book.category === 'Health') {
      if (!(book.authorId in indexedBooks)) {
         indexedBooks[book.authorId] = [];
      }
      indexedBooks[book.authorId].push(book);
   }
   return indexedBooks;
}, {});

And how do we use it?我们如何使用它?

const healthyAuthorIds = Object.keys(healthBooksByAuthor);

...
healthyAuthorIds.map(authorId => {
    const author = authors.byIds[authorId];

    return (<li>{ author.name }
       <ul>
         { healthBooksByAuthor[authorId].map(book => <li>{ book.name }</li> }
       </ul>
    </li>);
})
...

Etc etc.等等等等。

But but but you mentioned memory earlier, that's why we didn't denormalize stuff with getHealthAuthorsWithBooksSelector , right?但是,但是您之前提到了内存,这就是为什么我们没有使用getHealthAuthorsWithBooksSelector对内容进行非规范化,对吗? Correct!正确的! But in this case we aren't taking up memory with redundant information.但在这种情况下,我们不会用冗余信息占用内存。 In fact, every single entity, the books and the author s, are just reference to the original objects in the store!事实上,每一个实体, booksauthor ,都只是参考商店中的原始物品! This means that the only new memory being taken up is by the container arrays/objects themselves, not by the actual items in them.这意味着唯一占用的新内存是容器数组/对象本身,而不是其中的实际项目。

I've found this kind of solution ideal for many use cases.我发现这种解决方案非常适合许多用例。 Of course, I don't keep it in the component like above, I extract it into a reusable function which creates selectors based on certain criteria.当然,我不会像上面那样将它保存在组件中,而是将它提取到一个可重用的函数中,该函数根据某些条件创建选择器。 Although, I'll admit I haven't had a problem with the same complexity as yours, in that you have to filter a specific entity, through another entity.虽然,我承认我没有遇到与您相同的复杂性问题,因为您必须通过另一个实体过滤特定实体。 Yikes!哎呀! But still doable.不过还是可以的。

Let's extract our indexer function into a reusable function:让我们将索引器函数提取为可重用的函数:

const indexList = fieldsBy => list => {
 // so we don't have to create property keys inside the loop
  const indexedBase = fieldsBy.reduce((obj, field) => {
    obj[field] = {};
    return obj;
  }, {});

  return list.reduce(
    (indexedData, item) => {
      fieldsBy.forEach((field) => {
        const value = item[field];

        if (!(value in indexedData[field])) {
          indexedData[field][value] = [];
        }

        indexedData[field][value].push(item);
      });

      return indexedData;
    },
    indexedBase,
  );
};

Now this looks like kind of a monstrosity.现在这看起来有点像怪物。 But we must make certain parts of our code complex, so we can make many more parts clean.但是我们必须使代码的某些部分变得复杂,这样我们才能使更多的部分变得干净。 Clean how?怎么清洗?

const getBooksIndexed = createSelector([getBooksSelector], indexList(['category', 'authorId']));
const getBooksIndexedInCategory = category => createSelector([getBooksIndexed],
    booksIndexedBy => {
        return indexList(['authorId'])(booksIndexedBy.category[category])
    });
    // you can actually abstract this even more!

...
later that day
...

const mapStateToProps = state => ({
  booksIndexedBy: getBooksIndexedInCategory('Health')(state),
  authors: getAuthors(state),
});

...
const { booksIndexedBy, authors } = this.props;
const healthyAuthorIds = Object.keys(booksIndexedBy.authorId);

healthyAuthorIds.map(authorId => {
    const author = authors.byIds[authorId];

    return (<li>{ author.name }
       <ul>
         { healthBooksByAuthor[authorId].map(book => <li>{ book.name }</li> }
       </ul>
    </li>);
})
...

This is not as easy to understand of course, because it relies primarily on composing these functions and selectors to build representations of data, instead of renormalizing it.这当然不是那么容易理解,因为它主要依赖于组合这些函数和选择器来构建数据的表示,而不是重新规范化它。

The point is: We're not looking to recreate copies of the state with normalized data.关键是:我们不希望使用规范化数据重新创建状态副本。 We're trying to *create indexed representations (read: references) of that state which are easily digested by components.我们正在尝试*创建该状态的索引表示(阅读:引用),这些表示很容易被组件消化。

The indexing I've presented here is very reusable, but not without certain problems (I'll let everyone else figure those out).我在这里介绍的索引是非常可重用的,但并非没有某些问题(我会让其他人解决这些问题)。 I don't expect you to use it, but I do expect you to learn this from it: rather than trying to coerce your selectors to give you backend-like, ORM-like nested versions of your data, use the inherent ability to link your data using the tools you already have: ids and object references.我不希望您使用它,但我确实希望您从中学到这一点:与其试图强制您的选择器为您提供类似后端、类似 ORM 的数据嵌套版本,不如使用固有的链接能力使用您已有的工具处理您的数据:ID 和对象引用。

These principles can even be applied to your current selectors.这些原则甚至可以应用于您当前的选择器。 Rather than create a bunch of highly specialized selectors for every conceivable combination of data... 1) Create functions that create selectors for you based on certain parameters 2) Create functions that can be used as the resultFunc of many different selectors而不是为每个可能的数据组合创建一堆高度专业化的选择器...... 1)创建基于某些参数为您创建选择器的函数 2)创建可用作许多不同选择器的resultFunc的函数

Indexing isn't for everyone, I'll let others suggest other methods.索引并不适合所有人,我会让其他人建议其他方法。

Author of the question's here!问题的作者在这里!

One year later, now I'm going to summarize my experience and thoughts here.一年后,现在我要在这里总结一下我的经验和想法。

I was looking into two possible approaches for handling the relational data:我正在研究处理关系数据的两种可能方法:

1. Indexing 1. 索引

aaronofleonard , already gave us a great and very detailed answer here , where his main concept is as follows: aaronofleonard ,已经在这里给了我们一个很好而且非常详细的答案,他的主要概念如下:

We're not looking to recreate copies of the state with normalized data.我们不希望使用规范化数据重新创建状态副本。 We're trying to *create indexed representations (read: references) of that state which are easily digested by components.我们正在尝试*创建该状态的索引表示(阅读:引用),这些表示很容易被组件消化。

It perfectly fits to the examples, he mentions.他提到,它非常适合这些示例。 But it's important to highlight that his examples create indexes only for one-to-many relations (one Book has many Authors).但重要的是要强调他的例子只为一对多关系创建索引(一本书有很多作者)。 So I started to think about how this approach will fit to all my possible requirements:所以我开始考虑这种方法如何满足我所有可能的要求:

  1. Handing many-to-many cases.处理多对多案件。 Example: One Book has many Authors, through BookStore.示例:通过 BookStore,一本书有多个作者。
  2. Handling Deep filtration .处理深度过滤 Example: Get all the Books from the Healthy Category, where at least on Author is from a specific Country.示例:从健康类别中获取所有书籍,其中至少作者来自特定国家/地区。 Now just imagine if we have many more nested levels of entities.现在想象一下,如果我们有更多嵌套级别的实体。

Of course it's doable, but as you can see the things can get serious very soon.当然这是可行的,但正如你所看到的,事情很快就会变得严重。

If you feel comfortable with managing such complexity with Indexing, then make sure you have enough design time for creating your selectors and composing indexing utilities.如果您对使用索引管理这种复杂性感到满意,那么请确保您有足够的设计时间来创建选择器和组合索引实用程序。

I continued searching for a solution, because creating such an Indexing utility looks totally out-of-scope for the project.我继续寻找解决方案,因为创建这样的索引实用程序看起来完全超出了项目的范围。 It's more like creating a third-party library.这更像是创建一个第三方库。

So I decided to give a try to Redux-ORM library.所以我决定尝试一下Redux-ORM库。

2. Redux-ORM 2. Redux-ORM

A small, simple and immutable ORM to manage relational data in your Redux store.一个小型、简单且不可变的 ORM,用于管理 Redux 存储中的关系数据。

Without being verbose, here's how I managed all the requirements, just using the library:不冗长,以下是我管理所有需求的方式,仅使用库:

// Handing many-to-many case.
const getBooks = createSelector({ Book } => {
  return Books.all().toModelArray()
   .map( book => ({ 
     book: book.ref,
     authors: book.authors.toRefArray()
   })
})

// Handling Deep filtration.
// Keep in mind here you can pass parameters, instead of hardcoding the filtration criteria.
const getFilteredBooks = createSelector({ Book } => {
  return Books.all().toModelArray()
   .filter( book => {
     const authors = book.authors.toModelArray()
     const hasAuthorInCountry = authors.filter(a => a.country.name === 'Bulgaria').length

     return book.category.type === 'Health' && hasAuthorInCountry
   })
   .map( book => ({ 
     book: book.ref,
     authors: book.authors.toRefArray()
   })
})

As you can see - the library handles all the relations for us and we can easily access all the child entities and perform complex computation.如您所见 - 该库为我们处理所有关系,我们可以轻松访问所有子实体并执行复杂的计算。

Also using .ref we return the entity Store's reference, instead of creating a new object copy (you're worried about the memory).同样使用.ref我们返回实体 Store 的引用,而不是创建一个新的对象副本(您担心内存)。

So having this type of selectors my flow is as follows:所以有这种类型的选择器我的流程如下:

  1. Container components fetches the data via API.容器组件通过 API 获取数据。
  2. Selectors get only the needed slice of data.选择器只获取所需的数据片段。
  3. Render the Presentation components.呈现 Presentation 组件。

However, nothing is perfect as it sounds as.然而,没有什么是完美的。 Redux-ORM deals with relational operations as querying, filtering, etc. in a very easy of use way. Redux-ORM 以一种非常易于使用的方式处理查询、过滤等关系操作。 Cool!凉爽的!

But when we talk about selectors reusability, composition, extending and so on - it's kind of tricky and awkward task.但是当我们谈论选择器的可重用性、组合、扩展等时 - 这是一种棘手和尴尬的任务。 It's not a Redux-ORM problem, than to the reselect library itself and the way it works.这不是 Redux-ORM 问题,而是reselect库本身及其工作方式的问题。 Here we discussed the topic.在这里我们讨论了这个话题。

Conclusion (personal)结论(个人)

For simpler relational projects I would give a try to the Indexing approach.对于更简单的关系项目,我会尝试使用索引方法。

Otherwise, I would stick with Redux-ORM, as I used it in the App, for which one I asked the question.否则,我会坚持使用 Redux-ORM,因为我在应用程序中使用了它,为此我提出了这个问题。 There I have 70+ entities and still counting!在那里,我有 70 多个实体,而且还在计数!

When you start "overloading" your selectors (like getHealthAuthorsSelector ) with other named selectors (like getHealthAuthorsWithBooksSelector , ...) you might end up with something like getHealthAuthorsWithBooksWithRelatedBooksSelector etc etc.当您开始使用其他命名选择器(例如getHealthAuthorsWithBooksSelector ,...)“重载”您的选择器(例如getHealthAuthorsSelector )时,您最终可能会得到类似getHealthAuthorsWithBooksWithRelatedBooksSelector等的结果。

That is not sustainable.那是不可持续的。 I suggest you stick to the high level ones (ie getHealthAuthorsSelector ) and use a mechanism so that their books and the related books of those books etc are always available.我建议您坚持使用高级别的(即getHealthAuthorsSelector )并使用一种机制,以便他们的书籍和这些书籍的相关书籍等始终可用。

You can use TypeScript and turn the author.books into a getter, or just work with covenience functions to get the books from the store whenever they are needed.您可以使用 TypeScript 并将author.books转换为 getter,或者仅使用便利函数在需要时从商店中获取书籍。 With an action you can combine a get from store with a fetch from db to display (possibly) stale data directly and have Redux/React take care of the visual update once the data is retrieved from the database.通过一个操作,您可以将来自 store 的 get 与来自 d​​b 的 fetch 结合起来,以直接显示(可能)陈旧的数据,并在从数据库中检索数据后让 Redux/React 负责视觉更新。

I hadn't heard of this Reselect but it seems like it might be a good way to have all sorts of filters in one place to avoid duplicating code in components.我没有听说过这个 Reselect,但它似乎是将各种过滤器放在一个地方以避免在组件中重复代码的好方法。
Simple as they are, they are also easily testable.尽管它们很简单,但它们也很容易测试。 Business/Domain logic testing is usually a (very?) good idea, especially when you are not a domain expert yourself.业务/领域逻辑测试通常是一个(非常?)好主意,尤其是当您自己不是领域专家时。

Also keep in mind that a joining of multiple entities into something new is useful from time to time, for example flattening entities so they can be bound easily to a grid control.还要记住,有时将多个实体连接到新的东西中是有用的,例如展平实体,以便它们可以轻松地绑定到网格控件。

There is a library that solves relational selects: ngrx-entity-relationship .有一个解决关系选择的库: ngrx-entity-relationship

Its similar demo is here on codesandboxcodeandbox上有类似的演示

For the case with books and authors, it would be like that:对于书籍和作者的情况,它会是这样的:

The next code we need to define once.下一段代码我们需要定义一次。

// first we need proper state selectors, because of custom field names
const bookState = stateKeys(getBooks, 'byIds', 'allIds');
const authorState = stateKeys(getAuthors, 'byIds', 'allIds');

// now let's define root and relationship selector factories
const book = rootEntitySelector(bookState);
const bookAuthor = relatedEntitySelector(
  authorState,
  'authorId',
  'author'
);

// the same for authors
const author = rootEntitySelector(authorState);
const authorBooks = relatedEntitySelector(
  bookState,
  'books', // I would rename it to `booksId`
  'booksEntities', // and would use here `books`
);

Now we can build selectors and reuse them if it is needed.现在我们可以构建选择器并在需要时重用它们。

// now we can build a selector
const getBooksWithAuthors = rootEntities(
  book(
    bookAuthor(
      authorBooks(), // if we want to go crazy
    ),
  ),
);

// and connect it
const mapStateToProps = state => ({
  books: getBooksWithAuthors(state, [1, 2, 3]), // or a selector for ids
  // ...
});

The result would be结果是

this.props.books = [
  {
    id: 1,
    name: 'Book 1',
    category: 'Programming',
    authorId: 1
    author: {
      id: 1,
      name: 'Jordan Enev',
      books: [1],
      booksEntities: [
        {
          id: 1,
          name: 'Book 1',
          category: 'Programming',
          authorId: 1,
        },
      ],
    },
  },
];

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM