Skip to content

One_to_one


A one-to-one relationship connects two entities where each instance of one entity is associated with exactly one instance of another entity.

One-to-One Relationship
================================================================================
+------------------+ +------------------+
| User | | Profile |
+------------------+ +------------------+
| id (PK) | 1:1 | id (PK) |
| name | <-----> | bio |
| email | | avatar |
| | | userId (FK) |
+------------------+ +------------------+
|
|
Foreign Key
References User.id
Example:
- User 1 -----> Profile 1
- User 2 -----> Profile 2
- User 3 -----> Profile 3
================================================================================

src/profiles/profile.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne, JoinColumn } from 'typeorm';
import { User } from '../users/user.entity';
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true })
bio: string;
@Column({ nullable: true })
avatar: string;
@Column({ nullable: true })
website: string;
// Owning side - has the foreign key
@OneToOne(() => User, (user) => user.profile)
@JoinColumn() // Creates userId column as foreign key
user: User;
}
src/users/user.entity.ts
import { Entity, PrimaryGeneratedColumn, Column, OneToOne } from 'typeorm';
import { Profile } from '../profiles/profile.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
// Inverse side - references the owning side
@OneToOne(() => Profile, (profile) => profile.user)
profile: Profile;
}

Generated Database Schema
================================================================================
Table: users
+------------+----------+---------------+
| Column | Type | Constraints |
+------------+----------+---------------+
| id | integer | PRIMARY KEY |
| name | varchar | NOT NULL |
| email | varchar | UNIQUE |
+------------+----------+---------------+
Table: profiles
+------------+----------+---------------+
| Column | Type | Constraints |
+------------+----------+---------------+
| id | integer | PRIMARY KEY |
| bio | varchar | |
| avatar | varchar | |
| website | varchar | |
| userId | integer | UNIQUE FK |
+------------+----------+---------------+
|
| FOREIGN KEY
v
users.id
Note: userId is UNIQUE because it's a one-to-one relationship
(each user can only have one profile)
================================================================================

// Both entities can access each other
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['profile']
});
console.log(user.profile.bio);
const profile = await profileRepository.findOne({
where: { id: 1 },
relations: ['user']
});
console.log(profile.user.name);

Unidirectional (Only one side can navigate)

Section titled “Unidirectional (Only one side can navigate)”
src/profiles/profile.entity.ts
// Only the owning side can access the other entity
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
bio: string;
@OneToOne(() => User)
@JoinColumn()
user: User;
}
// src/users/user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// No reference to Profile - cannot navigate from User to Profile
}

src/users/user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile, (profile) => profile.user, {
cascade: true, // Automatically insert/update related entity
eager: false,
})
profile: Profile;
}
// Usage - Create user with profile in one operation
const user = userRepository.create({
name: 'John Doe',
email: 'john@example.com',
profile: {
bio: 'Software Developer',
avatar: 'avatar.jpg',
},
});
await userRepository.save(user);
@OneToOne(() => Profile, (profile) => profile.user, {
cascade: true, // All cascade operations
// or specific operations:
// cascade: ['insert', 'update', 'remove', 'soft-remove', 'recover'],
})
profile: Profile;
@OneToOne(() => Profile, (profile) => profile.user, {
cascade: ['insert', 'update', 'remove'],
})
profile: Profile;
// When user is deleted, profile is also deleted
await userRepository.remove(user);

src/users/user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile, (profile) => profile.user, {
eager: true, // Always load profile with user
})
profile: Profile;
}
// Profile is automatically loaded
const user = await userRepository.findOne({ where: { id: 1 } });
console.log(user.profile.bio); // Already loaded
src/users/user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile, (profile) => profile.user, {
eager: false, // Default - don't auto-load
})
profile: Profile;
}
// Must explicitly load the relation
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['profile'],
});
console.log(user.profile.bio);

26.7 Working with One-to-One Relationships

Section titled “26.7 Working with One-to-One Relationships”
// Method 1: Create separately and link
const user = await userRepository.save({
name: 'John Doe',
email: 'john@example.com',
});
const profile = await profileRepository.save({
bio: 'Software Developer',
avatar: 'avatar.jpg',
user: user, // Link to user
});
// Method 2: Create with cascade
const user = await userRepository.save({
name: 'John Doe',
email: 'john@example.com',
profile: {
bio: 'Software Developer',
avatar: 'avatar.jpg',
},
});
// Method 3: Using QueryBuilder
await userRepository
.createQueryBuilder()
.insert()
.into(User)
.values({
name: 'John Doe',
email: 'john@example.com',
})
.execute();
await profileRepository
.createQueryBuilder()
.insert()
.into(Profile)
.values({
bio: 'Software Developer',
user: { id: userId },
})
.execute();
// Using find options
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['profile'],
});
// Using QueryBuilder with join
const user = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.where('user.id = :id', { id: 1 })
.getOne();
// Using relation load
const user = await userRepository.findOne({ where: { id: 1 } });
const profile = await userRepository
.createQueryBuilder()
.relation(User, 'profile')
.of(user)
.loadOne();
// Update profile through user
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['profile'],
});
user.profile.bio = 'Updated bio';
await userRepository.save(user);
// Update profile directly
const profile = await profileRepository.findOne({
where: { user: { id: 1 } },
});
profile.bio = 'Updated bio';
await profileRepository.save(profile);
// Using QueryBuilder
await profileRepository
.createQueryBuilder()
.update(Profile)
.set({ bio: 'Updated bio' })
.where('userId = :userId', { userId: 1 })
.execute();
// Delete profile only
const profile = await profileRepository.findOne({
where: { user: { id: 1 } },
});
await profileRepository.remove(profile);
// Delete user with cascade (also deletes profile)
const user = await userRepository.findOne({
where: { id: 1 },
relations: ['profile'],
});
await userRepository.remove(user);
// Delete using QueryBuilder
await profileRepository
.createQueryBuilder()
.delete()
.from(Profile)
.where('userId = :userId', { userId: 1 })
.execute();

// User can have one mentor (another user)
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
// Mentor relationship
@OneToOne(() => User, (user) => user.mentee)
@JoinColumn({ name: 'mentorId' })
mentor: User;
// Mentee relationship (inverse side)
@OneToOne(() => User, (user) => user.mentor)
mentee: User;
}
src/profiles/profile.entity.ts
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column()
bio: string;
@OneToOne(() => User)
@JoinColumn({
name: 'user_id', // Custom column name
referencedColumnName: 'id', // Reference column in User
})
user: User;
}
src/users/user.entity.ts
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@OneToOne(() => Profile, (profile) => profile.user, {
cascade: true,
eager: true,
default: {}, // Default empty profile
})
profile: Profile;
}

// Basic find with relation
const userWithProfile = await userRepository.findOne({
where: { id: 1 },
relations: ['profile'],
});
// QueryBuilder with join
const userWithProfile = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.where('user.id = :id', { id: 1 })
.getOne();
// Select specific columns
const userWithProfile = await userRepository
.createQueryBuilder('user')
.leftJoin('user.profile', 'profile')
.select([
'user.id',
'user.name',
'profile.bio',
'profile.avatar',
])
.where('user.id = :id', { id: 1 })
.getOne();
// Find users with specific bio
const users = await userRepository
.createQueryBuilder('user')
.leftJoinAndSelect('user.profile', 'profile')
.where('profile.bio ILIKE :bio', { bio: '%developer%' })
.getMany();
// Find users without profile
const usersWithoutProfile = await userRepository
.createQueryBuilder('user')
.leftJoin('user.profile', 'profile')
.where('profile.id IS NULL')
.getMany();
// Count users with profile
const count = await userRepository
.createQueryBuilder('user')
.leftJoin('user.profile', 'profile')
.where('profile.id IS NOT NULL')
.getCount();

src/users/user.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { Profile } from '../profiles/profile.entity';
@Entity()
export class User {
@PrimaryGeneratedColumn()
id: number;
@Column()
name: string;
@Column({ unique: true })
email: string;
@Column({ select: false })
password: string;
@Column({ default: true })
isActive: boolean;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToOne(() => Profile, (profile) => profile.user, {
cascade: true,
eager: false,
onDelete: 'CASCADE', // Delete profile when user is deleted
})
profile: Profile;
}
// src/profiles/profile.entity.ts
import {
Entity,
PrimaryGeneratedColumn,
Column,
OneToOne,
JoinColumn,
CreateDateColumn,
UpdateDateColumn,
} from 'typeorm';
import { User } from '../users/user.entity';
@Entity()
export class Profile {
@PrimaryGeneratedColumn()
id: number;
@Column({ nullable: true, length: 500 })
bio: string;
@Column({ nullable: true })
avatar: string;
@Column({ nullable: true })
website: string;
@Column({ nullable: true })
location: string;
@CreateDateColumn()
createdAt: Date;
@UpdateDateColumn()
updatedAt: Date;
@OneToOne(() => User, (user) => user.profile)
@JoinColumn({ name: 'userId' })
user: User;
}
// src/users/users.service.ts
import { Injectable, NotFoundException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { User } from './user.entity';
import { Profile } from '../profiles/profile.entity';
@Injectable()
export class UsersService {
constructor(
@InjectRepository(User)
private userRepository: Repository<User>,
@InjectRepository(Profile)
private profileRepository: Repository<Profile>,
) {}
async createWithProfile(userData: any, profileData: any): Promise<User> {
const user = this.userRepository.create({
...userData,
profile: profileData,
});
return this.userRepository.save(user);
}
async findOneWithProfile(id: number): Promise<User> {
const user = await this.userRepository.findOne({
where: { id },
relations: ['profile'],
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
return user;
}
async updateProfile(userId: number, profileData: any): Promise<Profile> {
const profile = await this.profileRepository.findOne({
where: { user: { id: userId } },
});
if (!profile) {
// Create new profile
const newProfile = this.profileRepository.create({
...profileData,
user: { id: userId },
});
return this.profileRepository.save(newProfile);
}
// Update existing profile
Object.assign(profile, profileData);
return this.profileRepository.save(profile);
}
async deleteWithProfile(id: number): Promise<void> {
const user = await this.userRepository.findOne({
where: { id },
relations: ['profile'],
});
if (!user) {
throw new NotFoundException(`User with ID ${id} not found`);
}
// Profile will be deleted automatically due to CASCADE
await this.userRepository.remove(user);
}
}

One-to-One Relationship Quick Reference
+------------------------------------------------------------------+
| |
| Decorator | Purpose |
| -------------------|------------------------------------------|
| @OneToOne() | Define one-to-one relationship |
| @JoinColumn() | Specify foreign key column |
| |
| Options | Description |
| -------------------|------------------------------------------|
| cascade | Auto-save related entities |
| eager | Always load with parent |
| onDelete | Action on parent delete |
| nullable | Allow null foreign key |
| |
| Best Practices | Description |
| -------------------|------------------------------------------|
| Use cascade: true | Simplify entity creation |
| Avoid eager: true | Prevent N+1 problems |
| Use onDelete | Maintain referential integrity |
| Index FK columns | Improve join performance |
| |
+------------------------------------------------------------------+

Chapter 27: One-to-Many & Many-to-One Relationships


Last Updated: February 2026