集合
OneToMany
和 ManyToMany
属性存储在 Collection
封装器中。
¥OneToMany
and ManyToMany
properties are stored in a Collection
wrapper.
使用集合
¥Working with collections
Collection
类实现了迭代器,因此我们可以使用 for of
循环对其进行迭代。
¥The Collection
class implements iterator, so we can use for of
loop to iterate through it.
访问集合项的另一种方法是使用括号语法,就像我们访问数组项时一样。请记住,这种方法不会检查集合是否已初始化,而在这种情况下使用 get
方法会抛出错误。
¥Another way to access collection items is to use bracket syntax like when we access array items. Keep in mind that this approach will not check if the collection is initialized, while using get
method will throw error in this case.
请注意,
Collection
中的数组访问仅可用于读取已加载的项目,我们无法通过这种方式将新项目添加到Collection
。¥Note that array access in
Collection
is available only for reading already loaded items, we cannot add new items toCollection
this way.
要获取存储在 Collection
中的所有实体,我们可以使用 getItems()
方法。如果 Collection
未初始化,它将抛出。如果我们想禁用此验证,我们可以使用 getItems(false)
。这将为我们提供由身份映射管理的实体实例。
¥To get all entities stored in a Collection
, we can use getItems()
method. It will throw in case the Collection
is not initialized. If we want to disable this validation, we can use getItems(false)
. This will give us the entity instances managed by the identity map.
或者,我们可以使用 toArray()
将 Collection
序列化为 DTO 数组。修改这些不会对实际实体实例产生影响。
¥Alternatively we can use toArray()
which will serialize the Collection
to an array of DTOs. Modifying those will have no effect on the actual entity instances.
const author = em.findOne(Author, '...', { populate: ['books'] }); // populating books collection
// Or we could lazy load books collection later via `load()` method.
// Unlike `init()` it will check the state and do nothing if the collection is already initialized.
// await author.books.load();
for (const book of author.books) {
console.log(book.title); // initialized
console.log(book.author.isInitialized()); // true
console.log(book.author.id);
console.log(book.author.name); // Jon Snow
console.log(book.publisher); // just reference
console.log(book.publisher.isInitialized()); // false
console.log(book.publisher.id);
console.log(book.publisher.name); // undefined
}
// collection needs to be initialized before we can work with it
author.books.add(book);
console.log(author.books.contains(book)); // true
console.log(author.books.exists(item => item === book)); // true
console.log(author.books.find(item => item === book)); // book
console.log(author.books.map(item => item.title)); // array of book titles
console.log(author.books.filter(item => item.title.startsWith('Foo'))); // array of books matching the callback
author.books.remove(book);
console.log(author.books.contains(book)); // false
author.books.add(book);
console.log(author.books.count()); // 1
console.log(author.books.slice(0, 1)); // Book[]
console.log(author.books.slice()); // Book[]
console.log(author.books.slice().length); // 1
author.books.removeAll();
console.log(author.books.isEmpty()); // true
console.log(author.books.contains(book)); // false
console.log(author.books.count()); // 0
console.log(author.books.getItems()); // Book[]
console.log(author.books.getIdentifiers()); // array of string | number
console.log(author.books.getIdentifiers('_id')); // array of ObjectId
// array access works as well
console.log(author.books[1]); // Book
console.log(author.books[12345]); // undefined, even if the collection is not initialized
// getting array of the items
console.log(author.books.getItems()); // Book[]
// serializing the collection
console.log(author.books.toArray()); // EntityDTO<Book>[]
const author = em.findOne(Author, '...'); // books collection has not been populated
const count = await author.books.loadCount(); // gets the count of collection items from database instead of counting loaded items
console.log(author.books.getItems()); // throws because the collection has not been initialized
// initialize collection if not already loaded and return its items as array
console.log(await author.books.loadItems()); // Book[]
从集合中删除项目
¥Removing items from collection
从集合中删除项目并不一定意味着删除目标实体,而是意味着我们正在断开关系 - 从集合中删除项目,而不是从数据库中删除实体 - Collection.remove()
与 em.remove()
不同。当你使用 em.assign()
更新实体时,你还可以从集合中删除/断开实体,它们不会自动从数据库中删除。如果我们想通过从集合中删除实体来删除它,我们需要启用 orphanRemoval: true
,它告诉 ORM 我们不希望存在孤立的实体,所以我们知道应该删除这些实体。还请检查 孤立删除 上的文档
¥Removing items from a collection does not necessarily imply deleting the target entity, it means we are disconnecting the relation - removing items from collection, not removing entities from database - Collection.remove()
is not the same as em.remove()
. When you use em.assign()
to update entities you can also remove/disconnect entities from a collection, they do not get automatically removed from the database. If we want to delete the entity by removing it from collection, we need to enable orphanRemoval: true
, which tells the ORM we don't want orphaned entities to exist, so we know those should be removed. Also check the documentation on Orphan Removal
一对多集合
¥OneToMany Collections
OneToMany
集合是 ManyToOne
引用的反面,它们需要通过 fk
属性指向它:
¥OneToMany
collections are inverse side of ManyToOne
references, to which they need to point via fk
attribute:
@Entity()
export class Book {
@PrimaryKey()
_id!: ObjectId;
@ManyToOne()
author!: Author;
}
@Entity()
export class Author {
@PrimaryKey()
_id!: ObjectId;
@OneToMany(() => Book, book => book.author)
books1 = new Collection<Book>(this);
// or via options object
@OneToMany({ entity: () => Book, mappedBy: 'author' })
books2 = new Collection<Book>(this);
}
多对多集合
¥ManyToMany Collections
对于 ManyToMany,SQL 驱动程序使用保存对两个实体的引用的枢轴表。
¥For ManyToMany, SQL drivers use pivot table that holds reference to both entities.
与它们相反,使用 MongoDB 我们不需要为 ManyToMany
关系设置连接表。所有引用都作为 ObjectId
数组存储在拥有实体上。
¥As opposed to them, with MongoDB we do not need to have join tables for ManyToMany
relations. All references are stored as an array of ObjectId
s on owning entity.
单向
¥Unidirectional
单向 ManyToMany
关系仅在一侧定义,如果我们仅定义 entity
属性,则它将被视为拥有方:
¥Unidirectional ManyToMany
relations are defined only on one side, if we define only entity
attribute, then it will be considered the owning side:
@ManyToMany(() => Book)
books1 = new Collection<Book>(this);
// or mark it as owner explicitly via options object
@ManyToMany({ entity: () => Book, owner: true })
books2 = new Collection<Book>(this);
双向
¥Bidirectional
在两侧都定义了双向 ManyToMany
关系,而一侧是拥有方(存储引用),由指向反侧的 inversedBy
属性标记:
¥Bidirectional ManyToMany
relations are defined on both sides, while one is owning side (where references are store), marked by inversedBy
attribute pointing to the inverse side:
@ManyToMany(() => BookTag, tag => tag.books, { owner: true })
tags = new Collection<BookTag>(this);
// or via options object
@ManyToMany({ entity: () => BookTag, inversedBy: 'books' })
tags = new Collection<BookTag>(this);
而在反面,我们用 mappedBy
属性来定义它,该属性指向所有者:
¥And on the inversed side we define it with mappedBy
attribute pointing back to the owner:
@ManyToMany(() => Book, book => book.tags)
books = new Collection<Book>(this);
// or via options object
@ManyToMany({ entity: () => Book, mappedBy: 'tags' })
books = new Collection<Book>(this);
自定义数据透视表实体
¥Custom pivot table entity
默认情况下,生成的数据透视表实体在后台用于表示数据透视表。从 v5.1 开始,我们可以通过 pivotEntity
选项提供自己的实现。
¥By default, a generated pivot table entity is used under the hood to represent the pivot table. Since v5.1 we can provide our own implementation via pivotEntity
option.
数据透视表实体需要恰好具有两个多对一属性,其中第一个属性需要指向拥有实体,第二个属性需要指向多对多关系的目标实体。
¥The pivot table entity needs to have exactly two many-to-one properties, where first one needs to point to the owning entity and the second to the target entity of the many-to-many relation.
@Entity()
export class Order {
@ManyToMany({ entity: () => Product, pivotEntity: () => OrderItem })
products = new Collection<Product>(this);
}
对于双向 M:N 关系,仅在拥有方指定 pivotEntity
选项就足够了。我们仍然需要通过 inversedBy
或 mappedBy
选项链接双方。
¥For bidirectional M:N relations, it is enough to specify the pivotEntity
option only on the owning side. We still need to link the sides via inversedBy
or mappedBy
option.
@Entity()
export class Product {
@ManyToMany({ entity: () => Order, mappedBy: o => o.products })
orders = new Collection<Order>(this);
}
如果我们想将新项目添加到这样的 M:N 集合中,我们需要拥有所有非 FK 属性来定义数据库级别的默认值。
¥If we want to add new items to such M:N collection, we need to have all non-FK properties to define a database level default value.
@Entity()
export class OrderItem {
@ManyToOne({ primary: true })
order: Order;
@ManyToOne({ primary: true })
product: Product;
@Property({ default: 1 })
amount!: number;
}
或者,我们可以直接使用 pivot 实体:
¥Alternatively, we can work with the pivot entity directly:
// create new item
const item = em.create(OrderItem, {
order: 123,
product: 321,
amount: 999,
});
await em.persist(item).flush();
// or remove an item via delete query
const em.nativeDelete(OrderItem, { order: 123, product: 321 });
我们也可以像前面的示例一样定义针对枢轴实体的 1:m 属性,并使用它来修改集合,同时使用 M:N 属性以便于阅读和过滤。
¥We can as well define the 1:m properties targeting the pivot entity as in the previous example, and use that for modifying the collection, while using the M:N property for easier reading and filtering purposes.
强制集合项的固定顺序
¥Forcing fixed order of collection items
由于此事件实体将引用 ,并将能够调用 方法(包括所有实体引用和集合)。
¥Since v3 many to many collections does not require having auto increment primary key, that was used to ensure fixed order of collection items.
为了保留集合的固定顺序,我们可以使用 fixedOrder: true
属性,它将按 id
列开始排序。模式生成器将转换数据透视表以具有自动增量主键 id
。我们还可以通过 fixedOrderColumn: 'order'
更改顺序列名称。
¥To preserve fixed order of collections, we can use fixedOrder: true
attribute, which will start ordering by id
column. Schema generator will convert the pivot table to have auto increment primary key id
. We can also change the order column name via fixedOrderColumn: 'order'
.
我们还可以通过 orderBy: { ... }
属性指定默认顺序。当我们完全填充包括其项目在内的集合时,将使用它,因为它按引用的实体属性而不是数据透视表列(fixedOrderColumn
是)排序。另一方面,fixedOrder
用于维护项目的插入顺序,而不是按某些属性排序。
¥We can also specify default ordering via orderBy: { ... }
attribute. This will be used when we fully populate the collection including its items, as it orders by the referenced entity properties instead of pivot table columns (which fixedOrderColumn
is). On the other hand, fixedOrder
is used to maintain the insert order of items instead of ordering by some property.
填充引用
¥Populating references
有时我们可能只想知道哪些项目是集合的一部分,而我们不关心这些项目的值。为此,我们可以仅使用引用填充集合:
¥Sometimes we might want to know only what items are part of a collection, and we don't care about the values of those items. For this, we can populate the collection only with references:
const book1 = await em.findOne(Book, 1, { populate: ['tags:ref'] });
console.log(book1.tags.isInitialized()); // true
console.log(wrap(book1.tags[0]).isInitialized()); // false
// or alternatively use `init({ ref: true })`
const book2 = await em.findOne(Book, 1);
await book2.tags.init({ ref: true });
console.log(book2.tags.isInitialized()); // true
console.log(wrap(book2.tags[0]).isInitialized()); // false
Collection 的 add() 和 remove() 操作的传播
¥Propagation of Collection's add() and remove() operations
当我们使用 Collection.add()
方法之一时,该项目将添加到给定的集合中,并且此操作也会传播到其对应项。
¥When we use one of Collection.add()
method, the item is added to given collection, and this action is also propagated to its counterpart.
// one to many
const author = new Author(...);
const book = new Book(...);
author.books.add(book);
console.log(book.author); // author will be set thanks to the propagation
对于 M:N,这可以双向工作,无论是从拥有方还是从反向方。
¥For M:N this works in both ways, either from owning side, or from inverse side.
// many to many works both from owning side and from inverse side
const book = new Book(...);
const tag = new BookTag(...);
book.tags.add(tag);
console.log(tag.books.contains(book)); // true
tag.books.add(book);
console.log(book.tags.contains(tag)); // true
从 v5.2.2 开始,如果拥有的集合未初始化,则将新项目添加到逆侧 M:N 关系的传播也有效。对于删除操作的传播,双方仍然必须初始化。
¥Since v5.2.2 propagation of adding new items to inverse side M:N relation also works if the owning collection is not initialized. For propagation of remove operation, both sides still have to be initialized.
尽管这种传播也适用于 M:N 逆向侧,但我们应该始终使用拥有侧来操作集合。
¥Although this propagation works also for M:N inverse side, we should always use owning side to manipulate the collection.
同样适用于 Collection.remove()
。
¥Same applies for Collection.remove()
.
集合项的过滤和排序
¥Filtering and ordering of collection items
通过 collection.init()
初始化集合项时,你可以过滤集合以及对其项进行排序:
¥When initializing collection items via collection.init()
, you can filter the collection as well as order its items:
await book.tags.init({
where: { active: true },
orderBy: { name: QueryOrder.DESC },
});
你永远不应该修改部分加载的集合。
¥You should never modify partially loaded collections.
声明式部分加载
¥Declarative partial loading
集合也可以仅表示目标实体的子集:
¥Collections can also represent only a subset of the target entities:
@Entity()
class Author {
@OneToMany(() => Book, b => b.author)
books = new Collection<Book>(this);
@OneToMany(() => Book, b => b.author, { where: { favorite: true } })
favoriteBooks = new Collection<Book>(this);
}
这也适用于 M:N 关系。请注意,如果你想声明更多映射到同一个数据透视表的关系,则需要明确指定其名称(或使用相同的数据透视实体):
¥This works also for M:N relations. Note that if you want to declare more relations mapping to the same pivot table, you need to explicitly specify its name (or use the same pivot entity):
@Entity()
class Book {
@ManyToMany(() => BookTag)
tags = new Collection<BookTag>(this);
@ManyToMany({
entity: () => BookTag,
pivotTable: 'book_tags',
where: { popular: true },
})
popularTags = new Collection<BookTag>(this);
}
过滤集合
¥Filtering Collections
集合具有 matching
方法,允许从集合中切分部分数据。默认情况下,它将根据查询返回实体列表。我们可以使用 store
布尔参数将此列表保存到集合项目中 - 这会将集合标记为 readonly
,add
或 remove
等方法将抛出。
¥Collections have a matching
method that allows to slice parts of data from a collection. By default, it will return the list of entities based on the query. We can use the store
boolean parameter to save this list into the collection items - this will mark the collection as readonly
, methods like add
or remove
will throw.
const a = await em.findOneOrFail(Author, 1);
// only loading the list of items
const books = await a.books.matching({ limit: 3, offset: 10, orderBy: { title: 'asc' } });
console.log(books); // [Book, Book, Book]
console.log(a.books.isInitialized()); // false
// storing the items in collection
const tags = await books[0].tags.matching({
limit: 3,
offset: 5,
orderBy: { name: 'asc' },
store: true,
});
console.log(tags); // [BookTag, BookTag, BookTag]
console.log(books[0].tags.isInitialized()); // true
console.log(books[0].tags.getItems()); // [BookTag, BookTag, BookTag]
映射集合项
¥Mapping Collection items
Collection
类提供了几种方便的辅助方法来过滤、映射或转换集合项。
¥The Collection
class offers several handy helper methods to filter, map, or convert the collection items.
indexBy
当你想将集合转换为简单的键值字典时,请使用 indexBy()
方法:
¥When you want to convert the collection to a simple key-value dictionary, use the indexBy()
method:
// given `user.settings` is `Collection<Option>`
const settingsDictionary = user.settings.indexBy('key');
// `settingsDictionary` is `Record<string, Option>`
第二个参数允许你映射到属性值而不是目标实体:
¥The second argument lets you map to property values instead of the target entity:
const settingsDictionary = user.settings.indexBy('key', 'value');
// `settingsDictionary` is `Record<string, string>`