阿里云主机折上折
  • 微信号
Current Site:Index > Using ORM tools

Using ORM tools

Author:Chuan Chen 阅读数:11472人阅读 分类: Node.js

ORM (Object-Relational Mapping) tools play a crucial role in Node.js development by simplifying database operations, allowing developers to handle data in an object-oriented manner without writing SQL directly. Common Node.js ORM tools include Sequelize, TypeORM, Prisma, etc., each with unique features suited for different scenarios.

Basic Concepts of ORM Tools

The core idea of ORM is to map database tables to objects in a programming language, where each row in a table corresponds to an object instance, and columns correspond to object properties. This approach enables developers to interact with databases using familiar programming languages without worrying about low-level SQL details. For example, a user table might map to a User class:

class User {
  constructor(id, name, email) {
    this.id = id;
    this.name = name;
    this.email = email;
  }
}

ORM tools typically provide the following functionalities:

  • Model Definition: Define database table structures as classes or objects in code.
  • CRUD Operations: Convenient methods for creating, reading, updating, and deleting data.
  • Relationships: Handle associations between tables, such as one-to-one, one-to-many, and many-to-many.
  • Query Builder: Chainable methods to construct complex queries.
  • Transaction Management: Support for database transactions.
  • Data Validation: Validate data before saving it to the database.

Using Sequelize

Sequelize is a popular Node.js ORM that supports PostgreSQL, MySQL, MariaDB, SQLite, and SQL Server. Below is a complete example demonstrating its usage.

First, install Sequelize and the database driver:

npm install sequelize
npm install pg # Example for PostgreSQL

Define models and initialize the connection:

const { Sequelize, DataTypes } = require('sequelize');

// Initialize connection
const sequelize = new Sequelize('database', 'username', 'password', {
  host: 'localhost',
  dialect: 'postgres'
});

// Define User model
const User = sequelize.define('User', {
  name: {
    type: DataTypes.STRING,
    allowNull: false
  },
  email: {
    type: DataTypes.STRING,
    unique: true,
    validate: {
      isEmail: true
    }
  },
  age: {
    type: DataTypes.INTEGER,
    defaultValue: 18
  }
}, {
  timestamps: true // Automatically add createdAt and updatedAt fields
});

// Define Post model
const Post = sequelize.define('Post', {
  title: DataTypes.STRING,
  content: DataTypes.TEXT
});

// Define relationships
User.hasMany(Post); // A user can have many posts
Post.belongsTo(User); // A post belongs to a user

Basic CRUD operations example:

// Create a record
const newUser = await User.create({
  name: '张三',
  email: 'zhangsan@example.com',
  age: 25
});

// Query records
const users = await User.findAll({
  where: {
    age: {
      [Sequelize.Op.gt]: 20 // Age greater than 20
    }
  },
  include: Post // Include associated Post data
});

// Update a record
await User.update(
  { age: 26 },
  { where: { id: 1 } }
);

// Delete a record
await User.destroy({
  where: { id: 1 }
});

Using TypeORM

TypeORM is an ORM that supports TypeScript and JavaScript, particularly well-suited for use with the NestJS framework. It uses decorator syntax to define models, aligning with modern JavaScript development practices.

Install TypeORM:

npm install typeorm reflect-metadata

Define entities (models):

import { Entity, PrimaryGeneratedColumn, Column, OneToMany } from "typeorm";
import { Post } from "./Post";

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @Column({ default: 18 })
  age: number;

  @OneToMany(() => Post, post => post.author)
  posts: Post[];
}

@Entity()
export class Post {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  title: string;

  @Column("text")
  content: string;

  @ManyToOne(() => User, user => user.posts)
  author: User;
}

Database connection and operations:

import "reflect-metadata";
import { createConnection } from "typeorm";
import { User } from "./entity/User";

createConnection().then(async connection => {
  // Create a user
  const user = new User();
  user.name = "李四";
  user.email = "lisi@example.com";
  await connection.manager.save(user);

  // Query users and their posts
  const users = await connection
    .getRepository(User)
    .createQueryBuilder("user")
    .leftJoinAndSelect("user.posts", "post")
    .where("user.age > :age", { age: 20 })
    .getMany();

  // Update a user
  await connection
    .createQueryBuilder()
    .update(User)
    .set({ age: 26 })
    .where("id = :id", { id: 1 })
    .execute();
}).catch(error => console.log(error));

Using Prisma

Prisma is a next-generation ORM tool that offers an intuitive data modeling approach and strong type safety. Its core is the schema file, which defines the data model and database connection.

Install Prisma CLI:

npm install -g prisma
npm install @prisma/client

Define the data model (schema.prisma):

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id    Int     @id @default(autoincrement())
  name  String
  email String  @unique
  age   Int     @default(18)
  posts Post[]
}

model Post {
  id       Int     @id @default(autoincrement())
  title    String
  content  String?
  author   User    @relation(fields: [authorId], references: [id])
  authorId Int
}

Use Prisma Client to interact with the database:

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

async function main() {
  // Create a user
  const user = await prisma.user.create({
    data: {
      name: '王五',
      email: 'wangwu@example.com',
      posts: {
        create: {
          title: '我的第一篇文章',
          content: '这是文章内容...'
        }
      }
    },
    include: {
      posts: true
    }
  })

  // Query users
  const usersWithPosts = await prisma.user.findMany({
    where: {
      age: {
        gt: 20
      }
    },
    include: {
      posts: true
    }
  })

  // Update a user
  const updatedUser = await prisma.user.update({
    where: { id: 1 },
    data: { age: 26 }
  })
}

main()
  .catch(e => {
    throw e
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

Advanced ORM Features

Transaction Handling

Transactions are a critical concept in database operations. ORMs provide various ways to handle them:

Sequelize transactions:

// Manual transaction management
const transaction = await sequelize.transaction();
try {
  const user = await User.create({
    name: '赵六',
    email: 'zhaoliu@example.com'
  }, { transaction });

  await Post.create({
    title: '事务测试',
    content: '在事务中创建的文章',
    userId: user.id
  }, { transaction });

  await transaction.commit();
} catch (error) {
  await transaction.rollback();
}

// Automatic transaction management
await sequelize.transaction(async (t) => {
  const user = await User.create({
    name: '赵六',
    email: 'zhaoliu@example.com'
  }, { transaction: t });

  await Post.create({
    title: '事务测试',
    content: '在事务中创建的文章',
    userId: user.id
  }, { transaction: t });
});

Data Validation

ORMs often include data validation features to validate data before saving:

// Validation in Sequelize
const User = sequelize.define('User', {
  email: {
    type: DataTypes.STRING,
    validate: {
      isEmail: true,
      notEmpty: true,
      len: [5, 100]
    }
  },
  age: {
    type: DataTypes.INTEGER,
    validate: {
      min: 0,
      max: 120
    }
  }
});

// Validation errors throw exceptions
try {
  await User.create({ email: 'invalid', age: 150 });
} catch (error) {
  console.error(error.errors); // Display validation errors
}

Query Optimization

ORMs provide various query optimization techniques, such as eager loading, pagination, and sorting:

// Eager loading associations (avoid N+1 queries)
const usersWithPosts = await User.findAll({
  include: [{
    model: Post,
    where: { title: { [Sequelize.Op.like]: '%测试%' } }
  }],
  limit: 10,
  offset: 20,
  order: [['createdAt', 'DESC']]
});

// Use raw queries for performance
const [results, metadata] = await sequelize.query(
  'SELECT * FROM users WHERE age > $1 LIMIT 10',
  {
    bind: [20],
    type: Sequelize.QueryTypes.SELECT
  }
);

ORM Performance Considerations

While ORMs simplify development, they can introduce performance issues:

  1. N+1 Query Problem: Fetching primary records and their associations without eager loading can result in excessive queries.

    // Bad practice: N+1 queries
    const users = await User.findAll();
    for (const user of users) {
      const posts = await user.getPosts(); // One query per user
    }
    
    // Good practice: Eager load associations
    const users = await User.findAll({ include: Post });
    
  2. Over-fetching Data: ORMs may return more data than needed.

    // Select only required fields
    const users = await User.findAll({
      attributes: ['id', 'name'],
      where: { age: { [Sequelize.Op.gt]: 20 } }
    });
    
  3. Complex Queries May Be Inefficient: For highly complex queries, raw SQL might be more efficient.

Migrations and Synchronization

ORMs typically provide migration tools to manage database schema changes:

Sequelize migration example:

npx sequelize-cli migration:generate --name add-avatar-to-user

Generated migration file:

module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.addColumn('Users', 'avatar', {
      type: Sequelize.STRING,
      allowNull: true
    });
  },

  down: async (queryInterface) => {
    await queryInterface.removeColumn('Users', 'avatar');
  }
};

Run migrations:

npx sequelize-cli db:migrate

Rollback migrations:

npx sequelize-cli db:migrate:undo

Using ORM in Testing

When using ORM in tests, ensure data isolation and cleanup:

describe('User Service', () => {
  let transaction;

  beforeEach(async () => {
    // Start a transaction before each test
    transaction = await sequelize.transaction();
  });

  afterEach(async () => {
    // Rollback transaction after each test to avoid persisting test data
    await transaction.rollback();
  });

  it('should create user', async () => {
    const user = await User.create({
      name: '测试用户',
      email: 'test@example.com'
    }, { transaction });

    expect(user.id).toBeDefined();
  });
});

Combining ORM with Raw SQL

While ORMs provide convenient abstractions, raw SQL may sometimes be more appropriate:

// Using raw SQL in Sequelize
const [users, metadata] = await sequelize.query(
  `SELECT u.name, COUNT(p.id) as post_count 
   FROM Users u
   LEFT JOIN Posts p ON p.userId = u.id
   GROUP BY u.id
   HAVING post_count > 5`,
  {
    type: Sequelize.QueryTypes.SELECT
  }
);

// Using QueryBuilder in TypeORM for complex queries
const users = await connection
  .getRepository(User)
  .createQueryBuilder("user")
  .select("user.id", "id")
  .addSelect("COUNT(post.id)", "postCount")
  .leftJoin("user.posts", "post")
  .groupBy("user.id")
  .having("postCount > :count", { count: 5 })
  .getRawMany();

Common Issues and Solutions

  1. Connection Pool Issues:

    // Configure connection pool
    const sequelize = new Sequelize('database', 'username', 'password', {
      host: 'localhost',
      dialect: 'postgres',
      pool: {
        max: 10,
        min: 0,
        idle: 10000
      }
    });
    
  2. Long-running Transactions: Avoid long transactions by setting timeouts.

    const transaction = await sequelize.transaction({
      isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.READ_COMMITTED,
      timeout: 5000 // 5-second timeout
    });
    
  3. Model Definition Conflicts: Ensure model and table names are correctly mapped.

    const User = sequelize.define('User', {
      // Field definitions
    }, {
      tableName: 'app_users' // Explicitly specify table name
    });
    
  4. Timezone Issues: Standardize timezone settings.

    const sequelize = new Sequelize('database', 'username', 'password', {
      dialect: 'mysql',
      timezone: '+08:00' // Set to UTC+8
    });
    

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

上一篇:GraphQL实现

下一篇:模板引擎

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.