One_to_one
Chapter 26: One-to-One Relationships
Section titled “Chapter 26: One-to-One Relationships”Mastering One-to-One Entity Relationships
Section titled “Mastering One-to-One Entity Relationships”26.1 One-to-One Relationship Overview
Section titled “26.1 One-to-One Relationship Overview”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
================================================================================26.2 Basic One-to-One Relationship
Section titled “26.2 Basic One-to-One Relationship”Owning Side (Profile Entity)
Section titled “Owning Side (Profile Entity)”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;}Inverse Side (User Entity)
Section titled “Inverse Side (User Entity)”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;}26.3 Database Schema
Section titled “26.3 Database Schema” 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)
================================================================================26.4 Bidirectional vs Unidirectional
Section titled “26.4 Bidirectional vs Unidirectional”Bidirectional (Both sides can navigate)
Section titled “Bidirectional (Both sides can navigate)”// Both entities can access each otherconst 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)”// 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}26.5 Cascading Operations
Section titled “26.5 Cascading Operations”Cascade Insert
Section titled “Cascade Insert”@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 operationconst user = userRepository.create({ name: 'John Doe', email: 'john@example.com', profile: { bio: 'Software Developer', avatar: 'avatar.jpg', },});await userRepository.save(user);Cascade Options
Section titled “Cascade Options”@OneToOne(() => Profile, (profile) => profile.user, { cascade: true, // All cascade operations // or specific operations: // cascade: ['insert', 'update', 'remove', 'soft-remove', 'recover'],})profile: Profile;Cascade Remove
Section titled “Cascade Remove”@OneToOne(() => Profile, (profile) => profile.user, { cascade: ['insert', 'update', 'remove'],})profile: Profile;
// When user is deleted, profile is also deletedawait userRepository.remove(user);26.6 Eager vs Lazy Loading
Section titled “26.6 Eager vs Lazy Loading”Eager Loading
Section titled “Eager Loading”@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 loadedconst user = await userRepository.findOne({ where: { id: 1 } });console.log(user.profile.bio); // Already loadedLazy Loading
Section titled “Lazy Loading”@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 relationconst 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”Creating Related Entities
Section titled “Creating Related Entities”// Method 1: Create separately and linkconst 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 cascadeconst user = await userRepository.save({ name: 'John Doe', email: 'john@example.com', profile: { bio: 'Software Developer', avatar: 'avatar.jpg', },});
// Method 3: Using QueryBuilderawait 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();Reading Related Entities
Section titled “Reading Related Entities”// Using find optionsconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['profile'],});
// Using QueryBuilder with joinconst user = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.profile', 'profile') .where('user.id = :id', { id: 1 }) .getOne();
// Using relation loadconst user = await userRepository.findOne({ where: { id: 1 } });const profile = await userRepository .createQueryBuilder() .relation(User, 'profile') .of(user) .loadOne();Updating Related Entities
Section titled “Updating Related Entities”// Update profile through userconst user = await userRepository.findOne({ where: { id: 1 }, relations: ['profile'],});
user.profile.bio = 'Updated bio';await userRepository.save(user);
// Update profile directlyconst profile = await profileRepository.findOne({ where: { user: { id: 1 } },});
profile.bio = 'Updated bio';await profileRepository.save(profile);
// Using QueryBuilderawait profileRepository .createQueryBuilder() .update(Profile) .set({ bio: 'Updated bio' }) .where('userId = :userId', { userId: 1 }) .execute();Deleting Related Entities
Section titled “Deleting Related Entities”// Delete profile onlyconst 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 QueryBuilderawait profileRepository .createQueryBuilder() .delete() .from(Profile) .where('userId = :userId', { userId: 1 }) .execute();26.8 Advanced One-to-One Patterns
Section titled “26.8 Advanced One-to-One Patterns”Self-Referencing One-to-One
Section titled “Self-Referencing One-to-One”// 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;}One-to-One with Custom Join Column
Section titled “One-to-One with Custom Join Column”@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;}One-to-One with Default Value
Section titled “One-to-One with Default Value”@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;}26.9 Query Examples
Section titled “26.9 Query Examples”Find User with Profile
Section titled “Find User with Profile”// Basic find with relationconst userWithProfile = await userRepository.findOne({ where: { id: 1 }, relations: ['profile'],});
// QueryBuilder with joinconst userWithProfile = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.profile', 'profile') .where('user.id = :id', { id: 1 }) .getOne();
// Select specific columnsconst 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 by Profile Criteria
Section titled “Find Users by Profile Criteria”// Find users with specific bioconst users = await userRepository .createQueryBuilder('user') .leftJoinAndSelect('user.profile', 'profile') .where('profile.bio ILIKE :bio', { bio: '%developer%' }) .getMany();
// Find users without profileconst usersWithoutProfile = await userRepository .createQueryBuilder('user') .leftJoin('user.profile', 'profile') .where('profile.id IS NULL') .getMany();Count with Relations
Section titled “Count with Relations”// Count users with profileconst count = await userRepository .createQueryBuilder('user') .leftJoin('user.profile', 'profile') .where('profile.id IS NOT NULL') .getCount();26.10 Complete Example
Section titled “26.10 Complete Example”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.tsimport { 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.tsimport { 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); }}26.11 Summary
Section titled “26.11 Summary” 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 | | | +------------------------------------------------------------------+Next Chapter
Section titled “Next Chapter”Chapter 27: One-to-Many & Many-to-One Relationships
Last Updated: February 2026