我正在创建的应用程序有很多实体和关系(数据库是关系的)。为了得到一个想法,有25+实体,它们之间有任何类型的关系(一对多,多对多)。
该应用程序是基于React + Redux的。为了从商店中获取数据,我们使用重选库。
我面临的问题是当我试图从Store.获得一个实体与它的关系时
为了更好地解释这个问题,我创建了一个简单的演示应用程序,它具有类似的架构。我将突出强调最重要的代码库。最后,我将包括一个片段(小提琴),以发挥它。
演示应用程序
业务逻辑
我们有书和作者。一本书只有一个作者。一位作家有很多书。尽可能简单。
const authors = [{
id: 1,
name: 'Jordan Enev',
books: [1]
}];
const books = [{
id: 1,
name: 'Book 1',
category: 'Programming',
authorId: 1
}];Redux商店
商店是按扁平结构组织的,符合Redux最佳实践- 正火状态形状。
以下是Books和Authors的初始状态:
const initialState = {
// Keep entities, by id:
// { 1: { name: '' } }
byIds: {},
// Keep entities ids
allIds:[]
};组件
这些组件被组织成容器和演示文稿。
<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 />组件只用于演示。它将虚拟数据推送到存储区,并将所有表示组件呈现为<Author />, <Book />。
选择器
对于简单的选择器,它看起来很简单:
/**
* 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]) ));当您有一个选择器时,计算/查询关系数据会变得很麻烦。演示应用程序包括以下示例:
下面是令人讨厌的选择器:
/**
* 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])
}))
));总结
getHealthAuthorsWithBooksSelector())。
getHealthAuthorsWithBooksSelector(),想象一下作者是否有更多的关系。那么你如何处理Redux中的关系?
它看起来像一个常见的用例,但令人惊讶的是,这里没有任何好的实践。
*我检查了https://github.com/tommikaikkonen/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>
发布于 2017-07-28 17:57:28
这让我想起了我是如何启动一个数据高度相关的项目的。你还在想做事情的后端方式,但你必须开始考虑更多的JS做事方式(当然,这对某些人来说是个可怕的想法)。
1)各州的标准化数据
您在数据正常化方面做得很好,但实际上,它只是某种程度上的规范化。我为什么要这么说?
...
books: [1]
...
...
authorId: 1
...同样的概念数据存储在两个地方。这很容易变得不同步。例如,假设您从服务器收到新书。如果它们都有1的authorId,您还必须修改书本身并将这些it添加到其中!这是很多不需要做的额外工作。如果没有完成,数据就会不同步。
使用redux风格体系结构的一个一般经验规则是,永远不要(在状态中)存储可以计算的内容。其中包括这种关系,它很容易由authorId计算。
2)选择器中的非规范化数据
我们提到,在这个州有规范化的数据是不好的。但是在选择器中去甲基化是可以的,对吧?是的,确实如此。但问题是,它需要吗?我做了同样的事情,你现在正在做的事,让选择器基本上表现为后端ORM。“我只想打电话给author.books,拿到所有的书!”你可能在想。只需在React组件中遍历author.books并呈现每本书就很容易了,对吗?
但是,您真的想要将状态中的每一段数据规范化吗?React不需要。事实上,它也会增加您的内存使用量。为什么会这样呢?
因为现在您将拥有两个相同author的副本,例如:
const authors = [{
id: 1,
name: 'Jordan Enev',
books: [1]
}];和
const authors = [{
id: 1,
name: 'Jordan Enev',
books: [{
id: 1,
name: 'Book 1',
category: 'Programming',
authorId: 1
}]
}];因此,getHealthAuthorsWithBooksSelector现在为每个作者创建一个新对象,它将不是状态中的===。
这还不错。但我想说这并不理想。在冗余(<-关键字)内存使用的基础上,最好有一个对存储中的每个实体的权威引用。现在,对于每个作者来说,有两个实体在概念上是相同的,但是您的程序将它们视为完全不同的对象。
所以现在我们来看看你的mapStateToProps
const mapStateToProps = state => ({
books: getBooksSelector(state),
authors: getAuthorsSelector(state),
healthAuthors: getHealthAuthorsSelector(state),
healthAuthorsWithBooks: getHealthAuthorsWithBooksSelector(state)
});您基本上是在向组件提供所有相同数据的3-4个不同副本。
关于解决方案的思考
首先,在我们开始制造新的选择器并使其快速和花哨之前,让我们简单地提出一个简单的解决方案。
const mapStateToProps = state => ({
books: getBooksSelector(state),
authors: getAuthors(state),
});啊,这个组件唯一需要的数据!books和authors。使用其中的数据,它可以计算它需要的任何东西。
注意到我把它从getAuthorsSelector改为getAuthors?这是因为我们计算所需的所有数据都在books数组中,我们只需使用id one就可以拉出作者了!
记住,我们还没有担心选择器的使用,让我们简单地考虑一下这个问题。因此, an 组件,让我们构建一个由作者编写的图书“索引”。
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;
}, {});我们该怎么用呢?
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>);
})
...等。
但是,但是我刚才提到了内存,这就是为什么我们没有用getHealthAuthorsWithBooksSelector__去修饰东西的原因,对吗?对,是这样!但在这种情况下,我们不会用多余的信息占用内存。实际上,每个实体,books和author,都只是引用存储中的原始对象!这意味着所占用的唯一新内存是容器数组/对象本身,而不是它们中的实际项。
我发现这种解决方案对于许多用例来说都是理想的。当然,我不像上面那样把它保存在组件中,我将它提取到一个可重用的函数中,它根据特定的条件创建选择器。不过,我得承认,我还没有遇到像您这样复杂的问题,因为您必须过滤特定的实体,通过通过过滤另一个实体。呀!但还是可行的。
让我们将索引器函数解压缩为可重用的函数:
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,
);
};现在这看起来像个怪物。但是我们必须使代码的某些部分变得复杂,这样才能使更多的部分变得干净。怎么清理?
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>);
})
...当然,这并不容易理解,因为它主要依赖于组合这些函数和选择器来构建数据的表示,而不是重命名它。
重点是:我们不希望用规范化的数据重新创建状态的副本。我们试图*创建该状态的索引表示(阅读:引用),这些表示很容易被组件消化。
我在这里介绍的索引非常可重用,但并非没有某些问题(我将让其他人解决这些问题)。我不希望你使用它,但我希望你能从中学到这一点:与其试图强迫你的选择器给你提供类似于后端的、类似ORM的数据嵌套版本,不如使用你已经拥有的工具: ids和对象引用来链接你的数据。
这些原则甚至可以应用于当前的选择器。而不是为所有可以想象的数据组合创建一组高度专业化的选择器.1)根据某些参数为您创建选择器的函数;2)创建可用作许多不同选择器的resultFunc的函数
索引并不适合每个人,我会让其他人提出其他方法。
发布于 2018-06-29 13:15:28
问题的作者来了!
一年后,现在我要在这里总结我的经验和想法。
我正在研究两种处理关系数据的可能方法:
1.索引编制
莱昂纳德已经给了我们一个非常好的、非常详细的答案,这里,他的主要概念如下:
我们不希望用规范化的数据重新创建状态的副本。我们试图*创建该状态的索引表示(阅读:引用),这些表示很容易被组件消化。
他提到,这完全符合这些例子。但是必须强调的是,他的例子只为一对多的关系创建索引(一本书有许多作者)。因此,我开始考虑这种方法如何适合我所有可能的需求:
当然,这是可行的,但正如你所看到的,事情很快就会变得严肃起来。
如果您对使用索引管理这种复杂性感到满意,那么请确保您有足够的设计时间来创建选择器和编写索引实用程序。
我继续寻找解决方案,因为创建这样一个索引实用程序看起来完全超出了项目的范围。更像是创建第三方图书馆。
所以我决定尝试一下Redux-ORM库。
2. Redux-ORM
用于管理Redux存储中的关系数据的小型、简单和不可变的ORM。
在不冗长的情况下,下面是我如何管理所有需求的方法,只需使用库:
// 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()
})
})如您所见-库为我们处理所有关系,我们可以轻松地访问所有子实体并执行复杂的计算。
同样使用.ref,我们返回实体商店的引用,而不是创建一个新的对象副本(您担心内存)。
因此,有了这种类型的选择器,我的流程如下:
然而,没有什么是完美的,因为它听起来。Redux以一种非常容易使用的方式处理关系操作,如查询、筛选等。凉爽的!
但是当我们谈到选择器的可重用性、合成、扩展等等时--这是一项棘手而又尴尬的任务。这不是一个Redux问题,而是reselect库本身及其工作方式的问题。这里我们讨论了这个话题。
结论(个人)
对于更简单的关系项目,我会尝试使用索引方法。
否则,我会坚持使用Redux,因为我在应用程序中使用它,其中一个我提出了问题。在那里,我有70+实体,而且还在计数!
发布于 2017-07-28 15:17:19
当您开始用其他命名选择器“重载”选择器(如getHealthAuthorsSelector)时(如getHealthAuthorsWithBooksSelector,.)你可能最终会得到类似于getHealthAuthorsWithBooksWithRelatedBooksSelector等的东西。
这是不可持续的。我建议你坚持高层次的(即getHealthAuthorsSelector),并使用一种机制,使他们的书籍和相关的书籍等总是可用的。
您可以使用TypeScript并将author.books转换为getter,也可以使用方便函数在需要时从商店获取书籍。通过操作,您可以将get从存储区与从db的fetch组合起来,直接显示(可能)陈旧的数据,并让Redux/React在从数据库中检索数据后负责可视化更新。
我没有听说过这个Reselect,但是似乎在一个地方设置各种过滤器以避免组件中的代码重复似乎是一种很好的方法。
虽然它们很简单,但也很容易测试。业务/领域逻辑测试通常是(非常?)好主意,尤其是当你自己不是领域专家的时候。
还请记住,将多个实体连接到新的东西有时是有用的,例如,扁平实体,以便它们可以轻松地绑定到网格控件。
https://stackoverflow.com/questions/45373369
复制相似问题