In my NestJS project I have below entities with OneToMany / ManyToOne relations:
faculty.entity.ts:
#OneToMany(
type => Society,
society => society.faculty,
{ eager: false })
societies: Society[];
society.entity.ts:
#ManyToOne(
type => Faculty,
faculty => faculty.societies,
{ onDelete: 'CASCADE' })
#JoinColumn()
faculty: Faculty;
#Column()
facultyId: number;
I want to implement a functionality to change the Faculty a Society is bound to.
So, in societies.service.ts I have patch method:
async patch(id: number, patchSocietyDto: PatchSocietyDto): Promise<Society> {
const society = await this.findOne(id);
for (const prop in patchSocietyDto) society[prop] = patchSocietyDto[prop];
try {
await society.save();
} catch (err) {
throw new InternalServerErrorException(err);
}
return society;
}
I would like to pass the "facultyId" property in a request body, remove existing relation and then add a new one between Faculty with id "facultyId".
How can I achieve that?
I finally got to the solution. The only thing I had to change was "eager" option set to true in One-To-Many relation:
#OneToMany(
type => Society,
society => society.faculty,
{ eager: true },
)
societies: Society[];
With this change, relation updates by passing a new value for "facultyId" property.
Related
I have 2 entities like the ones below. My question is what is the easiest / most efficient way to return the number of total votes that are linked to a given post?
A PostVote has an enum vote that can be either Up or Down.
I was thinking about using the serializer, but you would still return all of the PostVote rows just to get a count essentially.
Should I just do separate em.count() queries or is there a better way?
#Entity()
export class Post extends Node<Post> {
#ManyToOne(() => User, { wrappedReference: true })
user: IdentifiedReference<User>;
#Property({ columnType: 'text' })
title: string;
#Property({ columnType: 'text', nullable: true })
description?: string;
#OneToMany(() => PostVotes, (pv) => pv.post, {
serializer: (v: PostVotes[]) => {
const upVotes = v.filter((v2) => v2.vote.Up).length;
const downVotes = v.filter((v2) => v2.vote.Down).length;
return upVotes - downVotes;
},
})
votes = new Collection<PostVotes>(this);
#Entity()
export class PostVotes extends BaseEntity<
PostVotesConstructorValues & PrivatePostVotesProperties,
'post' | 'user'
> {
#ManyToOne(() => Post, { wrappedReference: true, primary: true })
post: IdentifiedReference<Post>;
#ManyToOne(() => User, { wrappedReference: true, primary: true })
user: IdentifiedReference<User>;
[PrimaryKeyType]?: [IdentifiedReference<User>, IdentifiedReference<User>];
#Enum({ items: () => PostVote })
vote = PostVote;
Simple, yet more performant solution, would be to define a #Formula() property that loads the count via subquery. You could mark such property as lazy: true so it would not be selected automatically, only if you populate it.
Not tested, but something like this should work:
#Entity()
export class Post extends Node<Post> {
// keep the 1:m as a way to modify the collection
#OneToMany(() => PostVotes, (pv) => pv.post)
votes = new Collection<PostVotes>(this);
// add a subquery formula for easy reading of just the number
#Formula(alias => `(select count(*) from vote_count vc where vc.post = ${alias}.id)`, { lazy: true })
votesCount?: number;
}
And to read it:
const posts = await em.find(Post, {}, { populate: ['votesCount'] });
https://mikro-orm.io/docs/defining-entities#formulas
Similar can be achieved via virtual entities, depends on the use case:
https://mikro-orm.io/docs/virtual-entities
For performance critical paths it would make sense to have it as a real persistent property and maintain/sync its value when you create/remote votes.
I'm writing because I have a question while dealing with the mapping table. When creating a user on a web page, I want to put existing information and data called equipment. Each user can start with multiple equipment, so they created a mapping table like a picture to resolve the N:M relationship.
However, in order to put data in the mapping table in typeorm, you must first create a user object and an item object. After turning the loop as many times as the number of equipment set to the user to find the equipment number received from the web, we are creating an equipment object and inserting it into the mapping table.
Is there a better way than what I'm doing?
await Promise.all(
items.map(async (element) => {
const uItem = new UserItem();
uItem.item = element.item;
uItem.user = user;
uItem.period = element.period;
await transactionManager.save(uItem);
})
);
typeORM has an easy solution for it
So you define your 2 main entities like this
#Entity()
export class Item{
#PrimaryGeneratedColumn('uuid')
id: string; // or whatever you like
#OneToMany(() => UserItems, userItem => userItem.item, {
nullable: true,
cascade: true,
})
userItems: UserItem[];
...
#Entity()
export class User{
#PrimaryGeneratedColumn('uuid')
id: string; // or whatever you like
#OneToMany(() => UserItems, userItem => userItem.user, {
nullable: true,
cascade: true,
})
userItems: UserItem[];
...
And your mapping class as following:
#Entity()
export class UserItem{
#PrimaryGeneratedColumn('uuid')
id: string; // or whatever you like
#ManyToOne(() => User, {
onDelete: 'CASCADE', // decide on delete logic
})
user: User;
#ManyToOne(() => Item, {
onDelete: 'CASCADE', // decide on delete logic
})
item: Item;
...
I'm currently implementing an M:M relationship between a user and a product that uses a custom join table for implementing a shopping cart. Here is the custom join table:
#Entity({ tableName: "users_in_cart_products" })
export class UserInCartProducts {
#ManyToOne(() => User, { primary: true, nullable: true })
user: User;
#ManyToOne(() => Product, { primary: true, nullable: true })
product: Product;
#Property()
amount: number;
}
Parts of the user entity (similar to Product)
#Entity({ tableName: "users", customRepository: () => UserRepository })
export class User {
#PrimaryKey()
id: string = v4();
/* some properties */
#OneToMany(() => UserInCartProducts, (userInCartProducts) => userInCartProducts.user)
userInCartProducts = new Collection<UserInCartProducts>(this);
}
I'm currently implementing a functionality where a product will be deleted from the shopping cart. However, when I call user.userInCartProducts.remove(), instead of just removing that element, it removes everything from user.userInCartProducts, leaving an empty array. Here's the code that removes the product from a user's cart:
async removeCartItem(userId: string, productId: string) {
const user = await this.userRepository.findOneOrFail({ id: userId }, [
"userInCartProducts",
"userInCartProducts.product",
]);
for (const e of user.userInCartProducts) {
if (e.product.id === productId) user.userInCartProducts.remove(e);
}
await this.userRepository.persistAndFlush(user);
return user;
}
I've checked the SQL generated from Mikro-orm, and somehow it sets user_id to NULL for everything inside the users_in_cart_products join table:
[query] update "users_in_cart_products" set "user_id" = NULL [took 2808 ms]
So how can I solve this problem? I just want the collection to remove that one item, but not every single item. Thanks!
I have 3 tables as following:
users:
id
families:
id
families_users:
familyId | userId
I have these relations:
// User
#OneToMany(() => FamilyUser, (familyUser) => familyUser.user)
familyUsers: FamilyUser[];
// Family
#OneToMany(() => FamilyUser, (familyUser) => familyUser.family)
familyUsers: FamilyUser[];
// FamilyUser
#ManyToOne(() => User, (user) => user.familyUsers, { nullable: false })
user: User;
#ManyToOne(() => Family, (family) => family.familyUsers, { nullable: false })
family: Family;
I want to get a particular user's family list. The first option is:
await this.familiesUsersRepository.find({
relations: ['family'],
where: {
user: { id: 6 },
},
});
But in this case, I get the list of FamilyUsers which each one contains a family object. But I want to return the list of families that belongs to the user. I tried this:
return await this.familiesRepository.find({
relations: ['familyUsers', 'familyUsers.user'],
where: {
// ???
},
});
But have no idea what I should set in the where clause. Any idea?
I don't know if I got your question but I think you can return one user's family simply by going from userRepository :
let idUser = 6 ;
return await this.userRepository.findOne(idUser,{relations: ['familyUsers']});
UPDATED
return await this.familyRepository.createQueryBuilder('family')
.leftJoinAndSelect('family.familyUsers', 'familyusers')
.where("familyusers.user = :idUser", { idUser }) // if familyusers.user doesn't work replace 'user' with the name of colmun in the table
.getMany()
I have two tables:
#Entity('Reviews')
class Review {
...
#OneToMany((type) => MapCategory, map => map.review)
public categories: MapCategory[];
}
And:
#Entity('MapCategories')
export class MapCategory {
...
#ManyToOne(type => Review, (review) => review.categories)
public review: Review;
}
When I try the filter on 'categories' but the result doesn't filter 'categories' following the key that I already push.
const items = await this.reviewRepository.findAndCount({
relations: ['categories'],
where: {
categories: {
id: 1
}
}
});
We need to use queryBuilder for cases like this since find doesn't allow filtering relations:
const items = await reviewRepository.createQueryBuilder("review")
.leftJoinAndSelect("review.categories", "category")
.where("category.id = :id", { id })
.getManyAndCount()
I prefer to avoid query builder when possible.
There's a workaround for filtering based on relation fields for findOne()/find() methods that I've discovered recently. The problem with filtering related table fields only exists for ObjectLiteral-style where, while string conditions work perfectly.
Assume that we have two entities – User and Role, user belongs to one role, role has many users:
#Entity()
export class User {
name: string;
#ManyToOne(() => Role, role => role.users)
role: Role;
}
#Entity()
export class Role {
#OneToMany(() => User, user => user.role)
users: User[];
}
Now we can call findOne()/find() methods of EntityManager or repository:
roleRepository.find({
join: { alias: 'roles', innerJoin: { users: 'roles.users' } },
where: qb => {
qb.where({ // Filter Role fields
a: 1,
b: 2
}).andWhere('users.name = :userName', { userName: 'John Doe' }); // Filter related field
}
});
You can omit the join part if you've marked your relation as an eager one.